Golang 接口与反射知识要点

这篇文章以 Go 官方经典博客 The Laws of Reflection 为基础,详细介绍文中涉及的知识点,并有所扩展。

1. 接口类型变量

首先,我们谈谈接口类型的内存布局(memory layout),其他基础类型、Struct、Slice、Map、指针类型的内存布局会在以后单独分析。接口变量的值包含两部分内容:赋值给接口类型变量的实际值(concrete value),实际值的类型信息(type descriptor)。两部分在一起构成接口的值(interface value)。

接口变量的这两部分内容由两个字来存储(假设是 32 位系统,那么一个字就是 32 位),第一个字指向 itable (interface table)。itable 表示 interface 和实际类型的转换信息。itable 开头是一个存储了变量实际类型的描述信息,接着是一个由函数指针组成的列表。注意 itable 中的函数和接口类型相对应,而不是和动态类型。例如下面例子,itable 只关联了 Stringer 中定义的 String 方法,而 Binary 中定义的 Get 方法则不在其中。对于每个 interface 和实际类型,只要在代码中存在引用关系, go 就会在运行时为这一对具体的 <Interface, Type> 生成 itable 信息。

第二个字称为 data,指向实际的数据。例子中,赋值语句 var s Stringer = b 实际上对b做了拷贝,而不是对b进行引用。存放在接口变量中的数据大小可能任意,但接口只提供了一个字来专门存储真实数据,所以赋值语句在堆上分配了一块内存,并将该字设置为对这块内存的引用

type Stringer interface {
    String() string
}

type Binary uint64

func (i Binary) String() string {
    return strconv.Uitob64(i.Get(), 2)
}

func (i Binary) Get() uint64 {
    return uint64(i)
}

b := Binary(200)
var s Stringer = b

Golang 接口与反射知识要点-LMLPHP

Go 是静态类型语言(statically typed)。一个接口类型的不同变量总是有同样静态类型,尽管在运行时,接口变量的保存的实际值会改变。下面例子中,无论 r 被赋予的什么实际值,r 的类型总是 io.Reader。

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on

2. 类型断言

类型断言是一个使用在接口变量上的操作。

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}
r = tty

在这个例子中,r 被赋予了 tty 的一个拷贝,所以实际值是 tty。而实际类型是 *os.File。需要注意到,*os.File 类型自身还实现了除接口方法 Read 以外的方法。尽管接口变量只能访问 Read 方法,但接口的 data 字部分里携带了实际值的全部信息。因此我们可以有如下操作:

var w io.Writer
w = r.(io.Writer)

该赋值语句后边是一个类型断言。它断言的是 r 变量携带的元素,同时是 io.Writer 接口的实现,所以我们才能把 r 赋值给 w。赋值后的 w 可以访问 Write 方法,但无法访问 Read 方法了。

3. 鸭子类型

鸭子类型(duck typing)是动态类型和某些静态语言用到的一种对象推断风格。一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。这个概念也可以表述为:

鸭子类型像多态一样工作,但是没有继承。在鸭子类型中,关注点在于对象的行为,能作什么;而不是关注对象所属的类型。在常规类型中,我们能否在一个特定场景中使用某个对象取决于这个对象的类型,而在鸭子类型中,则取决于这个对象是否具有某种属性或者方法 —— 即只要具备特定的属性或方法,能通过鸭子类型测试,就可以使用。鸭子类型的缺点是没有任何静态检查,如类型检查、属性检查、方法签名检查等。

Go 语言虽然是静态语言,但在接口类型中使用了鸭子类型。不同于其他鸭子类型语言的是,它实现了在编译时进行静态检查,比如变量是否实现接口方法、调用接口方法时参数个数是否相符,同时也不失鸭子类型带来的灵活和自由。

4. 反射机制

  • 什么是反射机制?

简单来说,反射只是一种机制,在程序运行时获得对象类型信息和内存结构。通常高级语言借助反射机制来解决,编译时无法知道变量具体类型,而只有等到运行时才能检查值和类型的问题。不同语言的反射模型不尽相同,有些语言还不支持反射。对于低级语言,比如汇编语言,由于自身可以直接和内存打交道,所以无需反射机制。

  • 使用反射的场景?

Go 语言中使用反射的场景:有时候需要根据某些条件决定调用哪个函数,比如根据用户的输入来决定,但事先无法不知道接受到的参数是什么类型,全部以 interface{} 类型接受。这时就需要对函数的参数进行反射,在运行期间动态地执行函数。感兴趣的读者可以参考 fmt.Sprint(a ...interface{}) 方法的源码。

5. reflect 包

TypeOf()、ValueOf()

