40.Go 反射

Go是一种静态类型的语言。 在大多数情况下,变量的类型在编译时是已知的。接口类型是一种例外,尤其是空接口interface {}。

空接口是一种动态类型,类似于Java或C#中的Object。

在编译时,我们无法确定接口类型的基础值是整数还是字符串。

标准库中的reflect包,使我们可以在运行时使用此类动态值。 我们可以:

  • 检查动态值的类型
  • 枚举结构的字段
  • 设定值
  • 在运行时创建新值
    用于在运行时检查接口值的类型的相关语言级功能是类型switch和类型断言:
var v interface{} = 4
var reflectVal reflect.Value = reflect.ValueOf(v)

var typ reflect.Type = reflectVal.Type()
fmt.Printf("Type '%s' of size: %d bytes\n", typ.Name(), typ.Size())
if typ.Kind() == reflect.Int {
    fmt.Printf("v contains value of type int\n")
}

Type 'int' of size: 8 bytes
v contains value of type int

反射的基础是:

  • 以空接口interface{}类型的值开头
  • 使用reflect.ValueOf(v interface {})来获取reflect.Value,它代表有关该值的信息
  • 使用reflect.Value检查值的类型,测试值是否为nil,设置值

反射有几种实际用途。

原始类型

让我们看看可以对基本类型(例如int或string)执行哪种操作。

获取类型

func printType(v interface{}) {
    rv := reflect.ValueOf(v)
    typ := rv.Type()
    typeName := ""
    switch rv.Kind() {
    case reflect.Ptr:
        typeName = "pointer"
    case reflect.Int:
        typeName = "int"
    case reflect.Int32:
        typeName = "int32"
    case reflect.String:
        typeName = "string"
    // ... handle more cases
    default:
        typeName = "unrecognized type"
    }
    fmt.Printf("v is of type '%s'. Size: %d bytes\n", typeName, typ.Size())
}

printType(int32(3))
printType("")
i := 3
printType(&i) // *int i.e. pointer to int

v is of type 'int32'. Size: 4 bytes
v is of type 'string'. Size: 16 bytes
v is of type 'pointer'. Size: 8 bytes

在真实代码中,你需要处理关心的所有类型。

获取值

func getIntValue(v interface{}) {
    var reflectValue = reflect.ValueOf(v)
    n := reflectValue.Int()
    fmt.Printf("Int value is: %d\n", n)
}

getIntValue(3)
getIntValue(int8(4))
getIntValue("")

Int value is: 3
Int value is: 4
panic: reflect: call of reflect.Value.Int on string Value

goroutine 1 [running]:
reflect.Value.Int(...)
/usr/local/go/src/reflect/value.go:986
main.getIntValue(0x4a0120, 0x4db0d0)
/tmp/src282322283/main.go:14 +0x204
main.main()
/tmp/src282322283/main.go:24 +0x7b
exit status 2

为了最大程度地减少API表面,Int()返回int64并对所有带符号的整数值(int8,int16,int32,int64)有效。

UInt()方法返回uint64并对每个无符号整数值(uint8,uint16,uint32,uint64)有效。

试图从不兼容类型(如字符串)的值中获取整数值会引发panic。

为了避免出现panic,您可以先使用Kind()检查类型。

检索值的所有方法:

  • Bool() bool
  • Int() int64
  • UInt() uint64
  • Float() float64
  • String() string
  • Bytes() []byte

设置值

type S struct {
    N int
}

func setIntPtr() {
    var n int = 2
    reflect.ValueOf(&n).Elem().SetInt(4)
    fmt.Printf("setIntPtr: n=%d\n", n)
}

func setStructFieldDirect() {
    var s S
    reflect.ValueOf(&s.N).Elem().SetInt(5)
    fmt.Printf("setStructFieldDirect: n=%d\n", s.N)
}

func setStructPtrField() {
    var s S
    reflect.ValueOf(&s).Elem().Field(0).SetInt(6)
    fmt.Printf("setStructPtrField: s.N: %d\n", s.N)
}

func handlePanic(funcName string) {
    if msg := recover(); msg != nil {
        fmt.Printf("%s panicked with '%s'\n", funcName, msg)
    }
}

func setStructField() {
    defer handlePanic("setStructField")
    var s S
    reflect.ValueOf(s).Elem().Field(0).SetInt(4)
    fmt.Printf("s.N: %d\n", s.N)
}

func setInt() {
    defer handlePanic("setInt")
    var n int = 2
    rv := reflect.ValueOf(n)
    rv.Elem().SetInt(4)
}

func setIntPtrWithString() {
    defer handlePanic("setIntPtrWithString")
    var n int = 2
    reflect.ValueOf(&n).Elem().SetString("8")
}

