Go語言的反射


反射是語言里面是非常重要的一個特性,我們經常會看見這個詞,但是對於反射沒有一個很好的理解,主要是因為對於反射的使用場景不太熟悉。

 

一、理解變量的內在機制

1.類型信息,元信息,是預先定義好的,靜態的。

2.值信息,程序進行過程中,動態變化的。

 

二、反射和空接口

1.空接口相當於一個容器,能接受任何東西。

2.那怎么判斷空接口變量存儲的是什么類型呢?之前有使用過類型斷言,這只是一個比較基礎的方法

3.如果想獲取存儲變量的類型信息和值信息就要使用反射機制,所以反射是什么? 反射就是動態的獲取變量類型信息和值信息的機制。

 

三、怎么利用反射分析空接口里面的信息呢?

①首先利用的是GO語言里面的Reflect包

②利用包里的TypeOf方法可以獲取變量的類型信息

func reflect_typeof(a interface{}) {
    t := reflect.TypeOf(a)
    fmt.Printf("type of a is:%v\n", t)

    k := t.Kind()
    switch k {
    case reflect.Int64:
        fmt.Printf("a is int64\n")
    case reflect.String:
        fmt.Printf("a is string\n")
    }
}

利用Kind() 可以獲取t的類型,如代碼所示,這里可以判斷a是Int64還是string, 像下面一樣使用:

func main() {
    var x int64 = 3
    reflect_example(x)

    var y string = "hello"
    reflect_example(y)
}

打印結果:

type of a is:int64
a is int64
type of a is:string
a is string

③利用包里的ValueOf方法可以獲取變量的值信息

func reflect_value(a interface{}) {
    v := reflect.ValueOf(a)
    k := v.Kind()
    switch k {
    case reflect.Int64:
        fmt.Printf("a is Int64, store value is:%d\n", v.Int())
    case reflect.String:
        fmt.Printf("a is String, store value is:%s\n", v.String())
    }
}

利用ValueOf方法可以得到變量的值信息,ValueOf返回的是一個Value結構體類型,有趣的是 可以使用 v.Type() 獲取該變量的類型,和上面reflect.TypeOf() 獲取的結果一樣。

此外,因為值信息是動態的,所以我們不僅僅可以獲取這個變量的類型,還能取得這個變量里面存儲的值,利用 v.Int() 、 v.String() 等等就能取得值。如上面的main,調用此函數返回的結果:

a is Int64, store value is:3
a is String, store value is:hello

這里存在一個問題,如果傳進去一個類型,使用了錯誤的解析,那么將會在運行的時候報錯, 例如將  一個string類型強行的v.Int()。

 

既然值類型是動態的,能取到保存的值,同樣可以設置值。在反射里面有很多set的方法,例如SetFloat、SetInt()、SetString()等可以幫助我們設置值。

① 下面的例子,我想把 x設置為 6.28,但是會報錯!

func main() {
    var x float64 = 3.14
    v := reflect.ValueOf(x)
    v.SetFloat(6.28)
    fmt.Printf("After Set Value is %f", x)
}

錯誤結果:

panic: reflect: reflect.Value.SetFloat using unaddressable value
......

結果上說明是不可設置的,為什么呢? 因為我們的x是一個值類型,而值類型的傳遞是拷貝了一個副本,當 v := reflect.ValueOf(x) 函數通過傳遞一個 x 拷貝創建了 v,那么 v 的改變並不能更改原始的 x。要想 v 的更改能作用到 x,那就必須傳遞 x 的地址 v = reflect.ValueOf(&x)。修改程序如下:

func main() {
    var x float64 = 3.14
    v := reflect.ValueOf(&x)
    v.SetFloat(6.28)
    fmt.Printf("After Set Value is %f", x)
}

結果:依然報錯!為什么傳了地址還報錯?因為&x是地址了,所以它的類型就變了,可以通過v.Type(),看下改變成了什么:

func main() {
    var x float64 = 3.14
    v := reflect.ValueOf(&x)
    fmt.Printf("type of v is %v", v.Type())   //打印的結果是:type of v is *float64 
}

