Golang 学习笔记十四 反射 - Go语言中文社区

Golang 学习笔记十四 反射


参考
《快学 Go 语言》第 15 课 —— 反射
golang reflect反射(一):interface接口的入门(大白话)
golang reflect反射(二):interface接口的理解

反射是 Go 语言学习的一个难点,但也是非常重要的一个知识点。反射是洞悉 Go 语言类型系统设计的法宝,Go 语言的 ORM 库离不开它,Go 语言的 json 序列化库离不开它,Go 语言的运行时更是离不开它。笔者在学习反射功能的时候也是费了好大一番功夫才敢说自己确实搞懂了。下面请读者跟着我的步伐来一步一步深入理解反射功能。

一、反射的目标

反射的目标之一是获取变量的类型信息,例如这个类型的名称、占用字节数、所有的方法列表、所有的内部字段结构、它的底层存储类型等等。

反射的目标之二是动态的修改变量内部字段值。比如 json 的反序列化,你有的是对象内部字段的名称和相应的值,你需要把这些字段的值循环填充到对象相应的字段里。

二、元类型reflect.Kind

reflect 包定义了十几种内置的「元类型」,每一种元类型都有一个整数编号,这个编号使用 reflect.Kind 类型表示。不同的结构体是不同的类型,但是它们都是同一个元类型 Struct。包含不同子元素的切片也是不同的类型,但是它们都会同一个元类型 Slice。

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 // unsafe.Pointer 类型
)
三、反射的基础代码TypeOf() 和 ValueOf()

reflect 包提供了两个基础反射方法,分别是 TypeOf() 和 ValueOf() 方法,分别用于获取变量的类型和值,定义如下

func TypeOf(v interface{}) Type
func ValueOf(v interface{}) Value

下面是一个简单的例子,对结构体变量进行反射

package main

import "fmt"
import "reflect"

func main() {
    var s int = 42
    fmt.Println(reflect.TypeOf(s))
    fmt.Println(reflect.ValueOf(s))
}

--------
int
42

这两个方法的参数是 interface{} 类型,意味着调用时编译器首先会将目标变量转换成 interface{} 类型。在接口小节我们提到接口类型包含两个指针,一个指向类型,一个指向值,上面两个方法的作用就是将接口变量进行解剖分离出类型和值。

TypeOf() 方法返回变量的类型信息得到的是一个类型为 reflect.Type 的变量,ValueOf() 方法返回变量的值信息得到的是一个类型为 reflect.Value 的变量。

四、reflect.Type

它是一个接口类型,里面定义了非常多的方法用于获取和这个类型相关的一切信息。这个接口的结构体实现隐藏在 reflect 包里,每一种类型都有一个相关的类型结构体来表达它的结构信息。

type Type interface {
  ...
  Method(i int) Method  // 获取挂在类型上的第 i'th 个方法
  ...
  NumMethod() int  // 该类型上总共挂了几个方法
  Name() string // 类型的名称
  PkgPath() string // 所在包的名称
  Size() uintptr // 占用字节数
  String() string // 该类型的字符串形式
  Kind() Kind // 元类型
  ...
  Bits() // 占用多少位
  ChanDir() // 通道的方向
  ...
  Elem() Type // 数组,切片,通道,指针,字典(key)的内部子元素类型
  Field(i int) StructField // 获取结构体的第 i'th 个字段
  ...
  In(i int) Type  // 获取函数第 i'th 个参数类型
  Key() Type // 字典的 key 类型
  Len() int // 数组的长度
  NumIn() int // 函数的参数个数
  NumOut() int // 函数的返回值个数
  Out(i int) Type // 获取函数 第 i'th 个返回值类型
  common() *rtype // 获取类型结构体的共同部分
  uncommon() *uncommonType // 获取类型结构体的不同部分
}

所有的类型结构体都包含一个共同的部分信息,这部分信息使用 rtype 结构体描述,rtype 实现了 Type 接口的所有方法。剩下的不同的部分信息各种特殊类型结构体都不一样。可以将 rtype 理解成父类,特殊类型的结构体是子类,会有一些不一样的字段信息。

