Go 接口与反射的关系

Dec 31, 2018 00:40 · 1472 words · 3 minute read Golang

作为 Go 语言中为抽象而生的基本工具之一,接口在分配值时存储类型信息,而反射则是一种在运行时检查类型和值信息的方法。

reflect 包提供了检查接口甚至在运行时修改值的方法,Go 使用它实现了反射。

向接口分配值

接口打包了三样东西:

  • 方法集
  • 存储的值的类型

如下图:

图中可以清楚地看到接口的三个部分:_type 是类型信息,*data 是指向真实值的指针,itab 包含方法集。

当一个函数接受了一个接口作为参数时,传递给函数的接口打包了值、方法集和类型。

通过反射包在运行时检查接口数据

一旦值存储在接口中,可以使用 reflect 包来检查它的各个部分。我们不能直接检查接口结构,而反射包维护一份有权限访问的接口结构的副本。

甚至通过反射对象访问接口,也是和底层接口直接相关的。

reflect.Typereflect.Value 提供访问接口成分的方法。reflect.Type 暴露接口的 _type 部分(有关类型的信息),而 reflect.Value 允许程序员检查和操作值。

reflect.Type (检查类型)

reflect.TypeOf() 函数用来提取值的类型。既然它唯一的参数是空接口,传递给它的参数将被分配成一个接口,因此就有了类型、方法集和值。

reflect.TypeOf() 函数返回 reflect.Type,它有些方法可以让你查出值的类型。

package main

import (
    "fmt"
    "log"
    "reflect"
)

type Gift struct {
    Sender    string
    Recipient string
    Number    uint
    Contents  string
}

func main() {
    g := Gift{
        Sender:    "Hank",
        Recipient: "Sue",
        Number:    1,
        Contents:  "Scarf",
    }

    t := reflect.TypeOf(g)

    if kind := t.Kind(); kind != reflect.Struct {
        log.Fatalf("This program expects to work on a struct; we got a %v instead.", kind)
    }

    for i := 0; i < t.NumField(); i++ {
        f := t.Field(i)
        fmt.Printf("Field %03d: %-10.10s %v", i, f.Name, f.Type.Kind())
    }
}

这段代码的目的是打印 Gift 结构的字段。当 g 作为参数传递给 reflect.TypeOf() 函数,g 就被分配成一个接口,编译器自动填充类型、方法集和值三样东西。这就允许我们遍历接口结构中类型部分的 []fields

Field 000: Sender     string
Field 001: Recipient  string
Field 002: Number     uint
Field 003: Contents   string

reflect.Method(检查方法集)

reflect.Type 类型也允许访问 itab 部分来提取接口的方法信息。

package main

import (
    "log"
    "reflect"
)

type Reindeer string

func (r Reindeer) TakeOff() {
    log.Printf("%q lifts off.", r)
}

func (r Reindeer) Land() {
    log.Printf("%q gently lands.", r)
}

func (r Reindeer) ToggleNose() {
    if r != "rudolph" {
        panic("invalid reindeer operation")
    }
    log.Printf("%q nose changes state.", r)
}

func main() {
    r := Reindeer("rudolph")

    t := reflect.TypeOf(r)

    for i := 0; i < t.NumMethod(); i++ {
        m := t.Method(i)
        log.Printf("%s", m.Name)
    }
}

这段代码迭代 itab 中存储的数据并展示了每个方法的名称:

Land
TakeOff
ToggleNose

reflect.Value(检查值)

到此为止我们已经讨论了如何查看类型和方法,最后 reflect.Value 提供了接口中存储的真实值的信息。

reflect.Value 相关的方法必然将类型信息与真实值联合。举个例子,要提取结构体的字段,反射包必须结合接口的布局信息——尤其是字段的信息和 _type 中存储的字段偏移量还有 *data 指向的真实值。

package main

import (
    "fmt"
    "log"
    "reflect"
)

type Child struct {
    Name  string
    Grade int
    Nice  bool
}

type Adult struct {
    Name       string
    Occupation string
    Nice       bool
}

// Search a slice of structs for Name field that is "Hank" and set its Nice field to true.
func nice(i interface{}) {
    // Retrieve the underlying value of i. We know that i is an interface.
    v := reflect.ValueOf(i)

    // we're only interested in slices to let's check what kind of value v is. If it isn't a slice, return immediately.
    if v.Kind() != reflect.Slice {
        return
    }

    // v is a slice. Now let's ensure that it is a slice of structs. If not, return immediately.
    if e := v.Type().Elem(); e.Kind() != reflect.Struct {
        return
    }

    // Determine if our struct has a Name field of type string and a Nice field of type bool
    st := v.Type().Elem()

    if nameField, found := st.FieldByName("Name"); found == false || nameField.Type.Kind() != reflect.String {
        return
    }

    if niceField, found := st.FieldByName("Nice"); found == false || niceField.Type.Kind() != reflect.Bool {
        return
    }

    // Set any Nice fields to true where the Name is "Hank"
    for i := 0; i < v.Len(); i++ {
        e := v.Index(i)
        name := e.FieldByName("Name")
        nice := e.FieldByName("Nice")

        if name.String() == "Hank" {
            nice.SetBool(true)
        }
    }
}

func main() {
    children := []Child{
        {Name: "Sue", Grade: 1, Nice: true},
        {Name: "Ava", Grade: 3, Nice: true},
        {Name: "Hank", Grade: 6, Nice: false},
        {Name: "Nancy", Grade: 5, Nice: true},
    }

    adults := []Adult{
        {Name: "Bob", Occupation: "Carpenter", Nice: true},
        {Name: "Steve", Occupation: "Clerk", Nice: true},
        {Name: "Nikki", Occupation: "Rad Tech", Nice: false},
        {Name: "Hank", Occupation: "Go Programmer", Nice: false},
    }

    fmt.Printf("adults before nice: %v", adults)
    nice(adults)
    fmt.Printf("adults after nice: %v", adults)

    fmt.Printf("children before nice: %v", children)
    nice(children)
    fmt.Printf("children after nice: %v", children)
}
adults before nice: [{Bob Carpenter true} {Steve Clerk true} {Nikki Rad Tech false} {Hank Go Programmer false}]
adults after nice: [{Bob Carpenter true} {Steve Clerk true} {Nikki Rad Tech false} {Hank Go Programmer true}]
children before nice: [{Sue 1 true} {Ava 3 true} {Hank 6 false} {Nancy 5 true}]
children after nice: [{Sue 1 true} {Ava 3 true} {Hank 6 true} {Nancy 5 true}]

注意,nice() 可以更改你传递给它的任意切片的值,不管它接收到了什么类型的参数。

结论

Go 中的反射使用接口和 reflect 反射包实现,没啥黑科技——使用反射时就是直接访问接口里面的内容。

接口更像是一面镜子,允许程序来检查自身。

尽管 Go 是一门静态语言,但是反射和接口相结合非常强大,这个通常动态语言才有。

要想更多关于反射的信息,可以看看包的文档。