由程序可以知道,這個返回的是一個指針類型的。所以上面SetFloat才會失敗,那怎么做?

我們正常的賦值,如果是地址的話,例如下面:一般我們都會對*y進行賦值, *的意思就是往這個地址里面賦值。

var y *float64 = new(float64)
*y = 10.12
fmt.Printf("y = %v", *y)

同樣的,我們在反射里面也可以取地址,需要通過 Elem() 方法進行取地址。再次修改程序

func main() {
    var x float64 = 3.14
    v := reflect.ValueOf(&x)
    fmt.Printf("type of v is %v\n", v.Type())
    v.Elem().SetFloat(6.28)
    fmt.Printf("After set x is %v", x)
}

結果為:

type of v is *float64
After set x is 6.28

 

四、利用反射獲取結構體里面的方法和調用。

1.獲取結構體的字段

我們可以通過上面的方法判斷一個變量是不是結構體。

可以通過 NumField() 獲取所有結構體字段的數目、進而遍歷,通過Field()方法獲取字段的信息。

type Student struct {
    Name  string
    Sex   int
    Age   int
    Score float32
}

func main() {
    //創建一個結構體變量
    var s Student = Student{
        Name:  "BigOrange",
        Sex:   1,
        Age:   10,
        Score: 80.1,
    }

    v := reflect.ValueOf(s)
    t := v.Type()
    kind := t.Kind()
    
    //分析s變量的類型,如果是結構體類型,那么遍歷所有的字段
    switch kind {
    case reflect.Int64:
        fmt.Printf("s is int64\n")
    case reflect.Float32:
        fmt.Printf("s is int64\n")
    case reflect.Struct:
        fmt.Printf("s is struct\n")
        fmt.Printf("field num of s is %d\n", v.NumField())
        //NumFiled()獲取字段數,v.Field(i)可以取得下標位置的字段信息,返回的是一個Value類型的值
        for i := 0; i < v.NumField(); i++ {
            field := v.Field(i)
            //打印字段的名稱、類型以及值
            fmt.Printf("name:%s type:%v value:%v\n",
                t.Field(i).Name, field.Type().Kind(), field.Interface())
        }
    default:
        fmt.Printf("default\n")
    }
}

執行結果:

s is struct
field num of s is 4
name:Name type:string value:BigOrange
name:Sex type:int value:1
name:Age type:int value:10
name:Score type:float32 value:80.1

這里需要說明幾個問題:

①打印字段名稱的時候,使用的是t.Field(i).Name ,Name是靜態的,所以屬於類型的信息