// 基础类型 rtype 实现了 Type 接口
type rtype struct {
  size uintptr // 占用字节数
  ptrdata uintptr
  hash uint32 // 类型的hash值
  ...
  kind uint8 // 元类型
  ...
}

// 切片类型
type sliceType struct {
  rtype
  elem *rtype // 元素类型
}

// 结构体类型
type structType struct {
  rtype
  pkgPath name  // 所在包名
  fields []structField  // 字段列表
}
...
五、reflect.Value

不同于 reflect.Type 接口,reflect.Value 是结构体类型,一个非常简单的结构体。

type Value struct {
  typ *rtype  // 变量的类型结构体
  ptr unsafe.Pointer // 数据指针
  flag uintptr // 标志位
}

这个接口体包含变量的类型结构体指针、数据的地址指针和一些标志位信息。里面的类型结构体指针字段就是上面的 rtype 结构体地址,存储了变量的类型信息。标志位里有几个位存储了值的「元类型」。下面我们看个简单的例子

package main

import "reflect"
import "fmt"

func main() {
    type SomeInt int
    var s SomeInt = 42
    var t = reflect.TypeOf(s)
    var v = reflect.ValueOf(s)
    // reflect.ValueOf(s).Type() 等价于 reflect.TypeOf(s)
    fmt.Println(t == v.Type())
    fmt.Println(v.Kind() == reflect.Int) // 元类型
    // 将 Value 还原成原来的变量
    var is = v.Interface()
    fmt.Println(is.(SomeInt))
}

----------
true
true
42

Value 结构体的 Type() 方法也可以返回变量的类型信息,它可以作为 reflect.TypeOf() 函数的替代品,没有区别。通过 Value 结构体提供的 Interface() 方法可以将 Value 还原成原来的变量值。注意这里使用了类型断言,如果想做+1这种数字操作,直接用is这个变量是无法通过的。

将上面的各种关系整理一下,可以得到下面这张图


image.png

Value 这个结构体虽然很简单,但是附着在 Value 上的方法非常之多,主要是用来方便用户读写 ptr 字段指向的数据内存。虽然我们也可以通过 unsafe 包来精细操控内存,但是使用过于繁琐,使用 Value 结构体提供的方法会更加简单直接。

func (v Value) SetLen(n int)  // 修改切片的 len 属性
 func (v Value) SetCap(n int) // 修改切片的 cap 属性
 func (v Value) SetMapIndex(key, val Value) // 修改字典 kv
 func (v Value) Send(x Value) // 向通道发送一个值
 func (v Value) Recv() (x Value, ok bool) // 从通道接受一个值
 // Send 和 Recv 的非阻塞版本
 func (v Value) TryRecv() (x Value, ok bool)
 func (v Value) TrySend(x Value) bool

 // 获取切片、字符串、数组的具体位置的值进行读写
 func (v Value) Index(i int) Value
 // 根据名称获取结构体的内部字段值进行读写
 func (v Value) FieldByName(name string) Value
 // 将接口变量装成数组,一个是类型指针,一个是数据指针
 func (v Value) InterfaceData() [2]uintptr
 // 根据名称获取结构体的方法进行调用
 // Value 结构体的数据指针 ptr 可以指向方法体
 func (v Value) MethodByName(name string) Value
 ...

值得注意的是,观察 Value 结构体提供的很多方法,其中有不少会返回 Value 类型。比如反射数组类型的 Index(i int) 方法,它会返回一个新的 Value 对象,这个对象的类型指向数组内部子元素的类型,对象的数据指针会指向数组指定位置子元素所在的内存。

六、理解 Go 语言官方的反射三大定律

官方对 Go 语言的反射功能做了一个抽象的描述,总结出了三大定律,分别是

1.Reflection goes from interface value to reflection object.
2.Reflection goes from reflection object to interface value.
3.To modify a reflection object, the value must be settable.

