摘要:今天我们来学习 Golang 中的 interface 类型。
Go 的 5 个关键点
interface 是一种类型
type Animal interface { SetName(string) GetName() string }
首先 interface 是一种类型,从它的定义中就可以看出用了 type 关键字,更准确的说 interface 是一种具有一组方法的类型,这些方法定义了 interface 的行为。Go 允许不带任何方法的 interface, 这种类型的 interface 叫 empty interface。如果一个类型实现了一个 interface 中所有的方法,我们说该类型实现了该 interface, 所以所有类型都实现了 empty interface, Go 没有显式的关键字用来实现 interface, 只需要实现 interface 包含的方法即可。
interface 变量存储的是实现者的值
package main import ( "fmt" ) type Animal interface { SetName(string) GetName() string } type Cat struct { Name string } func (c Cat) SetName(name string) { fmt.Println("c addr in: ", c) c.Name = name fmt.Println(c.GetName()) } func (c Cat) GetName() string { return c.Name } func main() { // c := &Cat{Name: "Cat"} // fmt.Println("c addr out: ", c) // c.SetName("DogCat") // fmt.Println(c.GetName()) c := Cat{} var i Animal i = &c //把变量赋值给一个 interface fmt.Println(i.GetName()) }
如果有多种类型实现了某个 interface, 这些类型的值都可以直接使用 interface 的变量存储。不难看出 interface 的变量中存储的是实现了 interface 的类型的对象值,这种能力是 duck typing。在使用 interface 时,不需要显式在 struct 上声明要实现哪个 interface, 只需要实现对应的 interface 中的方法即可,Go 会在运行时执行从其它类型到 interface 的自动转换。
如何判断 interface 变量存储的是哪种类型的值
当一个 interface 被多个类型实现时, 有时候我们需要区分 interface 的变量究竟存储的是哪种类型的值, Go 可以使用 comma, ok 的形式做区分 value, ok := em.(T) : em 是 interface 类型的变量, T 代表要断言的类型, value 是 interface 变量存储的值, ok 是 bool 类型标识是否为该断言的类型 T。
c := Cat{Name: "cat"}
var i Animal i = &c //把变量赋值给一个 interface if t, ok := i.(*Cat); ok { fmt.Println("c implement i:", t) }
//当然我们也可以用 switch 语句
switch t := i.(*Cat) {
case *S:
fmt.Println("i store *S", t)
case *R:
fmt.Println("i store *R", t)
}
空 interface
interface{} 是一个空的 interface 类型, 空的 interface 没有方法,所以可以认为所有的类型都实现了 interface{}。如果定义一个函数参数是 interface{} 类型, 这个函数应该可以接受任何类型作为它的参数。
func doSomething(v interface{}) { //do something }
注意:在函数内部 v 并不是任何类型,在函数参数传递的过程中,任何类型都被自动转换为 interface{}。 关于 Go 是如何转换的,可以参考这里。
另外:既然空的 interface 可以接受任何类型的参数,那么一个 interface{} 类型的 slice 是不是就可以接受任何类型的 slice?
func printAll(vals []interface{}) { //1 for _, val := range vals { fmt.Println(val) } } func main(){ names := []string{"stanley", "david", "oscar"} printAll(names) }
//err:cannot use names (type []string) as type []interface {} in argument to printAll
上述示例代码中,我们将 []string 转换为 []interface{}, 但是我们编译的时候报错,这说明 Go 并没有帮助我们自动把 slice 转换为 interface{} 类型的 slice, 所以出错了。为什么不帮我们自动转换,相关说明在这里查看。但是我们可以手动进行转换来达到我们的目的:
var dataSlice []int = foo() var interfaceSlice []interface{} = make([]interface{}, len(dataSlice)) for i, v := range dataSlice { interfaceSlice[i] = v }
选择 interface 的实现者
package main import ( "fmt" ) type Animal interface { SetName(string) GetName() string } type Cat struct { Name string } func (c Cat) SetName(name string) { fmt.Println("c addr in: ", c) c.Name = name fmt.Println(c.GetName()) } func (c Cat) GetName() string { return c.Name } func main() { c := &Cat{Name: "Cat"} //指针调用 //cc := Cat{Name: "Cat"} //值调用 fmt.Println("c addr out: ", c) c.SetName("DogCat") fmt.Println(c.GetName()) c := Cat{Name: "cat"} fmt.Println(i.GetName()) }
上面代码中,接受者是个 value receiver。但是 interface 定义时并没有严格规定实现者的方法 receiver 是个 value receiver 还是 pointer receiver。如果接收者是 value receiver, 那么在方法内对这个接收者所做的修改都不会影响到调用者,这和 C++ 中的 “值传递” 类似,例如:
package main import ( "fmt" ) type Animal interface { SetName(string) GetName() string } type Cat struct { Name string } func (c Cat) SetName(name string) { fmt.Println("c addr in: ", c) // c addr in: {Cat}, 内部会把指针对应的值取出来,进行值调用 c.Name = name fmt.Println(c.GetName()) //print DogCat } func (c Cat) GetName() string { return c.Name } func main() { c := &Cat{Name: "Cat"} //指针调用,but receiver is value receiver fmt.Println("c addr out: ", c) // c addr out: &{Cat} 外部还是指针类型的变量 c.SetName("DogCat") fmt.Println(c.GetName()) //print Cat }
注意:如果 receiver 是 pointer receiver, 通过 value 进行调用,则会编译保持,提示该类型没有实现这个 interface, 这可以理解为:如果是 pointer 调用,go 会自动进行转换,因为有了指针总能得到指针指向的值是什么,如果是 value, go 将无法得知 value 的原始值是什么,因为 value 仅仅是份拷贝。go 会把指针进行隐式转换得到 value, 但反过来不行。
Go interface 的底层实现
interface 底层结构
func foo(x interface{}) { if x == nil { fmt.Println("empty interface") return } fmt.Println("non-empty interface") } func main() { var x *int = nil foo(x) //print non-empty interface }
通过上述的代码,我们或许有些疑惑,那就带着疑惑往下看吧,了解 go 是怎么把一种类型转换为 interface 类型的。
首先,根据 interface 是否包含 method, 底层实现上用了两种不同的 struct 来表示:iface 和 eface。eface 表示不含 method 的 interface 结构,或者叫 empty interface。对于 Golang 中的大部分数据类型都可以抽象成 _type 结构,同时针对不同的类型还会有一些其他信息。
type eface struct { _type *_type //接口指向的数据类型 data unsafe.Pointer //接口指向的数据的值 } type _type struct { size uintptr // type size ptrdata uintptr // size of memory prefix holding all pointers hash uint32 // hash of type tflag tflag align uint8 fieldAlign uint8 kind uint8 // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? equal func(unsafe.Pointer, unsafe.Pointer) bool // gcdata stores the GC type data for the garbage collector. // If the KindGCProg bit is set in kind, gcdata is a GC program. // Otherwise it is a ptrmask bitmap. See mbitmap.go for details. gcdata *byte str nameOff ptrToThis typeOff }
iface 表示 non-empty interface 的底层实现。包含一些 method。method 的具体实现存放在 itab.fun 变量里,如果 interface 包含多个 method, 这里只有一个 fun 变量怎么存呢?等会根据具体的例子来说明这个问题。我们先来看一下 iface 这个结构体:
type iface struct { tab *itab //包含函数的声明和具体实现 data unsafe.Pointer //指向数据的指针 } type itab struct { inter *interfacetype //包含函数的声明 _type *_type hash uint32 // copy of _type.hash. Used for type switches. _ [4]byte fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter. //函数的具体实现,如果 fun[0] == 0, 移位着没有实现 interfacettpe 中声明的函数 } type interfacetype struct { typ _type pkgpath name mhdr []imethod //包含函数的声明 }
Q1: 一个 [1]uintptr 如何存多个 method ? [此处完全来自文末参考资料]
A1: 我们通过汇编代码来看一下:
package main type MyInterface interface { Print() Hello() World() AWK() } func Foo(me MyInterface) { me.Print() me.Hello() me.World() me.AWK() } type MyStruct struct {} func (me MyStruct) Print() {} func (me MyStruct) Hello() {} func (me MyStruct) World() {} func (me MyStruct) AWK() {} func main() { var me MyStruct Foo(me) }
通过对其反汇编可以得到:
$ go build -gcflags '-l' -o main main.go $ go tool objdump -s main TEXT main.Foo(SB) TEXT main.Foo(SB) C:/Users/sweenzhang/learnGo/main.go interface8.go:10 0x104c060 65488b0c25a0080000 GS MOVQ GS:0x8a0, CX interface8.go:10 0x104c069 483b6110 CMPQ 0x10(CX), SP interface8.go:10 0x104c06d 7668 JBE 0x104c0d7 interface8.go:10 0x104c06f 4883ec10 SUBQ $0x10, SP interface8.go:10 0x104c073 48896c2408 MOVQ BP, 0x8(SP) interface8.go:10 0x104c078 488d6c2408 LEAQ 0x8(SP), BP interface8.go:11 0x104c07d 488b442418 MOVQ 0x18(SP), AX interface8.go:11 0x104c082 488b4830 MOVQ 0x30(AX), CX //取得 Print 函数地址 interface8.go:11 0x104c086 488b542420 MOVQ 0x20(SP), DX interface8.go:11 0x104c08b 48891424 MOVQ DX, 0(SP) interface8.go:11 0x104c08f ffd1 CALL CX // 调用 Print() interface8.go:12 0x104c091 488b442418 MOVQ 0x18(SP), AX interface8.go:12 0x104c096 488b4828 MOVQ 0x28(AX), CX //取得 Hello 函数地址 interface8.go:12 0x104c09a 488b542420 MOVQ 0x20(SP), DX interface8.go:12 0x104c09f 48891424 MOVQ DX, 0(SP) interface8.go:12 0x104c0a3 ffd1 CALL CX //调用 Hello() interface8.go:13 0x104c0a5 488b442418 MOVQ 0x18(SP), AX interface8.go:13 0x104c0aa 488b4838 MOVQ 0x38(AX), CX //取得 World 函数地址 interface8.go:13 0x104c0ae 488b542420 MOVQ 0x20(SP), DX interface8.go:13 0x104c0b3 48891424 MOVQ DX, 0(SP) interface8.go:13 0x104c0b7 ffd1 CALL CX //调用 World() interface8.go:14 0x104c0b9 488b442418 MOVQ 0x18(SP), AX interface8.go:14 0x104c0be 488b4020 MOVQ 0x20(AX), AX //取得 AWK 函数地址 interface8.go:14 0x104c0c2 488b4c2420 MOVQ 0x20(SP), CX interface8.go:14 0x104c0c7 48890c24 MOVQ CX, 0(SP) interface8.go:14 0x104c0cb ffd0 CALL AX //调用 AWK() interface8.go:15 0x104c0cd 488b6c2408 MOVQ 0x8(SP), BP interface8.go:15 0x104c0d2 4883c410 ADDQ $0x10, SP interface8.go:15 0x104c0d6 c3 RET interface8.go:10 0x104c0d7 e8f48bffff CALL runtime.morestack_noctxt(SB) interface8.go:10 0x104c0dc eb82 JMP main.Foo(SB)
其中 0x18(SP) 对应的 itab 的值。fun 在 x86-64 机器上对应 itab 内的地址偏移为 8+8+8+4+4 = 32 = 0x20,也就是 0x20(AX) 对应的 fun 的值,此时存放的 AWK 函数地址。然后 0x28(AX) = &Hello,0x30(AX) = &Print,0x38(AX) = &World。对的,每次函数是按字典序排序存放的。
我们再来看一下函数地址究竟是怎么写入的?首先 Golang 中的 uintptr 一般用来存放指针的值,这里对应的就是函数指针的值(也就是函数的调用地址)。但是这里的 fun 是一个长度为 1 的 uintptr 数组。我们看一下 runtime 包的 additab 函数
func additab(m *itab, locked, canfail bool) { ... *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn ... }
上面的代码的意思是在 fun[0] 的地址后面依次写入其他 method 对应的函数指针。熟悉 C++ 的同学可以类比 C++ 的虚函数表指针来看。
Type Assertion(类型断言)
我们知道使用 interface type assertion (中文一般叫断言) 的时候需要注意,不然很容易引入 panic。
func do(v interface{}) { n := v.(int) // might panic } func do(v interface{}) { n, ok := v.(int) if !ok { // 断言失败处理 } }
这个过程体现在下面的几个函数上:
// The assertXXX functions may fail (either panicking or returning false, // depending on whether they are 1-result or 2-result). func assertI2I(inter *interfacetype, i iface) (r iface) { tab := i.tab if tab == nil { // explicit conversions require non-nil interface value. panic(&TypeAssertionError{"", "", inter.typ.string(), ""}) } if tab.inter == inter { r.tab = tab r.data = i.data return } r.tab = getitab(inter, tab._type, false) r.data = i.data return } func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) { tab := i.tab if tab == nil { return } if tab.inter != inter { tab = getitab(inter, tab._type, true) if tab == nil { return } } r.tab = tab r.data = i.data b = true return } // 类似 func assertE2I(inter *interfacetype, e eface) (r iface) func assertE2I2(inter *interfacetype, e eface) (r iface, b bool)
总结
从某种意义上来说,Golang 的 interface 也是一种多态的体现。对比其他支持多态特性的语言,实现还是略有差异,很难说谁好谁坏
参考资料
https://research.swtch.com/interfaces
http://legendtkl.com/2017/07/01/golang-interface-implement/
https://sanyuesha.com/2017/07/22/how-to-understand-go-interface/