interface 底层结构
iface 和 eface 都是 Go 中描述接口的底层结构体,区别在于 iface 描述的接口包含方法,而 eface 则是不包含任何方法的空接口:interface{}
从源码层面看一下:
type iface struct {tab *itabdata unsafe.Pointer}type itab struct {inter *interfacetype_type *_typelink *itabhash uint32 // copy of _type.hash. Used for type switches.bad bool // type does not implement interfaceinhash bool // has this itab been added to hash?unused [2]bytefun [1]uintptr // variable sized}
iface 内部维护两个指针,tab 指向一个 itab 实体, 它表示接口的类型以及赋给这个接口的实体类型。data 则指向接口具体的值,一般而言是一个指向堆内存的指针。
再来仔细看一下 itab 结构体:_type 字段描述了实体的类型,包括内存对齐方式,大小等;inter 字段则描述了接口的类型。fun 字段放置和接口方法对应的具体数据类型的方法地址,实现接口调用方法的动态分派,一般在每次给接口赋值发生转换时会更新此表,或者直接拿缓存的 itab。
另外,你可能会觉得奇怪,为什么 fun 数组的大小为 1,要是接口定义了多个方法可怎么办?实际上,这里存储的是第一个方法的函数指针,如果有更多的方法,在它之后的内存空间里继续存储。从汇编角度来看,通过增加地址就能获取到这些函数指针,没什么影响。顺便提一句,这些方法是按照函数名称的字典序进行排列的。
再看一下 interfacetype 类型,它描述的是接口的类型:
type interfacetype struct {typ _typepkgpath namemhdr []imethod}
可以看到,它包装了 _type 类型,_type 实际上是描述 Go 语言中各种数据类型的结构体。我们注意到,这里还包含一个 mhdr 字段,表示接口所定义的函数列表, pkgpath 记录定义了接口的包名。
这里通过一张图来看下 iface 结构体的全貌:
接着来看一下 eface 的源码:
type eface struct {_type *_typedata unsafe.Pointer}
相比 iface,eface 就比较简单了。只维护了一个 _type 字段,表示空接口所承载的具体的实体类型。data 描述了具体的值。
我们最后再来看下 _type 结构体:
type _type struct {// 类型大小size uintptrptrdata uintptr// 类型的 hash 值hash uint32// 类型的 flag,和反射相关tflag tflag// 内存对齐相关align uint8fieldalign uint8// 类型的编号,有bool, slice, struct 等等等等kind uint8alg *typeAlg// gc 相关gcdata *bytestr nameOffptrToThis typeOff}
Go 语言各种数据类型都是在 _type 字段的基础上,增加一些额外的字段来进行管理的:
type arraytype struct {typ _typeelem *_typeslice *_typelen uintptr}type chantype struct {typ _typeelem *_typedir uintptr}type slicetype struct {typ _typeelem *_type}type structtype struct {typ _typepkgPath namefields []structfield}
这些数据类型的结构体定义,是反射实现的基础。
接口的动态类型和动态值
从源码里可以看到:iface包含两个字段:tab 是接口表指针,指向类型信息;data 是数据指针,则指向具体的数据。它们分别被称为动态类型和动态值。而接口值包括动态类型和动态值。
【引申1】接口类型和 nil 作比较
接口值的零值是指动态类型和动态值都为 nil。当仅且当这两部分的值都为 nil 的情况下,这个接口值就才会被认为 接口值 == nil。
package mainimport "fmt"type Coder interface {code()}type Gopher struct {name string}func (g Gopher) code() {fmt.Printf("%s is coding\n", g.name)}func main() {var c Coderfmt.Println(c == nil)fmt.Printf("c: %T, %v\n", c, c)var g *Gopherfmt.Println(g == nil)c = gfmt.Println(c == nil)fmt.Printf("c: %T, %v\n", c, c)}
输出:
truec: <nil>, <nil>truefalsec: *main.Gopher, <nil>
一开始,c 的 动态类型和动态值都为 nil,g 也为 nil,当把 g 赋值给 c 后,c 的动态类型变成了 *main.Gopher,仅管 c 的动态值仍为 nil,但是当 c 和 nil 作比较的时候,结果就是 false 了。
【引申2】 来看一个例子,看一下它的输出:
package mainimport "fmt"type MyError struct {}func (i MyError) Error() string {return "MyError"}func main() {err := Process()fmt.Println(err)fmt.Println(err == nil)fmt.Println(reflect.ValueOf(req).IsNil())}func Process() error {var err *MyError = nilreturn err}
函数运行结果:
<nil>falsetrue
这里先定义了一个 MyError 结构体,实现了 Error 函数,也就实现了 error 接口。Process 函数返回了一个 error 接口,这块隐含了类型转换。所以,虽然它的值是 nil,其实它的类型是 *MyError,最后和 nil 比较的时候,结果为 false。
【引申3】如何打印出接口的动态类型和值?
package mainimport ("unsafe""fmt")type iface struct {itab, data uintptr}func main() {var a interface{} = nilvar b interface{} = (*int)(nil)x := 5var c interface{} = (*int)(&x)ia := *(*iface)(unsafe.Pointer(&a))ib := *(*iface)(unsafe.Pointer(&b))ic := *(*iface)(unsafe.Pointer(&c))fmt.Println(ia, ib, ic)fmt.Println(*(*int)(unsafe.Pointer(ic.data)))}
代码里直接定义了一个 iface 结构体,用两个指针来描述 itab 和 data,之后将 a, b, c 在内存中的内容强制解释成我们自定义的 iface。最后就可以打印出动态类型和动态值的地址。
{0 0} {17426912 0} {17426912 842350714568}5
a 的动态类型和动态值的地址均为 0,也就是 nil;b 的动态类型和 c 的动态类型一致,都是 *int;最后,c 的动态值为 5。
编译器自动检测类型是否实现接口
经常看到一些开源库里会有一些类似下面这种奇怪的用法:
var _ io.Writer = (*myWriter)(nil)
这时候会有点懵,不知道作者想要干什么,实际上这就是此问题的答案。编译器会由此检查 *myWriter 类型是否实现了 io.Writer 接口。
package mainimport "io"type myWriter struct {}/*func (w myWriter) Write(p []byte) (n int, err error) {return}*/func main() {// 检查 *myWriter 类型是否实现了 io.Writer 接口var _ io.Writer = (*myWriter)(nil)// 检查 myWriter 类型是否实现了 io.Writer 接口var _ io.Writer = myWriter{}}
注释掉为 myWriter 定义的 Write 函数后,运行程序:
src/main.go:14:6: cannot use (*myWriter)(nil) (type *myWriter) as type io.Writer in assignment:*myWriter does not implement io.Writer (missing Write method)src/main.go:15:6: cannot use myWriter literal (type myWriter) as type io.Writer in assignment:myWriter does not implement io.Writer (missing Write method)
报错信息:*myWriter/myWriter 未实现 io.Writer 接口,也就是未实现 Write 方法。
解除注释后,运行程序不报错。
实际上,上述赋值语句会发生隐式地类型转换,在转换的过程中,编译器会检测等号右边的类型是否实现了等号左边接口所规定的函数。
总结一下,可通过在代码中添加类似如下的代码,用来检测类型是否实现了接口:
var _ io.Writer = (*myWriter)(nil)var _ io.Writer = myWriter{}
类型转换和断言的区别
我们知道,Go 语言中不允许隐式类型转换,也就是说 = 两边,不允许出现类型不相同的变量。类型转换、类型断言本质都是把一个类型转换成另外一个类型。不同之处在于,类型断言是对接口变量进行的操作。
类型转换
对于类型转换而言,转换前后的两个类型要相互兼容才行。类型转换的语法为:
<结果类型> := <目标类型> ( <表达式> )
package mainimport "fmt"func main() {var i int = 9var f float64f = float64(i)fmt.Printf("%T, %v\n", f, f)f = 10.8a := int(f)fmt.Printf("%T, %v\n", a, a)// s := []int(i)}
上面的代码里,我定义了一个 int 型和 float64 型的变量,尝试在它们之前相互转换,结果是成功的:int 型和 float64 是相互兼容的。
把最后一行代码的注释去掉,编译器会报告类型不兼容的错误:
断言**
前面说过,因为空接口 interface{} 没有定义任何函数,因此 Go 中所有类型都实现了空接口。当一个函数的形参是 interface{},那么在函数中,需要对形参进行断言,从而得到它的真实类型。
断言的语法为:
<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 ) // 安全类型断言
<目标类型的值> := <表达式>.( 目标类型 ) //非安全类型断言
类型转换和类型断言有些相似,不同之处,在于类型断言是对接口进行的操作。
package mainimport "fmt"type Student struct {Name stringAge int}func main() {var i interface{} = new(Student)s := i.(Student)fmt.Println(s)}
运行一下:
panic: interface conversion: interface {} is *main.Student, not main.Student
直接 panic 了,这是因为 i 是 *Student 类型,并非 Student 类型,断言失败。这里直接发生了 panic,线上代码可能并不适合这样做,可以采用“安全断言”的语法:
func main() {var i interface{} = new(Student)s, ok := i.(Student)if ok {fmt.Println(s)}}
这样,即使断言失败也不会 panic。
断言其实还有另一种形式,就是用在利用 switch 语句判断接口的类型。每一个 case 会被顺序地考虑。当命中一个 case 时,就会执行 case 中的语句,因此 case 语句的顺序是很重要的,因为很有可能会有多个 case 匹配的情况。
代码示例如下:
func main() {//var i interface{} = new(Student)//var i interface{} = (*Student)(nil)var i interface{}fmt.Printf("%p %v\n", &i, i)judge(i)}func judge(v interface{}) {fmt.Printf("%p %v\n", &v, v)switch v := v.(type) {case nil:fmt.Printf("%p %v\n", &v, v)fmt.Printf("nil type[%T] %v\n", v, v)case Student:fmt.Printf("%p %v\n", &v, v)fmt.Printf("Student type[%T] %v\n", v, v)case *Student:fmt.Printf("%p %v\n", &v, v)fmt.Printf("*Student type[%T] %v\n", v, v)default:fmt.Printf("%p %v\n", &v, v)fmt.Printf("unknow\n")}}type Student struct {Name stringAge int}
对于第一行语句:var i interface{} = new(Student)i 是一个 *Student 类型,匹配上第三个 case,从打印的三个地址来看,这三处的变量实际上都是不一样的。在 main 函数里有一个局部变量 i;调用函数时,实际上是复制了一份参数,因此函数里又有一个变量 v,它是 i 的拷贝;断言之后,又生成了一份新的拷贝。所以最终打印的三个变量的地址都不一样。结果为:
0xc4200701b0 [Name: ], [Age: 0]0xc4200701d0 [Name: ], [Age: 0]0xc420080020 [Name: ], [Age: 0]*Student type[*main.Student] [Name: ], [Age: 0]
对于第二行语句:var i interface{} = (Student)(nil)
这里想说明的其实是 i 在这里动态类型是 `(Student), 数据为nil,它的类型并不是nil,它与nil作比较的时候,得到的结果也是false`。结果为:
0xc42000e1d0 <nil>0xc42000e1f0 <nil>0xc42000c030 <nil>*Student type[*main.Student] <nil>
最后一行语句:var i interface{},这回 i 是 nil 类型。结果为:
0xc42000e1d0 <nil>0xc42000e1e0 <nil>0xc42000e1f0 <nil>nil type[<nil>] <nil>
接口转换的原理
通过前面提到的 iface 的源码可以看到,实际上它包含接口的类型 interfacetype 和 实体类型的类型 _type,这两者都是 iface 的字段 itab 的成员。也就是说生成一个 itab 同时需要接口的类型和实体的类型。
当判定一种类型是否满足某个接口时,Go 使用类型的方法集和接口所需要的方法集进行匹配,如果类型的方法集完全包含接口的方法集,则可认为该类型实现了该接口。
例如某类型有 m 个方法,某接口有 n 个方法,则很容易知道这种判定的时间复杂度为 O(mn),Go 会对方 法集的函数按照函数名的字典序进行排序,所以实际的时间复杂度为 O(m+n)。
这里我们来探索将一个接口转换给另外一个接口背后的原理,当然,能转换的原因必然是类型兼容。
直接来看一个例子:
package mainimport "fmt"type coder interface {code()run()}type runner interface {run()}type Gopher struct {language string}func (g Gopher) code() {return}func (g Gopher) run() {return}func main() {var c coder = Gopher{}var r runnerr = cfmt.Println(c, r)}
简单解释下上述代码:定义了两个 interface: coder 和 runner。定义了一个实体类型 Gopher,类型 Gopher 实现了两个方法,分别是 run() 和 code()。main 函数里定义了一个接口变量 c,绑定了一个 Gopher 对象,之后将 c 赋值给另外一个接口变量 r 。赋值成功的原因是 c 中包含 run() 方法。这样,两个接口变量完成了转换。
值接收者和指针接收者的区别
方法
方法能给用户自定义的类型添加新的行为。它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法。接收者可以是值接收者,也可以是指针接收者。
在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法。内部会转换为具体类型的接收者
也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。
package mainimport "fmt"type Person struct {age int}func (p Person) howOld() int {return p.age}func (p *Person) growUp() {p.age += 1}func main() {// qcrao 是值类型qcrao := Person{age: 18}// 值类型 调用接收者也是值类型的方法fmt.Println(qcrao.howOld())// 值类型 调用接收者是指针类型的方法qcrao.growUp()fmt.Println(qcrao.howOld())// ----------------------// stefno 是指针类型stefno := &Person{age: 100}// 指针类型 调用接收者是值类型的方法fmt.Println(stefno.howOld())// 指针类型 调用接收者也是指针类型的方法stefno.growUp()fmt.Println(stefno.howOld())}
上例子的输出结果是:
18
19
100
101
调用了 growUp 函数后,不管调用者是值类型还是指针类型,它的 Age 值都改变了。
实际上,当类型和方法的接收者类型不同时,其实是编译器在背后做了一些工作
值接收者和指针接收者
前面说过,不管接收者类型是值类型还是指针类型,都可以通过值类型或指针类型调用,这里面实际上通过语法糖起作用的。
先说结论:实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。
来看一个例子,就会完全明白:
package main
import "fmt"
type coder interface {
code()
debug()
}
type Gopher struct {
language string
}
func (p Gopher) code() {
fmt.Printf("I am coding %s language\n", p.language)
}
func (p *Gopher) debug() {
fmt.Printf("I am debuging %s language\n", p.language)
}
func main() {
var c coder = &Gopher{"Go"}
c.code()
c.debug()
}
上述代码里定义了一个接口 coder,接口定义了两个函数:
code()
debug()
接着定义了一个结构体 Gopher,它实现了两个方法,一个值接收者,一个指针接收者。
最后,我们在 main 函数里通过接口类型的变量调用了定义的两个函数。
运行一下,结果:
I am coding Go language
I am debuging Go language
但是如果我们把 main 函数的第一条语句换一下:
func main() {
var c coder = Gopher{"Go"}
c.code()
c.debug()
}
运行一下,报错:
./main.go:23:6: cannot use Gopher literal (type Gopher) as type coder in assignment:
Programmer does not implement coder (debug method has pointer receive
看出这两处代码的差别了吗?第一次是将 &Gopher 赋给了 coder;第二次则是将 Gopher 赋给了 coder。
第二次报错是说,Gopher 没有实现 coder。很明显了吧,因为 Gopher 类型并没有实现 debug 方法;表面上看, *Gopher 类型也没有实现 code 方法,但是因为 Gopher 类型实现了 code 方法,所以让 *Gopher 类型自动拥有了 code 方法。
最后,只要记住下面这点就可以了:如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。
转自:https://zhuanlan.zhihu.com/p/63649977