1.第一个定律的意思是反射将接口变量转换成反射对象 Type 和 Value,这个很好理解,就是下面这两个方法的功能

func TypeOf(v interface{}) Type
func ValueOf(v interface{}) Value

2.第二个定律的意思是反射可以通过反射对象 Value 还原成原先的接口变量,这个指的就是 Value 结构体提供的 Interface() 方法。注意它得到的是一个接口变量,如果要换成成原先的变量还需要经过一次造型。

func (v Value) Interface() interface{}

3.前两个定律比较简单,它的意思可以使用前面画的反射关系图来表达。第三个定律的功能不是很好理解,它的意思是想用反射功能来修改一个变量的值,前提是这个值可以被修改。

值类型的变量是不可以通过反射来修改,因为在反射之前,传参的时候需要将值变量转换成接口变量,值内容会被浅拷贝,反射对象 Value 指向的数据内存地址不是原变量的内存地址,而是拷贝后的内存地址。这意味着如果值类型变量可以通过反射功能来修改,那么修改操作根本不会影响到原变量的值,那就白白修改了。所以 reflect 包就直接禁止了通过反射来修改值类型的变量。我们看个例子

package main

import "reflect"

func main() {
    var s int = 42
    var v = reflect.ValueOf(s)
    v.SetInt(43)
}

---------
panic: reflect: reflect.Value.SetInt using unaddressable value

goroutine 1 [running]:
reflect.flag.mustBeAssignable(0x82)
    /usr/local/go/src/reflect/value.go:234 +0x157
reflect.Value.SetInt(0x107a1a0, 0xc000016098, 0x82, 0x2b)
    /usr/local/go/src/reflect/value.go:1472 +0x2f
main.main()
    /Users/qianwp/go/src/github.com/pyloque/practice/main.go:8 +0xc0
exit status 2

尝试通过反射来修改整型变量失败了,程序直接抛出了异常。下面我们来尝试通过反射来修改指针变量指向的值,这个是可行的。

package main

import "fmt"
import "reflect"

func main() {
    var s int = 42
    // 反射指针类型
    var v = reflect.ValueOf(&s)
    // 要拿出指针指向的元素进行修改
    v.Elem().SetInt(43)
    fmt.Println(s)
}

-------
43

可以看到变量 s 的值确实被修改成功了,不过这个例子修改的是指针指向的值而不是修改指针变量本身,如果不使用 Elem() 方法进行修改也会抛出一样的异常。

结构体也是值类型,也必须通过指针类型来修改。下面我们尝试使用反射来动态修改结构体内部字段的值。

package main

import "fmt"
import "reflect"

type Rect struct {
    Width int
    Height int
}

func SetRectAttr(r *Rect, name string, value int) {
    var v = reflect.ValueOf(r)
    var field = v.Elem().FieldByName(name)
    field.SetInt(int64(value))
}

func main() {
    var r = Rect{50, 100}
    SetRectAttr(&r, "Width", 100)
    SetRectAttr(&r, "Height", 200)
    fmt.Println(r)
}

-----
{100 200}
七、内置库反射的例子

节选自《Go语言圣经P427》
一个大家熟悉的例子是fmt.Fprintf函数提供的字符串格式化处理逻辑,它可以用例对任意类型的值格式化并打印,甚至支持用户自定义的类型。让我们也来尝试实现一个类似功能的函数。为了简单起见,我们的函数只接收一个参数,然后返回和fmt.Sprint类似的格式化后的字符串。我们实现的函数名也叫Sprint。

我们使用了switch类型分支首先来测试输入参数是否实现了String方法,如果是的话就使用该方法。然后继续增加类型测试分支,检查是否是每个基于string、int、bool等基础类型的动态类型,并在每种情况下执行相应的格式化操作。

