反射是什麼?
和 Java 語言一樣,Go 語言也有執行時反射,這為我們提供了一種可以在執行時操作任意型別物件的能力。比如檢視一個介面變數的具體型別、看看一個結構體有多少欄位、修改某個欄位的值等。
Go 語言是靜態編譯類語言,比如在定義一個變數的時候,已經知道了它是什麼型別,那麼為什麼還需要反射呢?這是因為有些事情只有在執行時才知道。比如定義了一個函式,它有一個interface{}型別的引數,這也就意味著呼叫者可以傳遞任何型別的引數給這個函式。在這種情況下,如果想知道呼叫者傳遞的是什麼型別的引數,就需要用到反射。如果想知道一個結構體有哪些欄位和方法,也需要反射。
還是以常用的函式 fmt.Println 為例,如下所示:
func Println(a ...interface{}) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
例子中 fmt.Println 的原始碼有一個可變引數,型別為 interface{},這意味著可以傳遞零個或者多個任意型別引數給它,都能被正確列印。
reflect.Value 和 reflect.Type
在 Go 語言的反射定義中,任何介面都由兩部分組成:介面的具體型別,以及具體型別對應的值。比如 var i int = 3,因為 interface{} 可以表示任何型別,所以變數 i 可以轉為 interface{}。你可以把變數 i 當成一個介面,那麼這個變數在 Go 反射中的表示就是 <Value,Type>。其中 Value 為變數的值,即 3,而 Type 為變數的型別,即 int。
小提示:interface{} 是空介面,可以表示任何型別,也就是說你可以把任何型別轉換為空介面,它通常用於反射、型別斷言,以減少重複程式碼,簡化程式設計。
在 Go 反射中,標準庫提供了兩種型別 reflect.Value 和 reflect.Type 來分別表示變數的值和型別,並且提供了兩個函式 reflect.ValueOf 和 reflect.TypeOf 分別獲取任意物件的 reflect.Value 和 reflect.Type。
用下面的程式碼進行演示:
func main() {
i:=3
iv:=reflect.ValueOf(i)
it:=reflect.TypeOf(i)
fmt.Println(iv,it)//3 int
}
程式碼定義了一個 int 型別的變數 i,它的值為 3,然後通過 reflect.ValueOf 和 reflect.TypeOf 函式就可以獲得變數 i 對應的 reflect.Value 和 reflect.Type。通過 fmt.Println 函式列印後,可以看到結果是 3 int,這也可以證明 reflect.Value 表示的是變數的值,reflect.Type 表示的是變數的型別。
reflect.Value
reflect.Value 可以通過函式 reflect.ValueOf 獲得,下面我將為你介紹它的結構和用法。
結構體定義
在 Go 語言中,reflect.Value 被定義為一個 struct 結構體,它的定義如下面的程式碼所示:
type Value struct {
typ *rtype
ptr unsafe.Pointer
flag
}
發現 reflect.Value 結構體的欄位都是私有的,也就是說,只能使用 reflect.Value 的方法。現在看看它有哪些常用方法,如下所示:
//針對具體型別的系列方法
//以下是用於獲取對應的值
Bool
Bytes
Complex
Float
Int
String
Uint
CanSet //是否可以修改對應的值
以下是用於修改對應的值
Set
SetBool
SetBytes
SetComplex
SetFloat
SetInt
SetString
Elem //獲取指標指向的值,一般用於修改對應的值
//以下Field系列方法用於獲取struct型別中的欄位
Field
FieldByIndex
FieldByName
FieldByNameFunc
Interface //獲取對應的原始型別
IsNil //值是否為nil
IsZero //值是否是零值
Kind //獲取對應的型別類別,比如Array、Slice、Map等
//獲取對應的方法
Method
MethodByName
NumField //獲取struct型別中欄位的數量
NumMethod//型別上方法集的數量
Type//獲取對應的reflect.Type
看著比較多,其實就三類:
- 一類用於獲取和修改對應的值;
- 一類和 struct 型別的欄位有關,用於獲取對應的欄位;
- 一類和型別上的方法集有關,用於獲取對應的方法。
下面通過幾個例子講解如何使用它們。
獲取原始型別
在上面的例子中,通過 reflect.ValueOf 函式把任意型別的物件轉為一個 reflect.Value,而如果想逆向轉回來也可以,reflect.Value 提供了 Inteface 方法,如下面的程式碼所示:
func main() {
i:=3
//int to reflect.Value
iv:=reflect.ValueOf(i)
//reflect.Value to int
i1:=iv.Interface().(int)
fmt.Println(i1)
}
這是 reflect.Value 和 int 型別互轉,換成其他型別也可以。
修改對應的值
已經定義的變數可以通過反射在執行時修改,比如上面的示例 i=3,修改為 4,如下所示:
func main() {
i:=3
ipv:=reflect.ValueOf(&i)
ipv.Elem().SetInt(4)
fmt.Println(i)
}
這樣就通過反射修改了一個變數。因為 reflect.ValueOf 函式返回的是一份值的拷貝,所以我們要傳入變數的指標才可以。 因為傳遞的是一個指標,所以需要呼叫 Elem 方法找到這個指標指向的值,這樣才能修改。 最後就可以使用 SetInt 方法修改值了。
要修改一個變數的值,有幾個關鍵點:傳遞指標(可定址),通過 Elem 方法獲取指向的值,才可以保證值可以被修改,reflect.Value 提供了 CanSet 方法判斷是否可以修改該變數。
那麼如何修改 struct 結構體欄位的值呢?參考變數的修改方式,可總結出以下步驟:
- 傳遞一個 struct 結構體的指標,獲取對應的 reflect.Value;
- 通過 Elem 方法獲取指標指向的值;
- 通過 Field 方法獲取要修改的欄位;
- 通過 Set 系列方法修改成對應的值。
執行下面的程式碼,會發現變數 p 中的 Name 欄位已經被修改為張三了。
func main() {
p:=person{Name: "Golang",Age: 20}
ppv:=reflect.ValueOf(&p)
ppv.Elem().Field(0).SetString("張三")
fmt.Println(p)
}
type person struct {
Name string
Age int
}
最後再來總結一下通過反射修改一個值的規則。
- 可被定址,通俗地講就是要向 reflect.ValueOf 函式傳遞一個指標作為引數。
- 如果要修改 struct 結構體欄位值的話,該欄位需要是可匯出的,而不是私有的,也就是該欄位的首字母為大寫。
- 記得使用 Elem 方法獲得指標指向的值,這樣才能呼叫 Set 系列方法進行修改。
記住以上規則,就可以在程式執行時通過反射修改一個變數或欄位的值。
獲取對應的底層型別
底層型別是什麼意思呢?其實對應的主要是基礎型別,比如介面、結構體、指標……因為可以通過 type 關鍵字宣告很多新的型別。比如在上面的例子中,變數 p 的實際型別是 person,但是 person 對應的底層型別是 struct 這個結構體型別,而 &p 對應的則是指標型別。通過下面的程式碼進行驗證:
func main() {
p:=person{Name: "Golang",Age: 20}
ppv:=reflect.ValueOf(&p)
fmt.Println(ppv.Kind())
pv:=reflect.ValueOf(p)
fmt.Println(pv.Kind())
}
執行以上程式碼,可以看到如下列印輸出:
ptr
struct
Kind 方法返回一個 Kind 型別的值,它是一個常量,有以下可供使用的值:
type Kind uint
const (
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Ptr
Slice
String
Struct
UnsafePointer
)
從以上原始碼定義的 Kind 常量列表可以看到,已經包含了 Go 語言的所有底層型別。
reflect.Type
reflect.Value 可以用於與值有關的操作中,而如果是和變數型別本身有關的操作,則最好使用 reflect.Type,比如要獲取結構體對應的欄位名稱或方法。
要反射獲取一個變數的 reflect.Type,可以通過函式 reflect.TypeOf。
介面定義
和 reflect.Value 不同,reflect.Type 是一個介面,而不是一個結構體,所以也只能使用它的方法。
以下列出來的 reflect.Type 介面常用的方法。從這個列表來看,大部分都和 reflect.Value 的方法功能相同。
type Type interface {
Implements(u Type) bool
AssignableTo(u Type) bool
ConvertibleTo(u Type) bool
Comparable() bool
//以下這些方法和Value結構體的功能相同
Kind() Kind
Method(int) Method
MethodByName(string) (Method, bool)
NumMethod() int
Elem() Type
Field(i int) StructField
FieldByIndex(index []int) StructField
FieldByName(name string) (StructField, bool)
FieldByNameFunc(match func(string) bool) (StructField, bool)
NumField() int
}
其中幾個特有的方法如下:
- Implements 方法用於判斷是否實現了介面 u;
- AssignableTo 方法用於判斷是否可以賦值給型別 u,其實就是是否可以使用 =,即賦值運算子;
- ConvertibleTo 方法用於判斷是否可以轉換成型別 u,其實就是是否可以進行型別轉換;
- Comparable 方法用於判斷該型別是否是可比較的,其實就是是否可以使用關係運算子進行比較。
同樣會通過一些示例來講解 reflect.Type 的使用。
遍歷結構體的欄位和方法
採用上面示例中的 person 結構體進行演示,不過需要修改一下,為它增加一個方法 String,如下所示:
func (p person) String() string{
return fmt.Sprintf("Name is %s,Age is %d",p.Name,p.Age)
}
新增一個 String 方法,返回對應的字串資訊,這樣 person 這個 struct 結構體也實現了 fmt.Stringer 介面。
可以通過 NumField 方法獲取結構體欄位的數量,然後使用 for 迴圈,通過 Field 方法就可以遍歷結構體的欄位,並列印出欄位名稱。同理,遍歷結構體的方法也是同樣的思路,程式碼也類似,如下所示:
func main() {
p:=person{Name: "Golang",Age: 20}
pt:=reflect.TypeOf(p)
//遍歷person的欄位
for i:=0;i<pt.NumField();i++{
fmt.Println("欄位:",pt.Field(i).Name)
}
//遍歷person的方法
for i:=0;i<pt.NumMethod();i++{
fmt.Println("方法:",pt.Method(i).Name)
}
}
執行這個程式碼,可以看到如下結果:
欄位: Name
欄位: Age
方法: String
這正好和結構體 person 中定義的一致,說明遍歷成功。
小技巧:可以通過 FieldByName 方法獲取指定的欄位,也可以通過 MethodByName 方法獲取指定的方法,這在需要獲取某個特定的欄位或者方法時非常高效,而不是使用遍歷。
是否實現某介面
通過 reflect.Type 還可以判斷是否實現了某介面。以 person 結構體為例,判斷它是否實現了介面 fmt.Stringer 和 io.Writer,如下面的程式碼所示:
func main() {
p:=person{Name: "Golang",Age: 20}
pt:=reflect.TypeOf(p)
stringerType:=reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
writerType:=reflect.TypeOf((*io.Writer)(nil)).Elem()
fmt.Println("是否實現了fmt.Stringer:",pt.Implements(stringerType))
fmt.Println("是否實現了io.Writer:",pt.Implements(writerType))
}
小提示:儘可能通過型別斷言的方式判斷是否實現了某介面,而不是通過反射。
這個示例通過 Implements 方法來判斷是否實現了 fmt.Stringer 和 io.Writer 介面,執行它,可以看到如下結果:
是否實現了fmt.Stringer: true
是否實現了io.Writer: false
因為結構體 person 只實現了 fmt.Stringer 介面,沒有實現 io.Writer 介面,所以和驗證的結果一致。
字串和結構體互轉
在字串和結構體互轉的場景中,使用最多的就是 JSON 和 struct 互轉。
JSON 和 Struct 互轉
Go 語言的標準庫有一個 json 包,通過它可以把 JSON 字串轉為一個 struct 結構體,也可以把一個 struct 結構體轉為一個 json 字串。下面以 person 這個結構體為例,講解 JSON 和 struct 的相互轉換。如下面的程式碼所示:
func main() {
p:=person{Name: "張三",Age: 20}
//struct to json
jsonB,err:=json.Marshal(p)
if err==nil {
fmt.Println(string(jsonB))
}
//json to struct
respJSON:="{\"Name\":\"李四\",\"Age\":40}"
json.Unmarshal([]byte(respJSON),&p)
fmt.Println(p)
}
這個示例使用 Go 語言提供的 json 標準包做的演示。通過 json.Marshal 函式,你可以把一個 struct 轉為 JSON 字串。通過 json.Unmarshal 函式,你可以把一個 JSON 字串轉為 struct。
執行以上程式碼,會看到如下結果輸出:
{“Name”:”張三”,”Age”:20}
Name is 李四,Age is 40
仔細觀察以上列印出的 JSON 字串,你會發現 JSON 字串的 Key 和 struct 結構體的欄位名稱一樣,比如示例中的 Name 和 Age。那麼是否可以改變它們呢?比如改成小寫的 name 和 age,並且欄位的名稱還是大寫的 Name 和 Age。當然可以,要達到這個目的就需要用到 struct tag 的功能了。
Struct Tag
顧名思義,struct tag 是一個新增在 struct 欄位上的標記,使用它進行輔助,可以完成一些額外的操作,比如 json 和 struct 互轉。在上面的示例中,如果想把輸出的 json 字串的 Key 改為小寫的 name 和 age,可以通過為 struct 欄位新增 tag 的方式,示例程式碼如下:
type person struct {
Name string `json:"name"`
Age int `json:"age"`
}
為 struct 欄位新增 tag 的方法很簡單,只需要在欄位後面通過反引號把一個鍵值對包住即可,比如以上示例中的 json:”name”。其中冒號前的 json 是一個 Key,可以通過這個 Key 獲取冒號後對應的 name。
小提示:json 作為 Key,是 Go 語言自帶的 json 包解析 JSON 的一種約定,它會通過 json 這個 Key 找到對應的值,用於 JSON 的 Key 值。
通過 struct tag 指定了可以使用 name 和 age 作為 json 的 Key,程式碼就可以修改成如下所示:
respJSON:="{\"name\":\"李四\",\"age\":40}"
沒錯,JSON 字串也可以使用小寫的 name 和 age 了。現在再執行這段程式碼,看到如下結果:
{“name”:”張三”,”age”:20}
Name is 李四,Age is 40
輸出的 JSON 字串的 Key 是小寫的 name 和 age,並且小寫的 name 和 age JSON 字串也可以轉為 person 結構體。
相信已經發現,struct tag 是整個 JSON 和 struct 互轉的關鍵,這個 tag 就像是為 struct 欄位起的別名,那麼 json 包是如何獲得這個 tag 的呢?這就需要反射了。來看下面的程式碼:
//遍歷person欄位中key為json的tag
for i:=0;i<pt.NumField();i++{
sf:=pt.Field(i)
fmt.Printf("欄位%s上,json tag為%s\n",sf.Name,sf.Tag.Get("json"))
}
要想獲得欄位上的 tag,就要先反射獲得對應的欄位,可以通過 Field 方法做到。該方法返回一個 StructField 結構體,它有一個欄位是 Tag,存有欄位的所有 tag。示例中要獲得 Key 為 json 的 tag,所以只需要呼叫 sf.Tag.Get(“json”) 即可。
結構體的欄位可以有多個 tag,用於不同的場景,比如 json 轉換、bson 轉換、orm 解析等。如果有多個 tag,要使用空格分隔。採用不同的 Key 可以獲得不同的 tag,如下面的程式碼所示:
//遍歷person欄位中key為json、bson的tag
for i:=0;i<pt.NumField();i++{
sf:=pt.Field(i)
fmt.Printf("欄位%s上,json tag為%s\n",sf.Name,sf.Tag.Get("json"))
fmt.Printf("欄位%s上,bson tag為%s\n",sf.Name,sf.Tag.Get("bson"))
}
type person struct {
Name string `json:"name" bson:"b_name"`
Age int `json:"age" bson:"b_name"`
}
執行程式碼,可以看到如下結果:
欄位Name上,key為json的tag為name
欄位Name上,key為bson的tag為b_name
欄位Age上,key為json的tag為age
欄位Age上,key為bson的tag為b_name
可以看到,通過不同的 Key,使用 Get 方法就可以獲得自定義的不同的 tag。
實現 Struct 轉 JSON
相信理解了什麼是 struct tag,下面通過一個 struct 轉 json 的例子演示它的使用:
func main() {
p:=person{Name: "Golang",Age: 20}
pv:=reflect.ValueOf(p)
pt:=reflect.TypeOf(p)
//自己實現的struct to json
jsonBuilder:=strings.Builder{}
jsonBuilder.WriteString("{")
num:=pt.NumField()
for i:=0;i<num;i++{
jsonTag:=pt.Field(i).Tag.Get("json") //獲取json tag
jsonBuilder.WriteString("\""+jsonTag+"\"")
jsonBuilder.WriteString(":")
//獲取欄位的值
jsonBuilder.WriteString(fmt.Sprintf("\"%v\"",pv.Field(i)))
if i<num-1{
jsonBuilder.WriteString(",")
}
}
jsonBuilder.WriteString("}")
fmt.Println(jsonBuilder.String())//列印json字串
}
這是一個比較簡單的 struct 轉 json 示例,但是已經可以很好地演示 struct 的使用。在上述示例中,自定義的 jsonBuilder 負責 json 字串的拼接,通過 for 迴圈把每一個欄位拼接成 json 字串。執行以上程式碼,可以看到如下列印結果:
{“name”:”Golang”,”age”:”20”}
json 字串的轉換隻是 struct tag 的一個應用場景,完全可以把 struct tag 當成結構體中欄位的後設資料配置,使用它來做想做的任何事情,比如 orm 對映、xml 轉換、生成 swagger 文件等。
反射定律
反射是計算機語言中程式檢視其自身結構的一種方法,它屬於超程式設計的一種形式。反射靈活、強大,但也存在不安全。它可以繞過編譯器的很多靜態檢查,如果過多使用便會造成混亂。為了幫助開發者更好地理解反射,Go 語言的作者在部落格上總結了反射的三大定律。
- 任何介面值 interface{} 都可以反射出反射物件,也就是 reflect.Value 和 reflect.Type,通過函式 reflect.ValueOf 和 reflect.TypeOf 獲得。
- 反射物件也可以還原為 interface{} 變數,也就是第 1 條定律的可逆性,通過 reflect.Value 結構體的 Interface 方法獲得。
- 要修改反射的物件,該值必須可設定,也就是可定址,參考上節課修改變數的值那一節的內容理解。
小提示:任何型別的變數都可以轉換為空介面 intferface{},所以第 1 條定律中函式 reflect.ValueOf 和 reflect.TypeOf 的引數就是 interface{},表示可以把任何型別的變數轉換為反射物件。在第 2 條定律中,reflect.Value 結構體的 Interface 方法返回的值也是 interface{},表示可以把反射物件還原為對應的型別變數。
一旦理解了這三大定律,就可以更好地理解和使用 Go 語言反射。
總結
在反射中,reflect.Value 對應的是變數的值,如果需要進行和變數的值有關的操作,應該優先使用 reflect.Value,比如獲取變數的值、修改變數的值等。reflect.Type 對應的是變數的型別,如果需要進行和變數的型別本身有關的操作,應該優先使用 reflect.Type,比如獲取結構體內的欄位、型別擁有的方法集等。
此外要再次強調:反射雖然很強大,可以簡化程式設計、減少重複程式碼,但是過度使用會讓程式碼變得複雜混亂。所以除非非常必要,否則儘可能少地使用它們。
本作品採用《CC 協議》,轉載必須註明作者和本文連結