setIntPtr: n=4
setStructFieldDirect: n=5
setStructPtrField: s.N: 6
setInt panicked with 'reflect: call of reflect.Value.Elem on int Value'
setStructField panicked with 'reflect: call of reflect.Value.Elem on struct Value'
setIntPtrWithString panicked with 'reflect: call of reflect.Value.SetString on int Value'

如setInt和setStructField所示,只有从指向值的指针开始,才可以更改值。

如setInt和setStructField所示,只有从指向值的指针开始,才可以更改值。

setStructPtrField显示如何通过字段值在结构中的位置来获取对字段值的引用。

尝试设置不兼容类型的值会引发panic。

设置值的方法反映了读取值的方法:

  • SetBool(v bool)
  • SetInt(v int64)
  • SetUInt(v uint64)
  • SetFloat(v float64)
  • SetString(v string)
  • SetBytes(v []byte)

指针

指向X的指针与X是不同的类型。

如果reflect.Value引用了指向值的指针,则可以通过调用Elem()获得对该值的引用。

func printIntResolvingPointers(v interface{}) {
    rv := reflect.ValueOf(v)
    typeName := rv.Type().String()
    name := ""
    for rv.Kind() == reflect.Ptr {
        name = "pointer to " + name
        rv = rv.Elem()
    }
    name += rv.Type().String()
    fmt.Printf("Value: %d. Type: '%s' i.e. '%s'.\n\n", rv.Int(), name, typeName)
}

func main() {
    n := 3
    printIntResolvingPointers(n)

    n = 4
    printIntResolvingPointers(&n)

    n = 5
    np := &n
    printIntResolvingPointers(&np)
}

Value: 3. Type: 'int' i.e. 'int'.

Value: 4. Type: 'pointer to int' i.e. '*int'.

Value: 5. Type: 'pointer to pointer to int' i.e. '**int'.

在底层,接口也是指向其基础值的指针,因此Elem()也可以在代表接口的reflect.Value上工作。

struct

列出struct的字段

使用反射,我们可以列出struct的所有字段。

type S struct {
    FirstName string `my_tag:"first-name"`
    lastName  string
    Age       int `json:"age",xml:"AgeXml`
}

func describeStructSimple(rv reflect.Value) {
    structType := rv.Type()
    for i := 0; i < rv.NumField(); i++ {
        v := rv.Field(i)
        structField := structType.Field(i)
        name := structField.Name
        typ := structField.Type
        tag := structField.Tag
        jsonTag := tag.Get("json")
        isExported := structField.PkgPath == ""
        if isExported {
            fmt.Printf("name: '%s',\ttype: '%s', value: %v,\ttag: '%s',\tjson tag: '%s'\n", name, typ, v.Interface(), tag, jsonTag)
        } else {
            fmt.Printf("name: '%s',\ttype: '%s',\tvalue: not accessible\n", name, v.Type().Name())
        }
    }
}

func main() {
    s := S{
        FirstName: "John",
        lastName:  "Doe",
        Age:       27,
    }
    describeStructSimple(reflect.ValueOf(s))
}

name: 'FirstName', type: 'string', value: John, tag: 'my_tag:"first-name"', json tag: ''
name: 'lastName', type: 'string', value: not accessible
name: 'Age', type: 'int', value: 27, tag: 'json:"age",xml:"AgeXml', json tag: 'age'

使用反射,我们只能访问导出的字段的值(v.Interface {})。

导出的字段是名称以大写字母开头的字段(导出的是“姓氏”和“年龄”,而不是“ lastName”)。

如果reflect.StructField.PkgPath ==“”,则导出字段。

递归列出结构的字段

检查struct本质上是递归过程。

您必须追逐指针并递归到嵌入式struct中。

在实际程序中,使用反射检查struct是递归的。

type Inner struct {
    N int
}

type S struct {
    Inner
    NamedInner Inner
    PtrInner   *Inner
    unexported int
    N          int8
}

func indentStr(level int) string {
    return strings.Repeat("  ", level)
}

// if sf is not nil, this is a field of a struct
func describeStruct(level int, rv reflect.Value, sf *reflect.StructField) {
    structType := rv.Type()
    nFields := rv.NumField()
    typ := rv.Type()
    if sf == nil {
        fmt.Printf("%sstruct %s, %d field(s), size: %d bytes\n", indentStr(level), structType.Name(), nFields, typ.Size())
    } else {
        fmt.Printf("%sname: '%s' type: 'struct %s', offset: %d, %d field(s), size: %d bytes, embedded: %v\n", indentStr(level), sf.Name, structType.Name(), sf.Offset, nFields, typ.Size(), sf.Anonymous)
    }

    for i := 0; i < nFields; i++ {
        fv := rv.Field(i)
        sf := structType.Field(i)
        describeType(level+1, fv, &sf)
    }
}

// if sf is not nil, this is a field of a struct
func describeType(level int, rv reflect.Value, sf *reflect.StructField) {
    switch rv.Kind() {

    case reflect.Int, reflect.Int8:
        // in real code we would handle more primitive types
        i := rv.Int()
        typ := rv.Type()
        if sf == nil {
            fmt.Printf("%stype: '%s', value: '%d'\n", indentStr(level), typ.Name(), i)
        } else {
            fmt.Printf("%s name: '%s' type: '%s', value: '%d', offset: %d, size: %d\n", indentStr(level), sf.Name, typ.Name(), i, sf.Offset, typ.Size())
        }

    case reflect.Ptr:
        fmt.Printf("%spointer\n", indentStr(level))
        describeType(level+1, rv.Elem(), nil)

    case reflect.Struct:
        describeStruct(level, rv, sf)
    }
}

func main() {
    var s S
    describeType(0, reflect.ValueOf(s), nil)
}

struct S, 5 field(s), size: 40 bytes
name: 'Inner' type: 'struct Inner', offset: 0, 1 field(s), size: 8 bytes, embedded: true
name: 'N' type: 'int', value: '0', offset: 0, size: 8
name: 'NamedInner' type: 'struct Inner', offset: 8, 1 field(s), size: 8 bytes, embedded: false
name: 'N' type: 'int', value: '0', offset: 0, size: 8
pointer
name: 'unexported' type: 'int', value: '0', offset: 24, size: 8
name: 'N' type: 'int8', value: '0', offset: 32, size: 1

slice

使用反射读取切片

a := []int{3, 1, 8}
rv := reflect.ValueOf(a)

fmt.Printf("len(a): %d\n", rv.Len())
fmt.Printf("cap(a): %d\n", rv.Cap())

fmt.Printf("slice kind: '%s'\n", rv.Kind().String())

fmt.Printf("element type: '%s'\n", rv.Type().Elem().Name())

el := rv.Index(0).Interface()
fmt.Printf("a[0]: %v\n", el)

elRef := rv.Index(1)
fmt.Printf("elRef.CanAddr(): %v\n", elRef.CanAddr())
fmt.Printf("elRef.CanSet(): %v\n", elRef.CanSet())

elRef.SetInt(5)
fmt.Printf("a: %v\n", a)

len(a): 3
cap(a): 3
slice kind: 'slice'
element type: 'int'
a[0]: 3
elRef.CanAddr(): true
elRef.CanSet(): true
a: [3 5 8]

使用反射创建新切片

typ := reflect.SliceOf(reflect.TypeOf("example"))
// create slice with capacity 10 and length 1
rv := reflect.MakeSlice(typ, 1, 10)
rv.Index(0).SetString("foo")

a := rv.Interface().([]string)
fmt.Printf("a: %#v\n", a)

a: []string{"foo"}

reflect.Kind

Reflection.Value上的函数Kind()返回描述值类型的reflect.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
)

反射用途

序列化为JSON,XML,SQL,protobufs

反射使实现通用JSON序列化/反序列化成为可能。

对于通用JSON序列化,我们可以枚举任意结构的字段,读取它们的字段并创建相应的JSON字符串。

对于一般的JSON反序列化,我们可以枚举任意结构的字段并根据已解析的JSON数据进行设置。

其他序列化格式(例如XML,YAML或协议缓冲区)也是如此。

反射使为SQL数据库定义通用API成为可能,因为我们可以将任意结构转换为SQL数据库可以理解的格式,并将从SQL数据库接收的数据放入任意结构。

使用Go函数扩展模板语言

由于能够在运行时调用任意函数,因此我们可以在文本/模板中为模板定义自定义函数。 我们向模板引擎注册Go函数。

然后,引擎可以在执行模板时在运行时调用这些函数。

编写与Go紧密集成的口译员

由于反射功能可以在运行时调用任意函数,因此JavaScript解释器可以轻松地扩展为用Go编写的其他函数。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • 目录 通过反射获取类型信息[https://www.cnblogs.com/itbsl/p/10551880.ht...
    gurlan阅读 520评论 0 0
  • 一、认识反射 维基百科中的定义:在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测和修改...
    Every_dawn阅读 1,572评论 0 0
  • 参考 :http://c.biancheng.net/view/4407.html[http://c.bianch...
    天空蓝雨阅读 601评论 0 0
  • 能力模式 选择题 【初级】下面属于关键字的是()A. funcB. defC. structD. class 参考...
    灵魂深灵阅读 5,280评论 2 5
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,520评论 28 53