func Sprint(x interface{}) string {
    type stringer interface {
        String() string
    }
    
    switch x := x.(type) {
        case stringer:
            return x.String()
        case string:
            return x
        case int:
            return strconv.Itoa(x)
            // ...similar cases for int16, uint32, and so on...
        case bool:
            if x {
                return "true"
            }
            return "false"
        default:
            // array, chan, func, map, pointer, slice, struct
            return "???"
    }
}

但是我们如何处理其它类似[]float64、map[string][]string等类型呢?我们当然可以添加更多的测试分支,但是这些组合类型的数目基本是无穷的。还有如何处理url.Values等命名的类型呢?虽然类型分支可以识别出底层的基础类型是map[string][]string,但是它并不匹配url.Values类型,因为它们是两种不同的类型,而且switch类型分支也不可能包含每个类似url.Values的类型,这会导致对这些库的循环依赖。没有一种方法来检查未知类型的表示方式,我们被卡住了。这就是我们为何需要反射的原因。

现在使用 reflect.Value 的 Kind 方法来替代之前的类型 switch. 虽然还是有无穷多的类型, 但是它们的kinds类型却是有限的。

package format

import (
"reflect"
"strconv"
)

// Any formats any value as a string.
func Any(value interface{}) string {
    return formatAtom(reflect.ValueOf(value))
}

// formatAtom formats a value without inspecting its internal structure.
func formatAtom(v reflect.Value) string {
    switch v.Kind() {
    case reflect.Invalid:
        return "invalid"
        
    case reflect.Int, reflect.Int8, reflect.Int16,
        reflect.Int32, reflect.Int64:
        return strconv.FormatInt(v.Int(), 10)
        
    case reflect.Uint, reflect.Uint8, reflect.Uint16,
        reflect.Uint32, reflect.Uint64, reflect.Uintptr:
        return strconv.FormatUint(v.Uint(), 10)
        
    // ...floating-point and complex cases omitted for brevity...
    case reflect.Bool:
        return strconv.FormatBool(v.Bool())
        
    case reflect.String:
        return strconv.Quote(v.String())
        
    case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map:
        return v.Type().String() + " 0x" +
        strconv.FormatUint(uint64(v.Pointer()), 16)
        
    default: // reflect.Array, reflect.Struct, reflect.Interface
        return v.Type().String() + " value"
}

}

到目前为止, 我们的函数将每个值视作一个不可分割没有内部结构的, 因此它叫 formatAtom.对于聚合类型(结构体和数组)个接口只是打印类型的值, 对于引用类型(channels, functions,pointers, slices, 和 maps), 它十六进制打印类型的引用地址. 虽然还不够理想, 但是依然是一个重大的进步, 并且 Kind 只关心底层表示, format.Any 也支持新命名的类型. 例如:

var x int64 = 1
var d time.Duration = 1 * time.Nanosecond
fmt.Println(format.Any(x)) // "1"
fmt.Println(format.Any(d)) // "1"
// "[]int64 0x8202b87b0"
fmt.Println(format.Any([]int64{x})) 
// "[]time.Duration 0x8202b87e0"
fmt.Println(format.Any([]time.Duration{d})) 
八、Golang的反射reflect深入理解和示例

1.未知原有类型【遍历探测其Filed】
很多情况下,我们可能并不知道其具体类型,那么这个时候,该如何做呢?需要我们进行遍历探测其Filed来得知,示例如下:

package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Id   int
    Name string
    Age  int
}

func (u User) ReflectCallFunc() {
    fmt.Println("Allen.Wu ReflectCallFunc")
}

func main() {

    user := User{1, "Allen.Wu", 25}

    DoFiledAndMethod(user)

}