②打印值的時候,這里將field.Interface()實際上相當於ValueOf的反操作(可以參考這篇文章https://www.cnblogs.com/baicaiyoudu/archive/2016/09/25/5905766.html),所以才能把值打印出來

③此外如果Student中的Name字段變為name(私有),那么則會報錯,不能反射出私有變量 錯誤信息 “panic: reflect.Value.Interface: cannot return value obtained from unexported field or method”

 

2.對結構體內的字段進行賦值操作

參考下面的代碼,對上面的Student進行賦值操作:

func main() {
    s := Student{
        Name:  "BigOrange",
        Sex:   1,
        Age:   10,
        Score: 80.1,
    }

    fmt.Printf("Name:%v, Sex:%v,Age:%v,Score:%v \n", s.Name, s.Sex, s.Age, s.Score)
    v := reflect.ValueOf(&s)  //這里傳的是地址!!!

    v.Elem().Field(0).SetString("ChangeName")
    v.Elem().FieldByName("Score").SetFloat(99.9)

    fmt.Printf("Name:%v, Sex:%v,Age:%v,Score:%v \n", s.Name, s.Sex, s.Age, s.Score)
}

結果:

Name:BigOrange, Sex:1,Age:10,Score:80.1
Name:ChangeName, Sex:1,Age:10,Score:99.9

3.獲取結構體里面的方法

可以通過NumMethod()獲得接頭體里面的方法數量,然后遍歷通過Method()獲取方法的具體信息。如下代碼所示:

//新增-設置名稱方法
func (s *Student) SetName(name string) {
     fmt.Printf("有參數方法 通過反射進行調用:%v\n", s)
     s.Name = name
}
//新增-打印信息方法
func (s *Student) PrintStudent() {
    fmt.Printf("無參數方法 通過反射進行調用:%v\n", s)
}

func main() {
    s := Student{
        Name:  "BigOrange",
        Sex:   1,
        Age:   10,
        Score: 80.1,
    }

    v := reflect.ValueOf(&s)
    //取得Type信息
    t := v.Type()
    
    fmt.Printf("struct student have %d methods\n", t.NumMethod())

    for i := 0; i < t.NumMethod(); i++ {
        method := t.Method(i)
        fmt.Printf("struct %d method, name:%s type:%v\n", i, method.Name, method.Type)
    }
}

輸出:

struct student have 2 methods
struct 0 method, name:PrintStudent type:func(*main.Student)
struct 1 method, name:SetName type:func(*main.Student, string)

從結果中看到我們可以獲取方法的名稱以及簽名信息,並且這個方法的輸出順序是按照字母排列的。

並且輸出結果可以看到一個有趣的現象:結構體的方法其實也是通過函數實現的例如 func(s Student) SetName(name string) 這個方法,反射之后的結果就是 func(main.Student , string) 實際上把Student當參數了。

 此外還可以通過反射來調用這些方法。想要通過反射調用結構體里面的方法,首先要知道方法調用時一個動態的,所以要先通過ValueOf獲取值,然后通過獲取的值進行方法的調用 ,通過 value里面的Method方法 返回一個方法,然后通過Call方法調用,Call是參數是一個切片,也就是參數的列表。以下是Call方法的定義:可以看到參數是一個Value的數組:

如下代碼展示了如何調用有參數的方法和無參數的方法:

func main() {
    s := Student{
        Name:  "BigOrange",
        Sex:   1,
        Age:   10,
        Score: 80.1,
    }

    v := reflect.ValueOf(&s)

    //通過reflect.Value獲取對應的方法並調用
    m1 := v.MethodByName("PrintStudent")
    var args []reflect.Value
    m1.Call(args)

    m2 := v.MethodByName("SetName")
    var args2 []reflect.Value
    name := "stu01"
    nameVal := reflect.ValueOf(name)
    args2 = append(args2, nameVal)
    m2.Call(args2)
    m1.Call(args)
}

執行結果:

無參數方法 通過反射進行調用:&main.Student{Name:"BigOrange", Sex:1, Age:10, Score:80.1}
有參數方法 通過反射進行調用:&main.Student{Name:"BigOrange", Sex:1, Age:10, Score:80.1}
無參數方法 通過反射進行調用:&main.Student{Name:"stu01", Sex:1, Age:10, Score:80.1}

上面格式打印:

%v      相應值的默認格式。            Printf("%v", people)   {zhangsan},  

%+v     打印結構體時,會添加字段名     Printf("%+v", people)  {Name:zhangsan}

%#v     相應值的Go語法表示            Printf("#v", people)   main.Human{Name:"zhangsan"}

 

五、怎么獲取結構體里tag的信息。

有時候我們在類型上面定義一些tag,例如使用json和數據庫的時候。Field()方法返回的StructField結構體中保存着Tag信息,並且Tag信息可以通過一個Get(Key)的方法獲取出來,如下代碼所示:

 

type Student struct {
    Name string `json:"jsName" db:"dbName"`
}

func main() {
    s := Student{
        Name: "BigOrange",
    }
    v := reflect.ValueOf(&s)
    t := v.Type()
    field0 := t.Elem().Field(0)
    fmt.Printf("tag json=%s\n", field0.Tag.Get("json"))
    fmt.Printf("tag db=%s\n", field0.Tag.Get("db"))
}

結果:

tag json=jsName
tag db=dbName

 

六、應用場景

1.序列化和反序列化,比如json, protobuf等各種數據協議

2.各種數據庫的ORM,比如gorm,sqlx等數據庫中間件

3.配置文件解析相關的庫,比如yaml、ini等


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM