定義類型、聲明變量、使用變量是一門編程語言的基本功能,咱們能夠這樣來定義一個結構體類型:golang
type Foo struct {
X string `foo:"x"`
Y int `foo:"y"`
}
複製代碼
像這樣來使用這個類型聲明一個變量:編程
var bar Foo
複製代碼
使用變量也很方便,像這樣就能夠在終端打印出結構體的字段了:json
fmt.Printf("%s, %s", bar.X, bar.Y)
複製代碼
以上這些操做都是在明確了變量的類型的時候進行的,不過在編程過程當中,咱們可能會遇到一種狀況:在編寫代碼時沒法明確變量的類型,變量的信息只有在程序運行時在會獲取,好比這樣的函數:bash
func theFunc(val interface{}) 複製代碼
它的函數簽名只有一個參數類型爲 interface 的參數 val,這意味着能夠將任意類型的變量做爲實參傳入函數。這樣的狀況下該如何操做變量 val 呢?golang 標準庫中有一個 reflect包--即反射--它提供了一系列的方法能幫助咱們在運行時獲取變量的信息,或者修改變量的值。在反射中有三個比較重要的概念:Type、Kind 和 Value。下面就一塊兒來看看反射的奇妙之處吧。編程語言
若是咱們把 bar 變量傳入了 theFunc 函數,在函數中咱們須要知道些什麼信息呢?可能會須要知道傳入的變量是什麼類型的,若是是一個結構體,可能還會須要知道結構體中有那些字段,這些字段又是什麼類型的。咱們可能還須要根據結構體字段特定的 tag 來執行特定的操做,好比 encoding/json 包就會根據 tag 來給序列化的 json 字段取名。若是傳入的變量是咱們所指望的,咱們可能還須要修改它的值,或者用它來建立一個新的變量。咱們就一個個來談談如何用反射來實現這些功能把。函數
reflect 提供了 TypeOf 函數來獲取指定變量的類型,它的返回值類型爲 reflect.Type,這是一個接口類型,它提供了一系列的方法來獲取變量相關信息的方法。ui
Name() 方法獲取變量的類型名稱,不過它有一個限制,即只能獲取基本類型或者自定義的結構體的類型名稱,其餘的類型會返回一個空的字符串。spa
Kind() 方法會返回變量的內置類型名稱,好比 ptr、slice、array、map、func、struct 等等。它一般能夠和 switch 配合來作類型判斷。指針
Elem() 方法用於判斷類型的元素類型(type's element type)。它是對 Name 方法的補充,它能夠返回 array, chan, map, ptr, 或 slice 類型中元素的類型。好比,針對一個指針 &bar, Elem 方法會返回 Foo 這個類型名稱;針對 []string 這樣一個字符串 slice,Elem 會返回 string 這個類型名稱。若是不在容許的類型上調用 Elem 方法,會致使 panic ,其實 reflect 包中不少方法和函數都是這樣的,它要求使用者知道本身在作什麼。code
下面來舉一個完整的示例吧:
import (
"fmt"
"reflect"
)
type Foo struct {
X string `foo:"x"`
Y string `foo:"y"`
}
func main() {
bar := Foo{
X: "hello",
Y: "world",
}
sli := make([]string, 0)
ch := make(chan bool)
m := make(map[int]int)
arr := [10]int{}
i := 0
f := 1.1
b := true
theFunc(bar)
theFunc(&bar)
theFunc(sli)
theFunc(ch)
theFunc(m)
theFunc(arr)
theFunc(theFunc)
theFunc(i)
theFunc(f)
theFunc(b)
}
func theFunc(val interface{}) {
valType := reflect.TypeOf(val)
fmt.Printf("name of value : %s, kind of value : %s, ", valType.Name(), valType.Kind())
switch valType.Kind() {
case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Slice, reflect.Map:
fmt.Printf("elem of value : %s", valType.Elem())
}
fmt.Println()
}
複製代碼
輸出爲:
ValueOf() 函數獲取變量中實際存儲的值。例如:
bar := Foo {
X: "hello",
Y: "world",
}
fmt.Println(reflect.ValueOf(bar))
複製代碼
輸出的結果爲:
{"hello", "world"}
複製代碼
若是使用者知道傳入的值是什麼類型的,或者經過類型判斷的方式獲取了值的類型信息,那麼可使用 val.(type) 這種類型斷言的方式將傳入的 interface 類型的數據強制轉換成咱們所須要的類型,這時候就能夠正常使用類型的字段了。須要注意的是,若是類型斷言出錯了,那麼將會引起 panic,因此,在使用的時候能夠先確認變量的類型再進行類型斷言,或者利用類型斷言的第二個布爾類型的返回值來判斷斷言是否成功。
func main() {
bar := Foo {
X: "hello",
Y: "world",
}
theFunc(bar)
}
func theFunc(val interface{}) {
// v1 := val.(int) // 引起 panic
//在使用斷言前先判斷類型是否正確
if reflect.TypeOf(val) == reflect.TypeOf(Foo{}) {
v2 := val.(Foo)
fmt.Println(v2)
}
//使用 ok 來判斷斷言是否成功
if v2, ok := val.(Foo); ok {
fmt.Println(v2)
}
}
複製代碼
輸出爲
{"hello" "world"}
{"hello" "world"}
複製代碼
NumMethod()、Method(int)、 MethodByName(string) 這幾個方法能夠獲取變量所對應的類型已導出的方法:
type Foo struct {
X string `foo:"x"`
Y string `foo:"y"`
}
func (Foo) unExported() {
fmt.Println("unExported")
}
func (Foo) Exported() {
fmt.Println("Exported")
}
func main() {
bar := Foo{
X: "hello",
Y: "world",
}
valTheFun(bar)
}
func valTheFun(val interface{}) {
valType := reflect.TypeOf(val)
fmt.Println(valType.NumMethod())
for i := 0; i < valType.NumMethod(); i++ {
fmt.Println(valType.Method(i).Name)
}
}
複製代碼
輸出結果:
1
Exported
複製代碼
對於函數類型,Type 接口也提供了相應的方法來遍歷入參和出參:NumIn(),In(i int), NumOut,Out(i int)。這幾個方法只能用與類型爲函數的變量,不然將會引起 panic 。
func main() {
valTheFun(valTheFun)
valTheFun(param)
}
func param(i int, s string) (int, string) {
return i, s
}
func valTheFun(val interface{}) {
valType := reflect.TypeOf(val)
fmt.Printf("number of in args %d:\n", valType.NumIn())
for i := 0; i < valType.NumIn(); i++ {
fmt.Printf("\t %s\n", valType.In(i))
}
fmt.Printf("number of out args %d:\n", valType.NumOut())
for i := 0; i < valType.NumOut(); i++ {
fmt.Printf("\t %s\n", valType.Out(i))
}
}
複製代碼
輸出:
number of in args 1:
interface {}
number of out args 0:
number of in args 2:
int
string
number of out args 2:
int
string
複製代碼
遍歷結構體的字段是十分經常使用的,當咱們須要統一處理 kind 爲結構體的入參,但又不知道結構體的具體類型,也不須要知道結構體的具體類型的時候,就能夠用上遍歷結構體字段的一系列方法了。結構體中還有一個 tag 屬性,用它能夠給結構體中的字段添加額外的屬性,golang 也提供了方法用於遍歷 tag 的數據。我的以爲這一部分的功能仍是須要好好研究一下的,熟悉這部分的操做能夠寫出更好的代碼,golang 標準庫中比較經常使用的場景有: encoding/json 將結構體序列化爲 json 字符串;encoding/xml 將結構體序列化爲 xml 數據等等。下面給一個簡單的例子:
func main() {
bar := Foo{
X: "hello",
Y: "world",
}
structFunc(bar)
}
func structFunc(val interface{}) {
valType := reflect.TypeOf(val)
fmt.Println("number of fields in val :", valType.NumField())
for i := 0; i < valType.NumField(); i++ {
fmt.Printf("field : %s ", valType.Field(i).Name)
fmt.Println("tags", valType.Field(i).Tag)
}
}
複製代碼
輸出:
number of fields in val : 2
field : X tags foo:"x"
field : Y tags foo:"y"
複製代碼
reflect 中有兩種函數能夠建立新的變量,分別是 New(Type) 和 Make* 。用兩種是由於 Make* 是一系列的函數,它們和內建函數 make 同樣,只能爲 slice, map, chan 來建立新的變量,不一樣的是 reflect 爲這幾個類型都分別聲明瞭一個 Make 函數,而且還爲 func 類型也聲明瞭一個 Make 函數。而 New 和內建的 new 函數同樣,返回的是建立的變量的指針,這個很重要。由於給新建的變量設置值的時候,須要使用 Field() 方法來指定結構體中的字段,而這個方法的接收器必須爲 struct,因此,新建的變量必需要先調用 Elem() 方法來獲取對應的結構體類型,而後再調用 Field() 方法來設置新的值。
func main() {
bar := Foo{
X: "hello",
Y: "world",
}
valType := reflect.TypeOf(bar)
valFields := reflect.ValueOf(bar)
val := reflect.New(valType)
//由於 val 是一個指針,因此須要使用 Elem 來獲取元素的實際類型
val.Elem().Field(0).SetString(valFields.Field(0).String())
val.Elem().Field(1).SetString("golang")
//val 是一個 reflect.Value 類型的變量,
//須要經過 Interface() 來獲取它所維護的數據,
//而後再經過類型斷言強制轉換爲指定的類型
if v, ok := val.Interface().(*Foo); !ok {
panic("wrong type")
} else {
fmt.Println(*v)
}
}
複製代碼
輸出:
{hello golang}
複製代碼
到這裏,基本是把 reflect 的基本操做都講了一遍了,原本是想簡單寫寫的,結果越寫越多。可能有些方面沒有將的十分清楚,請各位看官多多斧正。