// 通过接口来获取任意参数,然后一一揭晓
func DoFiledAndMethod(input interface{}) {

    getType := reflect.TypeOf(input)
    fmt.Println("get Type is :", getType.Name())

    getValue := reflect.ValueOf(input)
    fmt.Println("get all Fields is:", getValue)

    // 获取方法字段
    // 1. 先获取interface的reflect.Type,然后通过NumField进行遍历
    // 2. 再通过reflect.Type的Field获取其Field
    // 3. 最后通过Field的Interface()得到对应的value
    for i := 0; i < getType.NumField(); i++ {
        field := getType.Field(i)
        value := getValue.Field(i).Interface()
        fmt.Printf("%s: %v = %vn", field.Name, field.Type, value)
    }

    // 获取方法
    // 1. 先获取interface的reflect.Type,然后通过.NumMethod进行遍历
    for i := 0; i < getType.NumMethod(); i++ {
        m := getType.Method(i)
        fmt.Printf("%s: %vn", m.Name, m.Type)
    }
}

运行结果:
get Type is : User
get all Fields is: {1 Allen.Wu 25}
Id: int = 1
Name: string = Allen.Wu
Age: int = 25
ReflectCallFunc: func(main.User)

这里注意,getType和getValue返回的Field是不同的,另外Elem也不同:
getType.Field(i int) StructField
getValue.Field(i int) Value
getType. Elem() Type
getValue:func (v Value) Elem() Value {


type StructField struct {

    Name string         // name 常用

    PkgPath string

 

    Type      Type      // field type 常用

    Tag       StructTag // field tag string

    Offset    uintptr   // offset within struct, in bytes

    Index     []int     // index sequence for Type.FieldByIndex

    Anonymous bool      // is an embedded field

}

2.通过reflect.Value设置实际变量的值
reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。

示例如下:

package main

import (
    "fmt"
    "reflect"
)

func main() {

    var num float64 = 1.2345
    fmt.Println("old value of pointer:", num)

    // 通过reflect.ValueOf获取num中的reflect.Value,
    //注意,参数必须是指针才能修改其值
    pointer := reflect.ValueOf(&num)
    newValue := pointer.Elem()

    fmt.Println("type of pointer:", newValue.Type())
    fmt.Println("settability of pointer:", newValue.CanSet())

    // 重新赋值
    newValue.SetFloat(77)
    fmt.Println("new value of pointer:", num)

    ////////////////////
    // 如果reflect.ValueOf的参数不是指针,会如何?
    pointer = reflect.ValueOf(num)
    // 如果非指针,这里直接panic,“panic: reflect: 
    //call of reflect.Value.Elem on float64 Value”
    //newValue = pointer.Elem() 
}

运行结果:
old value of pointer: 1.2345
type of pointer: float64
settability of pointer: true
new value of pointer: 77

3.通过reflect.ValueOf来进行方法的调用
这算是一个高级用法了,前面我们只说到对类型、变量的几种反射的用法,包括如何获取其值、其类型、如果重新设置新值。但是在工程应用中,另外一个常用并且属于高级的用法,就是通过reflect来进行方法【函数】的调用。比如我们要做框架工程的时候,需要可以随意扩展方法,或者说用户可以自定义方法,那么我们通过什么手段来扩展让用户能够自定义呢?关键点在于用户的自定义方法是未可知的,因此我们可以通过reflect来搞定

示例如下:

package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Id   int
    Name string
    Age  int
}

func (u User) ReflectCallFuncHasArgs(name string, age int) {
    fmt.Println("ReflectCallFuncHasArgs name: ", name, 
    ", age:", age, "and origal User.Name:", u.Name)
}

func (u User) ReflectCallFuncNoArgs() {
    fmt.Println("ReflectCallFuncNoArgs")
}

// 如何通过反射来进行方法的调用?
// 本来可以用u.ReflectCallFuncXXX直接调用的,但是如果要通过反射,
//那么首先要将方法注册,也就是MethodByName,然后通过反射调动mv.Call

func main() {
    user := User{1, "Allen.Wu", 25}
    
    // 1. 要通过反射来调用起对应的方法,必须要先通过
    //reflect.ValueOf(interface)来获取到reflect.Value,
    //得到“反射类型对象”后才能做下一步处理
    getValue := reflect.ValueOf(user)

    // 一定要指定参数为正确的方法名
    // 2. 先看看带有参数的调用方法
    methodValue := getValue.MethodByName("ReflectCallFuncHasArgs")
    args := []reflect.Value{reflect.ValueOf("wudebao"), reflect.ValueOf(30)}
    methodValue.Call(args)

    // 一定要指定参数为正确的方法名
    // 3. 再看看无参数的调用方法
    methodValue = getValue.MethodByName("ReflectCallFuncNoArgs")
    args = make([]reflect.Value, 0)
    methodValue.Call(args)
}


