用手寫一個工具的過程講清楚 Go反射的使用方法和應用場景
今天來聊一個平時用的不多,但是很多框架或者基礎庫會用到的語言特性--反射,反射并不是Go語言獨有的能力,其他編程語言都有。這篇文章的目標是簡單地給大家梳理一下反射的應用場景和使用方法。
我們平時寫代碼能接觸到與反射聯系比較緊密的一個東西是結構體字段的標簽,這個我準備放在后面的文章再梳理。
我準備通過用反射搞一個通用的SQL構造器的例子,帶大家掌握反射這個知識點。這個是看了國外一個博主寫的例子,覺得思路很好,我又對其進行了改進,讓構造器的實現更豐富了些。
本文的思路參考自:https://golangbot.com/reflection/ ,本文內容并非只是對原文的簡單翻譯,具體看下面的內容吧~!
什么是反射
反射是程序在運行時檢查其變量和值并找到它們類型的能力。聽起來比較籠統,接下來我通過文章的例子一步步帶你認識反射。
為什么需要反射
當學習反射的時候,每個人首先會想到的問題都是 “為什么我們要在運行時檢查變量的類型呢,程序里的變量在定義的時候我們不都已經給他們指定好類型了嗎?” 確實是這樣的,但也并非總是如此,看到這你可能心里會想,大哥,你在說什么呢,em... 還是先寫一個簡單的程序,解釋一下。
- package main
- import (
- "fmt"
- )
- func main() {
- i := 10
- fmt.Printf("%d %T", i, i)
- }
在上面的程序里, 變量i的類型在編譯時是已知的,我們在下一行打印了它的值和類型。
現在讓我們理解一下 ”在運行時知道變量的類型的必要“。假設我們要編寫一個簡單的函數,它將一個結構體作為參數,并使用這個參數創建一個SQL插入語句。
考慮一下下面這個程序:
- package main
- import (
- "fmt"
- )
- type order struct {
- ordId int
- customerId int
- }
- func main() {
- o := order{
- ordId: 1234,
- customerId: 567,
- }
- fmt.Println(o)
- }
我們需要寫一個接收上面定義的結構體o作為參數,返回類似INSERT INTO order VALUES(1234, 567)這樣的SQL語句。這個函數定義寫來很容易,比如像下面這樣。
- package main
- import (
- "fmt"
- )
- type order struct {
- ordId int
- customerId int
- }
- func createQuery(o order) string {
- i := fmt.Sprintf("INSERT INTO order VALUES(%d, %d)", o.ordId, o.customerId)
- return i
- }
- func main() {
- o := order{
- ordId: 1234,
- customerId: 567,
- }
- fmt.Println(createQuery(o))
- }
上面例子的createQuery使用參數o 的ordId和customerId字段創建SQL。
現在讓我們將我們的SQL創建函數定義地更抽象些,下面還是用程序附帶說明舉一個案例,比如我們想泛化我們的SQL創建函數使其適用于任何結構體。
- package main
- type order struct {
- ordId int
- customerId int
- }
- type employee struct {
- name string
- id int
- address string
- salary int
- country string
- }
- func createQuery(q interface{}) string {
- }
現在我們的目標是,改造createQuery函數,讓它能接受任何結構作為參數并基于結構字段創建INSERT 語句。比如如果傳給createQuery的參數不再是order類型的結構體,而是employee類型的結構體時
- e := employee {
- name: "Naveen",
- id: 565,
- address: "Science Park Road, Singapore",
- salary: 90000,
- country: "Singapore",
- }
那它應該返回的INSERT語句應該是
- INSERT INTO employee (name, id, address, salary, country)
- VALUES("Naveen", 565, "Science Park Road, Singapore", 90000, "Singapore")
由于createQuery 函數要適用于任何結構體,因此它需要一個 interface{}類型的參數。為了說明問題,簡單起見,我們假定createQuery函數只處理包含string 和 int 類型字段的結構體。
編寫這個createQuery函數的唯一方法是檢查在運行時傳遞給它的參數的類型,找到它的字段,然后創建SQL。這里就是需要反射發揮用的地方啦。在后續步驟中,我們將學習如何使用Go語言的反射包來實現這一點。
Go語言的反射包
Go語言自帶的reflect包實現了在運行時進行反射的功能,這個包可以幫助識別一個interface{}類型變量其底層的具體類型和值。我們的createQuery函數接收到一個interface{}類型的實參后,需要根據這個實參的底層類型和值去創建并返回INSERT語句,這正是反射包的作用所在。
在開始編寫我們的通用SQL生成器函數之前,我們需要先了解一下reflect包中我們會用到的幾個類型和方法,接下來我們先逐個學習一下。
reflect.Type 和 reflect.Value
經過反射后interface{}類型的變量的底層具體類型由reflect.Type表示,底層值由reflect.Value表示。reflect包里有兩個函數reflect.TypeOf() 和reflect.ValueOf() 分別能將interface{}類型的變量轉換為reflect.Type和reflect.Value。這兩種類型是創建我們的SQL生成器函數的基礎。
讓我們寫一個簡單的例子來理解這兩種類型。
- package main
- import (
- "fmt"
- "reflect"
- )
- type order struct {
- ordId int
- customerId int
- }
- func createQuery(q interface{}) {
- t := reflect.TypeOf(q)
- v := reflect.ValueOf(q)
- fmt.Println("Type ", t)
- fmt.Println("Value ", v)
- }
- func main() {
- o := order{
- ordId: 456,
- customerId: 56,
- }
- createQuery(o)
- }
上面的程序會輸出:
- Type main.order
- Value {456 56}
上面的程序里createQuery函數接收一個interface{}類型的實參,然后把實參傳給了reflect.Typeof和reflect.Valueof 函數的調用。從輸出,我們可以看到程序輸出了interface{}類型實參對應的底層具體類型和值。
Go語言反射的三法則
這里插播一下反射的三法則,他們是:
- 從接口值可以反射出反射對象。
- 從反射對象可反射出接口值。
- 要修改反射對象,其值必須可設置。
反射的第一條法則是,我們能夠吧Go中的接口類型變量轉換成反射對象,上面提到的reflect.TypeOf和 reflect.ValueOf 就是完成的這種轉換。第二條指的是我們能把反射類型的變量再轉換回到接口類型,最后一條則是與反射值是否可以被更改有關。三法則詳細的說明可以去看看德萊文大神寫的文章 Go反射的實現原理,文章開頭就有對三法則說明的圖解,再次膜拜。
下面我們接著繼續了解完成我們的SQL生成器需要的反射知識。
reflect.Kind
reflect包中還有一個非常重要的類型,reflect.Kind。
reflect.Kind和reflect.Type類型可能看起來很相似,從命名上也是,Kind和Type在英文的一些Phrase是可以互轉使用的,不過在反射這塊它們有挺大區別,從下面的程序中可以清楚地看到。
- package main
- import (
- "fmt"
- "reflect"
- )
- type order struct {
- ordId int
- customerId int
- }
- func createQuery(q interface{}) {
- t := reflect.TypeOf(q)
- k := t.Kind()
- fmt.Println("Type ", t)
- fmt.Println("Kind ", k)
- }
- func main() {
- o := order{
- ordId: 456,
- customerId: 56,
- }
- createQuery(o)
- }
上面的程序會輸出
- Type main.order
- Kind struct
通過輸出讓我們清楚了兩者之間的區別。reflect.Type 表示接口的實際類型,即本例中main.order 而Kind表示類型的所屬的種類,即main.order是一個「struct」類型,類似的類型map[string]string的Kind就該是「map」。
反射獲取結構體字段的方法
我們可以通過reflect.StructField類型的方法來獲取結構體下字段的類型屬性。reflect.StructField可以通過reflect.Type提供的下面兩種方式拿到。
- // 獲取一個結構體內的字段數量
- NumField() int
- // 根據 index 獲取結構體內字段的類型對象
- Field(i int) StructField
- // 根據字段名獲取結構體內字段的類型對象
- FieldByName(name string) (StructField, bool)
reflect.structField是一個struct類型,通過它我們又能在反射里知道字段的基本類型、Tag、是否已導出等屬性。
- type StructField struct {
- Name string
- Type Type // field type
- Tag StructTag // field tag string
- ......
- }
與reflect.Type提供的獲取Field信息的方法相對應,reflect.Value也提供了獲取Field值的方法。
- func (v Value) Field(i int) Value {
- ...
- }
- func (v Value) FieldByName(name string) Value {
- ...
- }
這塊需要注意,不然容易迷惑。下面我們嘗試一下通過反射拿到order結構體類型的字段名和值
- package main
- import (
- "fmt"
- "reflect"
- )
- type order struct {
- ordId int
- customerId int
- }
- func createQuery(q interface{}) {
- t := reflect.TypeOf(q)
- if t.Kind() != reflect.Struct {
- panic("unsupported argument type!")
- }
- v := reflect.ValueOf(q)
- for i:=0; i < t.NumField(); i++ {
- fmt.Println("FieldName:", t.Field(i).Name, "FiledType:", t.Field(i).Type,
- "FiledValue:", v.Field(i))
- }
- }
- func main() {
- o := order{
- ordId: 456,
- customerId: 56,
- }
- createQuery(o)
- }
上面的程序會輸出:
- FieldName: ordId FiledType: int FiledValue: 456
- FieldName: customerId FiledType: int FiledValue: 56
除了獲取結構體字段名稱和值之外,還能獲取結構體字段的Tag,這個放在后面的文章我再總結吧,不然篇幅就太長了。
reflect.Value轉換成實際值
現在離完成我們的SQL生成器還差最后一步,即還需要把reflect.Value轉換成實際類型的值,reflect.Value實現了一系列Int(), String(),Float()這樣的方法來完成其到實際類型值的轉換。
用反射搞一個SQL生成器
上面我們已經了解完寫這個SQL生成器函數前所有的必備知識點啦,接下來就把他們串起來,加工完成createQuery函數。
這個SQL生成器完整的實現和測試代碼如下:
- package main
- import (
- "fmt"
- "reflect"
- )
- type order struct {
- ordId int
- customerId int
- }
- type employee struct {
- name string
- id int
- address string
- salary int
- country string
- }
- func createQuery(q interface{}) string {
- t := reflect.TypeOf(q)
- v := reflect.ValueOf(q)
- if v.Kind() != reflect.Struct {
- panic("unsupported argument type!")
- }
- tableName := t.Name() // 通過結構體類型提取出SQL的表名
- sql := fmt.Sprintf("INSERT INTO %s ", tableName)
- columns := "("
- values := "VALUES ("
- for i := 0; i < v.NumField(); i++ {
- // 注意reflect.Value 也實現了NumField,Kind這些方法
- // 這里的v.Field(i).Kind()等價于t.Field(i).Type.Kind()
- switch v.Field(i).Kind() {
- case reflect.Int:
- if i == 0 {
- columns += fmt.Sprintf("%s", t.Field(i).Name)
- values += fmt.Sprintf("%d", v.Field(i).Int())
- } else {
- columns += fmt.Sprintf(", %s", t.Field(i).Name)
- values += fmt.Sprintf(", %d", v.Field(i).Int())
- }
- case reflect.String:
- if i == 0 {
- columns += fmt.Sprintf("%s", t.Field(i).Name)
- values += fmt.Sprintf("'%s'", v.Field(i).String())
- } else {
- columns += fmt.Sprintf(", %s", t.Field(i).Name)
- values += fmt.Sprintf(", '%s'", v.Field(i).String())
- }
- }
- }
- columns += "); "
- values += "); "
- sql += columns + values
- fmt.Println(sql)
- return sql
- }
- func main() {
- o := order{
- ordId: 456,
- customerId: 56,
- }
- createQuery(o)
- e := employee{
- name: "Naveen",
- id: 565,
- address: "Coimbatore",
- salary: 90000,
- country: "India",
- }
- createQuery(e)
- }
同學們可以把代碼拿到本地運行一下,上面的例子會根據傳遞給函數不同的結構體實參,輸出對應的標準SQL插入語句
- INSERT INTO order (ordId, customerId); VALUES (456, 56);
- INSERT INTO employee (name, id, address, salary, country); VALUES ('Naveen', 565, 'Coimbatore', 90000, 'India');
總結
這篇文章通過利用反射完成一個實際應用來教會大家Go語言反射的基本使用方法,雖然反射看起來挺強大,但使用反射編寫清晰且可維護的代碼非常困難,應盡可能避免,僅在絕對必要時才使用。
我的看法是如果是要寫業務代碼,根本不需要使用反射,如果要寫類似encoding/json,gorm這些樣的庫倒是可以利用反射的強大功能簡化庫使用者的編碼難度。