Go interface深入分析
1.鸭子类型(Duck Typing)
- If it walks like a duck and it quacks like a duck, then it must be a duck.
- interface是一种鸭子类型
- 无需显示声明,只要对象实现了接口声明的的全部方法,就实现了该接口
- 把对象的类型检查从编译时推迟到运行时
- 好处:
- 松耦合
- 可以先实现类型,再抽象接口
2.值receiver VS. 指针receiver
type T struct {}
func (t T) Value() {} //value receiver
func (t *T) Pointer() {} //pointer receiver
- 值receiver会复制对象实例,而指针receiver不会
- 把方法看作普通函数,receiver可以理解为传入的第一个参数
- 只要receiver参数类型正确,方法就可以被执行
思考题:下面哪些语句在运行时会报错?
func main() {
var p *T
p.Pointer()
(*T)(nil).Pointer()
(*T).Pointer(nil)
p.Value()
}
另外,map中的元素是不可寻址的(not addressable),简单来说就是不能取指针。所以如果map中存储struct元素的话,大部分情况都是以指针类型定义的。
func main() {
m := make(map[string]T, 0)
m["a"] = T{}
m["a"].Value() // GOOD
m["a"].Pointer() // BAD,编译错误
}
-----------------------------------------
func main() {
m := make(map[string]*T, 0)
m["a"] = T{}
m["a"].Value() // GOOD
m["a"].Pointer() // GOOD
}
3.方法集
- 类型有一个与之相关的方法集,决定了它是否实现某个接口
- 类型T的方法集包含所有receiver T的方法
- 类型*T的方法集包含所有receiver T + *T的方法
可以通过反射进行验证:
func printMethodSet(obj interface{}) {
t := reflect.TypeOf(obj)
for i, n := 0, t.NumMethod(); i < n; i++ {
m := t.Method(i)
fmt.Println(t, m.Name, m.Type)
}
}
func main() {
var t T
printMethodSet(t)
fmt.Println("----------------")
printMethodSet(&t)
}
输出结果:
main.T Value func(main.T)
----------------
*main.T Pointer func(*main.T)
*main.T Value func(*main.T)
可以看到,*T类型包含了receiver T + *T的方法。但是,似乎Value()方法的receiver被改变了?
敲黑板:方法集仅仅用来验证接口实现,对象或对象指针会直接调用原实现,不会使用方法集
思考题:下面程序的输出是什么?
type T struct {
x int
}
func (t T) Value() { //value receiver
t.x++
}
func (t *T) Pointer() { //pointer receiver
t.x++ //Go没有->运算符,编译器会自动把t转成(*t)
}
func main() {
var t *T = &T{1}
t.Value()
fmt.Println(t.x)
t.Pointer()
fmt.Println(t.x)
}
4.什么是interface?
先看一下Go语言的实现,代码位于runtime/runtime2.go:
type iface struct {
tab *itab //类型信息
data unsafe.Pointer //实际对象指针
}
type itab struct {
inter *interfacetype //接口类型
_type *_type //实际对象类型
hash uint32
_ [4]byte
fun [1]uintptr //实际对象方法地址
}
可以看到,interface其实就是两个指针,一个指向类型信息,一个指向实际的对象。
对象方法查找的两大阵营:
- 静态类型语言:如C++/Java,在编译时生成完整的方法表
- 动态类型语言:如Python/Javascript,在每次调用方法时进行查找(会使用cache)
Go采取了一种独有(折衷)的实现方式:
- 在进行类型转换时计算itab,查找具体实现
- itab类型只和interface相关,也就是说只包含接口声明的方法的具体实现(没有多余方法)
举例:
1 type I interface {
2 hello()
3 }
4
5 type S struct {
6 x int
7 }
8 func (S) hello() {}
9
10 func main() {
11 s := S{1}
12 var iter I = s
13 for i := 0; i < 100; i++ {
14 iter.hello()
15 }
16 }
Go会在第12行完成itable的计算,然后在第14行直接跳转。而在Python中则要到第14行才进行方法查找,虽然有cache的存在,仍然比直接一条跳转指令低效得多。
5.interface赋值
- 将对象赋值给接口变量时,会复制该对象
- 把指针赋值给接口变量则不会发生复制操作
可以用gdb查看接口内部数据。先用下面的命令阻止编译器优化:
go build -gcflags “-N -l”
从下面的例子可以看出,s的地址和i.data不同,发生了对象复制:
type I interface {
hello()
}
type S struct {
x int
}
func (S) hello() {}
func main() {
s := S{100}
var i I = s
i.hello()
}
================= gdb调试信息 =======================
(gdb) i locals
i = {tab = 0x1071dc0 <S,main.I>, data = 0xc420012098}
s = {x = 100}
(gdb) p/x &s
$1 = 0xc420041f58
而下面这个例子中是指针赋值,因此s的地址和i.data是相同的。
type I interface {
hello()
}
type S struct {
x int
}
func (*S) hello() {}
func main() {
s := S{100}
var i I = &s
i.hello()
}
================= gdb调试信息 =======================
(gdb) i locals
&s = 0xc420076000
i = {tab = 0x1071cc0 <S,main.I>, data = 0xc420076000}
6.interface何时等于nil?
- 只有当接口变量中的itab和data指针都为nil时,接口才等于nil
常见错误:
type MyError struct{}
func (*MyError) Error() string {
return "myerror"
}
func isPositive(x int) (int, error) {
var err *MyError
if (x <= 0) {
err = new(MyError)
return -x, err
}
return x, err //注意,err是有类型的!
}
func main() {
_, err := isPositive(100)
if err != nil {
fmt.Println("ERROR!")
}
}
可以看到,isPositive()函数返回err时相当于进行了一次类型转换,把*MyError对象转换为一个error接口。这个接口变量的data指针为nil,但itab指针不为空,指向MyError类型。
正确做法:直接返回nil即可
7.空接口interface{}
- interface{}可以接受任意类型,会自动进行转换(类似于Java中的Object)
- 例外:接口切片[]interface{}不会自动进行类型转换
看下面的例子:
func print(names []interface{}) {
for _, n := range names {
fmt.Println(n)
}
}
func main() {
names := []string {"star", "jivin", "sheng"}
print(names)
}
编译后会报以下错误:
cannot use names (type []string) as type []interface {} in argument to print
原因解释:[]interface{}在编译时就有确定的内存布局,每个元素的大小是固定的(2个指针),而[]string的内存布局显然不同。至于为什么Go为什么不帮我们做这个转换,个人猜测可能是因为转换的开销比较大。
解决方案1: 使用interface{}代替[]interface{}作为参数
func print(names interface{}) {
ns := names.([]string)
for _, n := range ns {
fmt.Println(n)
}
}
解决方案2:手动做一次类型转换
func main() {
inames := make([]interface{}, len(names))
for i, n := range names {
inames[i] = n
}
print(inames)
}
参考:
https://github.com/golang/go/wiki/MethodSets
https://research.swtch.com/interfaces
https://github.com/golang/go/wiki/InterfaceSlice
更多文章欢迎关注“鑫鑫点灯”专栏:https://blog.****.net/turkeycock
或关注飞久微信公众号: