类型和接口
Go
是静态类型语言。每一个变量都有一个静态的类型,即在编译时类型已知且固定:比如
int
、float32
。
接口类型
接口类型是类型的一个重要类别,它表示固定的方法集。接口变量可以存储任何具体值(非接口),只要该值实现接口的方法即可。如:
1 | // Reader 是封装基本 Read 方法的接口 |
任何实现了 Read(p []byte) (n int, err error)
方法的类型都被称为实现了 Reader
接口(Writer
同理)。这意味着 Reader
可以保存实现了 Read
方法的任何值:
1 | var r io.Reader |
需要明确的是,不管 r
可能包含什么具体值,r
的类型始终是 io.Reader
:Go 是静态类型的语言,而
r
的静态类型是 io.Reader
。
空接口
接口类型的一个非常重要的示例是空接口:
1 | interface{} |
它表示空的方法集,并且任何值都满足空接口,因为任何值都有零个或者多个方法。
有人说 Go 的接口是动态类型的,但这会产生误导。接口是静态类型的:接口类型的变量始终具有相同的类型,即使在运行时存储在接口变量中的值可能会更改类型,该值也始终满足接口的要求。
接口的表示形式
接口类型的变量存储了一对值:分配给该变量的具体值,以及该值的类型描述。更确切地说,该值是实现接口的基础具体数据项,而类型描述了该数据项的完整类型。例如:
1 | var r io.Reader |
r
中包含了 (value, type)
对,即
(tty, *os.File)
。请注意,类型 *os.File
实现的方法不只有 Read
; 尽管接口仅提供对 Read
方法的访问,但是其内部的值仍包含有关该值的所有类型信息。这就是为什么我们可以做下面的事情:
1 | var w io.Writer |
因为 r
的具体类型里面包含了 Write
方法,而
r
里面包含的值依然持有它原来的值,所以这个断言是没有问题的。
一个重要的细节是,接口内始终保存
(值, 具体类型)
形式的元素对,而不会有(值, 接口类型)
的形式。接口内部不持有接口值。
反射
反射第一定律:从接口值反射出反射对象
反射对象主要有两类:reflect.Type
、reflect.Value
从底层讲,反射只是一种检查存储在接口变量中的值和类型对的机制。首先,我们需要了解反射包的两个类型:Type
和 Value
,
通过这两个类型可以访问接口变量的内容。还有两个函数
reflect.TypeOf
和
reflect.ValueOf
,它们可以从接口值中取出
reflect.Type
和 reflect.Value
。(另外,从
reflect.Value
可以很容易地获取到
reflect.Type
,但是让我们暂时将 Value
和
Type
的概念分开。)
1 | package main |
上面的代码看起来像将 float64
类型的变量 x
传递给了
reflect.TypeOf
,而不是传递的接口值。但实际上,传递的是接口;
1 | // TypeOf 返回 interface{} 中值的反射类型 |
当我们调用 reflect.TypeOf(x)
时,x
先被存在一个空接口中,然后再作为参数传递;reflect.TypeOf
从该空接口中恢复类型信息。
相应的,reflect.ValueOf
函数会恢复值信息。
1 | var x float64 = 3.4 |
reflect.Type
和 reflect.Value
都有许多方法可以让我们执行检查和操作: * Value
具有
Type
方法,该方法返回 reflect.Value
的
Type
类型。 * Type
和 Value
都有一个 Kind
方法,该方法返回 go
的类型(语言本身的类型,而不是自定义的类型) * Value
的很多方法,名字类似于 Int
和
Float64
,可以让我们获取存储在里面的值。 * 还有诸如
SetInt
和 SetFloat
之类的方法,可以修改接口的值。
反射第二定律:从反射对象到接口值
给定 reflect.Value
,我们可以使用
Interface()
方法恢复接口值;
实际上,该方法将类型和值信息打包回接口表示形式并返回结果:
1 | //接口返回v的值作为接口{}。 |
结果,我们可以说
1 | y := v.Interface().(float64) // y的类型为float64 |
打印反射对象 v
表示的 float64
值。一种更简洁的写法是:
1 | // fmt.Println 本身就接受 interface{} 参数 |
反射第三定律:要修改反射对象,该值必须可设置
不可设置的例子:
1 | var x float64 = 3.4 |
因为调用 reflect.ValueOf(x)
的时候,函数只拿到了
x
的副本,而不是 x
变量本身,如果我们在函数内部修改了 x
那也只是修改了副本而已。
Value
的 CanSet
方法报告 Value
的可设置性:
1 | var x float64 = 3.4 |
如果我们想修改它,可以在反射的时候,直接使用 x
的指针:
1 | var x float64 = 3.4 |
我们注意到,这里我们使用了指针,但依然是不能设置其值。这是因为反射对象
p
是不可设置的,实际上我们想要设置的不是
p
,而是 *p
。为了获取 p
指向的内容,我们调用 Value
值的 Elem
方法,该方法指向指针:
1 | v := p.Elem() |
现在,v
是一个可设置的反射对象了,我们可以使用
v.SetFloat
来修改 x
的值了:
1 | v.SetFloat(7.1) |
反射值需要变量的地址才能修改其表示的值。
结构体
在下面的例子中,我们使用结构体的地址创建反射对象,因为稍后将要对其进行修改。然后我们将
typeOfT
设置为其反射类型,
并使用简单的方法调用对字段进行迭代。
1 | type T struct { |
1 | 0: A int = 23 |
此处传递的内容还涉及可设置性的另一点:
T
的字段名是大写(已导出),因为只能设置结构体的导出字段。
因为 s
包含可设置的反射对象,所以我们可以修改结构的字段:
1 | s.Field(0).SetInt(77) |
如果我们修改代码从
t
而不是&t
创建s
,则对SeteInt
和SetString
的调用将失败,因为无法设置t
的字段。
结论
反射定律:
- 反射可以从接口值到反射对象
- 反射可以从反射对象到接口值
- 要修改反射对象,该值必须可设置。