reflect 包封装了很多简单的方法(reflect.TypeOf 和 reflect.ValueOf)来动态获得类型信息和实际值(reflect.Type,reflect.Value)。

var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))  // 打印 type: float64

var r io.Reader = strings.NewReader("Hello")
fmt.Println("type:", reflect.TypeOf(r))  // 打印 type: *strings.Reader

reflect.TypeOf 方法的函数签名是 func TypeOf(i interface{}) Type 。它接受任意类型的变量。当我们调用 reflect.TypeOf(x) 时,x 首先存储在一个空接口类型中,作为传参。reflect.TypeOf 解析空接口,恢复 x 的类型信息。而调用 reflect.ValueOf 则可以恢复 x 实际值。

var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String()) // 打印 value: <float64 Value>

Type()、Kind()

reflect.Type 和 reflect.Value 都提供了很多方法支持来操作他们。1. reflect.Value 的 Type() 方法返回实际类型信息;2. reflect.Type 和 reflect.Value 都有 Kind() 方法,来获得实际值的底层类型,结果对应的是 reflect 包中定义的常量;3. reflect.Value 的那些以类型名为方法名的方法,比如 Int()、Float(),能获得实际值。

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

打印结果:

shell script type: float64 kind is float64: true value: 3.4

有一点需要注意的是,Kind() 方法返回的是反射对象的底层类型,而不是静态类型。比如,如果反射对象接受一个用户定义的整数型变量:

func main() {
    type MyInt int
    var x MyInt = 7
    v := reflect.ValueOf(x)
    fmt.Println("type:", v.Type())
    fmt.Println("kind is int:", v.Kind() == reflect.Int)
    fmt.Println("value:", v.Int())
}

打印结果:
shell script type: main.MyInt kind is int: true value: 7

v 调用 Kind() 仍是 reflect.Int,即使 x 的静态类型是 MyInt 而不是 int。总而言之,Kind() 方法无法区分来自 MyInt 的整数型和 int 型,但 Type() 方法可以

Interface()

Interface() 方法能从 reflect.Value 变量中恢复接口值,是 ValueOf() 的逆向。注意的是,Interface() 方法返回总是静态类型 interface{}。

6. 反射对象的可设置性

SetXXX(), CanSet()

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)  // will panic: reflect.Value.SetFloat using unaddressable value

运行上面的例子,我们可以发现 v 不可修改(settable)。可设置性(Settability)是 reflect.Value 的一个特性,但不是所有的 Value 都有。

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())  // settability of v: false

Elem()

由于是 x 的一个拷贝传入 reflect.ValueOf,所以 reflect.ValueOf 创建的接口值也是 x 的一个拷贝,不是原 x 本身。因此修改反射对象,无法修改 x,反射对象不具有可设置性。

显然,要使反射对象具有可设置性。传入 reflect.ValueOf 的参数应该是 x 的地址,即 &x。

var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())  // type of p: *float64
fmt.Println("settability of p:", p.CanSet())  // settability of p: false

反射对象 p 仍是不可设置的,因为我们不是要设置 p,而是 p 所指向的内容。使用 Elem 方法获取。

// Elem returns the value that the interface v contains
// or that the pointer v points to.
// It panics if v's Kind is not Interface or Ptr.
// It returns the zero Value if v is nil.
func (v Value) Elem() Value
v := p.Elem()
fmt.Println("settability of v:", v.CanSet())  // settability of v: true

v.SetFloat(7.1)
fmt.Println(v.Interface())  // 7.1
fmt.Println(x)  // 7.1

7. Struct 的反射

NumField(), Type.Field(i int)

我们用 struct 的地址来创建反射对象,这样后续我们可以修改这个 struct:

type T struct {
    A int
    B string
}

t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()

for i := 0; i < s.NumField(); i++ {
    f := s.Field(i)
    fmt.Printf("%d: %s %s = %v\n", i,
        typeOfT.Field(i).Name, f.Type(), f.Interface())
}

Type.Field(i int) 方法返回字段信息,一个 StructField 类型的对象,包含字段名等。

打印结果:

0: A int = 23
1: B string = skidoo

Value.Field(i int)

T 的字段必须是首字母大写的才可以设置,因为只有暴露的 struct 字段,才具有可设置性

s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t) // t is now {77 Sunset Strip}

Value.Field(i int) 返回 struct s 的字段实际值,所以可以用来设置操作。注意 Type.Field(i int) 和 Value.Field(i int) 的用途区别:前者总是负责和实际类型信息获取相关的操作,后者是与实际值相关的操作

参考文档

The Laws of Reflection

Go Data Structures: Interfaces

Go 语言的数据结构:Interfaces

浅析 Golang Interface 实现原理

深度解密Go语言之反射

10-04 11:50