运行结果:
ReflectCallFuncHasArgs name:  wudebao , age: 30 and origal User.Name: Allen.Wu
ReflectCallFuncNoArgs
九、golang中struct关于反射tag

关于结构体中的json Tag,可以参考Golang json。其实除了json Tag,结构体也支持自定义Tag,然后自己利用反射来解析,可以参考golang自定义struct字段标签

package main

import (
    "fmt"
    "reflect"
)

type User struct {
    name string `json:name-field`
    age  int
}

func main() {
    user := &User{"John Doe The Fourth", 20}

    field, ok := reflect.TypeOf(user).Elem().FieldByName("name")
    if !ok {
        panic("Field not found")
    }
    fmt.Println(getStructTag(field))
}

func getStructTag(f reflect.StructField) string {
    return string(f.Tag)
}
output:

json:name-field

由反射可直接得到结构域,调用结构域中的Tag即可获取到tag进行处理。在golang中,例如著名的xorm包都没有加入mongodb的支持。利用golang的struct很好的支持JSON还是比较困难的,大部分都得依赖tag。在底层使用反射获得tag内容还是非常有必要的。

看一下源码:

// A StructTag is the tag string in a struct field.
//
// By convention, tag strings are a concatenation of
// optionally space-separated key:"value" pairs.
// Each key is a non-empty string consisting of non-control
// characters other than space (U+0020 ' '), quote (U+0022 '"'),
// and colon (U+003A ':').  Each value is quoted using U+0022 '"'
// characters and Go string literal syntax.
type StructTag string

// Get returns the value associated with key in the tag string.
// If there is no such key in the tag, Get returns the empty string.
// If the tag does not have the conventional format, the value
// returned by Get is unspecified. To determine whether a tag is
// explicitly set to the empty string, use Lookup.
func (tag StructTag) Get(key string) string {
    v, _ := tag.Lookup(key)
    return v
}

// Lookup returns the value associated with key in the tag string.
// If the key is present in the tag the value (which may be empty)
// is returned. Otherwise the returned value will be the empty string.
// The ok return value reports whether the value was explicitly set in
// the tag string. If the tag does not have the conventional format,
// the value returned by Lookup is unspecified.
func (tag StructTag) Lookup(key string) (value string, ok bool) {
...

使用Get方法:

package main

import (
    "fmt"
    "reflect"
)

type Config struct {
    Listen       string `json:"listen" toml:"listen"`
    UserAPIURL   string `json:"userapisrv" toml:"userAPIURL"`
    SecretKey    string `json:"secret_key" toml:"secret_key"`
    AuthorityUrl string `json:"authorityapisrv" toml:"authority_url"`
    ResourceKey  string `json:"resource_key" toml:"resource_key"`
}

func main() {
    c := &Config{}
    cType := reflect.TypeOf(c)
    cValue := reflect.ValueOf(c)

    structLen := cValue.Elem().NumField()
    structMap := make(map[string]string, structLen)

    for i := 0; i < structLen; i++ {
        field := cType.Elem().Field(i)
        jsonName := field.Tag.Get("json")
        fmt.Println(fmt.Sprintf("index:%d,jsonName:%s",i,jsonName))
        structMap[jsonName] = field.Name
    }
}

------------------
index:0,jsonName:listen
index:1,jsonName:userapisrv
index:2,jsonName:secret_key
index:3,jsonName:authorityapisrv
index:4,jsonName:resource_key
版权声明:本文来源简书,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://www.jianshu.com/p/444b55edf32e
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-01-09 22:13:46
  • 阅读 ( 912 )
  • 分类:Go

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