Loading... # 0x01 <div class="tip inlineBlock info"> 本篇分析基于 **Go 1.14.12** 注意信息时效 </div> Interface 是 Go 里面非常有趣的一个概念,很多学习过其他面向对象语言的同学可能会下意识的觉得他是类似 `object 对象` 的东西,毕竟语法上所有类型好像都可以用 `interface{}` 来代替。但是 Go 又不是一门面向对象的语言,那这个 Interface 到底是啥玩意呢?为什么要这么设计?有什么用呢? # 鸭子类型 解释为什么要有 Interface 之前,我们先来聊聊鸭子类型。鸭子类型想必大家都听说过: > If it walks like a duck and it quacks like a duck, then it must be a duck. > > 如果一个东西走起来像鸭子、叫起来像鸭子,那么他就是鸭子 我们用一段 Python 代码来具体说明,如果你没有学过 Python 可以直接把下述代码当做伪代码来看。 ```python class Duck: def swim(self): print("Duck swimming") def fly(self): print("Duck flying") class Whale: def swim(self): print("Whale swimming") for animal in (Duck(), Whale()): animal.swim() animal.fly() # 结果如下: # Duck swimming # Duck flying # Whale swimming # AttributeError: 'Whale' object has no attribute 'fly' ``` 我们定义了一个 鸭子`Duck`类型 和 鲸鱼`Whale`类型,鸭子类型中实现了 游泳`swim`方法 和 飞行`fly`方法,鲸鱼类型实现了 游泳`swim`方法。然后通过这两个类型生成了两个 动物`animal`对象,分别去调用这个对象的`游泳`和`飞行`方法,其中`鲸鱼类型`生成的对象没有`飞行`方法,所以它的类型不是鸭子类型。 使用鸭子类型,可以在不使用`继承`的情况下实现`多态`,可以让代码更加优雅。但在 Python 这种动态语言中,确定一个对象是否实现了一个方法,是在运行中确定的,如果没有实现就会抛出一个异常,就比如上面 `Whale().fly()` 。如果想要在对象创建时检查有没有实现必须的方法,就需要修改 `元类(metaclass)` 对象的创建行为之类的东西了,这反而让代码更加晦涩难懂。 然而,Go 就使用 Interface 很好的解决了这个问题。 ```go type Duck interface { fly() swim() } type Goose struct {} type Whale struct {} func (g Goose) fly() { fmt.Println("I'm Goose i can fly!") } func (g Goose) swim() { fmt.Println("I'm Goose i can swim!") } func (w Whale) swim() { fmt.Println("I'm whale i can swim!") } func main() { var g Duck = Goose{} g.fly() g.swim() var w Duck = Whale{} w.fly() w.swim() } // 编译报错如下: // .\main.go:82:9: cannot use Whale literal (type Whale) as type Duck in assignment: // Whale does not implement Duck (missing fly method) ``` # iface 和 eface 那么 Go 中 Interface 是如何以静态语言之身实现动态语言的特性的呢?那就让我们来看看 Interface 的源码吧~ 首先我们要明确一个知识点:interface 是一个类型,一种抽象的类型。而一种类型的实现,必然会有他的结构体,而 interface 的结构体就是 `eface` 和 `iface`,编译阶段会根据 interface 不同的行为去决定使用哪个结构体。 - 使用 [runtime.iface](https://github.com/golang/go/blob/6c64b6db6802818dd9a4789cdd564f19b70b6b4c/src/runtime/runtime2.go#L203) 结构体表示包含方法的接口 - 使用 [runtime.eface](https://github.com/golang/go/blob/6c64b6db6802818dd9a4789cdd564f19b70b6b4c/src/runtime/runtime2.go#L208) 结构体表示不包含任何方法的 interface{} 类型; ```go type iface struct { tab *itab data unsafe.Pointer } type eface struct { _type *_type 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. } type interfacetype struct { typ _type pkgpath name mhdr []imethod } type _type struct { size uintptr ptrdata uintptr // size of memory prefix holding all pointers hash uint32 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 } ``` 他们之间的关系如图所示  ## eface 使用 `eface` 的情况就是我们前文提到的,被很多同学误认为 Go 语言中的 Interface 是其他语言的 object 的情况了。他就是一个空的接口,即 `interface{}`。`eface`结构体由 `_type` 和 `data` 两个指针组成,`data`指向存储具体对象的值,而`_type`是类型的元信息,包含类型的大小、哈希、对齐以及种类等。讲对象转换为 `interface{}` 之前,我们先来看这个例子。 ```go var a int64 = 100 var b float64 b = float64(a) fmt.Println(a, b) // 100 100 fmt.Printf("%T %T", a, b) // int64 float64 ``` 这是一个非常简单的类型转换例子,我们可以轻易的得知其作用就是把 a 的数据转换类型并赋值给了 b。那么再看下面这个例子也很好理解了。能够类型转换的必要条件是 **转换前后的两个类型要相互兼容** 。 ```go var a int64 = 100 var b interface{} b = a fmt.Println(a, b) // 100 100 fmt.Printf("%T %T", a, b) // int64 int64 ``` 而空的 `interface{}` 的结构体为 `eface` ,而所有类型均实现了 `eface` 。所以,任意一个包含 `类型` 和 `数据` 的对象都可以转化为空的接口,即 Go 语言中任意类型都可以转化为 `interface{}`。 ### 扩展 `fmt` 系的函数形参均为 `interface{}` 类型,所以可以输出所有类型。 ```go func Println(a ...interface{}) (n int, err error) { return Fprintln(os.Stdout, a...) } ``` #### 延伸阅读:`fmt.Println`是如何实现的 `fmt.Println`接收的是多个 `interface{}` ,即 ```go func Println(a ...interface{}) (n int, err error) { return Fprintln(os.Stdout, a...) } ``` 而在将传入的对象转化为字符串时,会先查找对象是否实现了 `String()`方法,如果实现了会直接调用,如果没有,则会使用 `反射(reflect)` 对结构体内每一个字段深度优先遍历并将其转化为字符串。 所以如果有将自定义结构体转化为字符串的需求,最好还是自定义一个 `String()` 方法,可以提升一点性能。 ```go type User struct { username string } func (u User) String() string { return fmt.Sprintf("自定义输出:{%s}", u.username) } func main() { user := User{"ewdager"} fmt.Println(user) } // 输出入下: // 自定义输出:{ewdager} ``` ## iface `iface` 与 `eface` 的主要区别在于 `iface` 描述的接口包含方法,而 `eface` 则是不包含任何方法的空接口。我们把两个结构体拆开来看,`iface` 比 `eface` 主要多了 `inter` 、`fun` 字段,`inter` 字段描述了接口的类型。`fun` 字段放置和接口方法对应的具体数据类型的方法地址,实现接口调用方法的动态分派。 那么 Go 是如何校验一个结构体 `struct` 有没有实现一个包含方法的接口 `interface`呢?我们先写一段无法通过编译的代码: ```go package main import "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{} } ``` 编译后得到报错: ```bash .\test.go:23:9: cannot use (*myWriter)(nil) (type *myWriter) as type io.Writer in assignment: *myWriter does not implement io.Writer (missing Write method) .\test.go:27:9: cannot use myWriter literal (type myWriter) as type io.Writer in assignment: myWriter does not implement io.Writer (missing Write method) ``` 全局搜索一下这个报错 ```bash find /usr/local/go/ -name "*.go" | xargs grep "does not implement" | grep "(missing" ``` 就可以得知校验的代码位置位于 ```bash cmd/compile/internal/gc/subr.go ``` 主要部分如下: ```go // 判断 t 是否实现了 iface 的类型 func implements(t, iface *types.Type, m, samename **types.Field, ptr *int) bool { // 如果 t 也是接口类型,遍历 iface 的方法,判断 t 是否全部实现了 if t.IsInterface() { ... } // t 不是接口类型,取出其实现全部的方法 t = methtype(t) var tms []*types.Field if t != nil { expandmeth(t) // 取出实现的全部方法 tms = t.AllMethods().Slice() } // 遍历 iface 的方法,判断 t 是否全部实现了 i := 0 for _, im := range iface.Fields().Slice() { ... } return true } ``` 于是我们就得出了一个 `struct` 是否实现了(类型转换)成 `interface` 的过程  # 几个关于 interface 常见的问题 ## 空接口是否等于 nil 的问题 提问,下面这段代码的两个 print 分别输出什么? ```go type User struct { username string } func main() { var a interface{} fmt.Println(a == nil) a = User{} fmt.Println(a == nil) } ``` <div class="panel panel-default collapse-panel box-shadow-wrap-lg"><div class="panel-heading panel-collapse" data-toggle="collapse" data-target="#collapse-257e259ffe37abc6a9db0f3010152fd26" aria-expanded="true"><div class="accordion-toggle"><span>点击展开答案~</span> <i class="pull-right fontello icon-fw fontello-angle-right"></i> </div> </div> <div class="panel-body collapse-panel-body"> <div id="collapse-257e259ffe37abc6a9db0f3010152fd26" class="collapse collapse-content"><p></p> 答案分别是 `true` 和 `false`,一开始我们定义了一个空接口,空接口的 `_type` 和 `data` 均为 nil,所以他是 nil。后面把一个空的 `User{}` 赋值给了这个空接口,则会调用 `runtime.convI2I()` 方法,把 `User{}` 的 `_type` 赋值给空接口。所以最后空接口的 `_type` 不为空、`data` 为空,则他就不等于 nil 了 <p></p></div></div></div> ## 关于 []interface 的问题 下面代码能编译通过吗? ```go func printStr(str []interface{}) { for _, val := range str { fmt.Println(val) } } func main(){ names := []string{"stanley", "david", "oscar"} printStr(names) } ``` <div class="panel panel-default collapse-panel box-shadow-wrap-lg"><div class="panel-heading panel-collapse" data-toggle="collapse" data-target="#collapse-41507faa9a89162c6f3ccff2ca1f96b594" aria-expanded="true"><div class="accordion-toggle"><span>点击展开答案~</span> <i class="pull-right fontello icon-fw fontello-angle-right"></i> </div> </div> <div class="panel-body collapse-panel-body"> <div id="collapse-41507faa9a89162c6f3ccff2ca1f96b594" class="collapse collapse-content"><p></p> 答案是不能编译通过:`cannot use names (type []string) as type []interface {} in argument to printStr` 这里直接贴官方说明的翻译了,原文见[InterfaceSlice](https://github.com/golang/go/wiki/InterfaceSlice) > 这有两个主要原因。 > > 第一个原因是,一个类型为[]interface{}的变量不是一个接口!它是一个切片,其元素类型恰好是interface{}。 > > 一个类型为[]interface{}的变量有一个特定的内存布局,在编译时就已经知道了。 > > 每个interface{}占用两个字(一个字段是包含的类型`_type`,另一个字段是包含的数据`data`)。因此,一个长度为N、类型为[]interface{}的片断,其背后是一个长度为N*2个字的数据块。 > > 这与支持类型为[]MyType且长度相同的片断的数据块不同。它的数据块将是N*sizeof(MyType)字长。 > > 其结果是,你不能快速地将[]MyType类型的东西分配给[]interface{}类型的东西;它们背后的数据只是看起来不同。 这里简单总结一下:**interface** 会占用两个字长的存储空间,一个是自身的 methods 数据,一个是指向其存储值的指针,也就是 interface 变量存储的值,因而 slice []interface{} 其长度是固定的**N*2**,但是 []T 的长度是**N*sizeof(T)**,两种 slice 实际存储值的大小是有区别的。把他改成下面的样子就能正常运行: ```go func printStr(str []interface{}) { for _, val := range str { fmt.Println(val) } } func main(){ names := []interface{}{"stanley", "david", "oscar"} printStr(names) } ``` <p></p></div></div></div> # interface{} 的性能问题 通过前文的介绍,我们知道了使用 `interface` 作为函数参数,是在 runtime 的时候动态的确定行为的。而动态必然带来了性能损耗。 - 直接使用接口方法调用,会发生内存逃逸。而具体类型调用或者断言后调用,不会发生内存逃逸。 - 直接使用接口方法调用,不能内联。 > **什么是内联** > > 内联是一个基本的编译器优化,它用被调用函数的主体替换函数调用。以消除调用开销,但更重要的是启用了其他编译器优化。这是在编译过程中自动执行的一类基本优化之一。它对于我们程序性能的提升主要有两方面 如果还想没懂什么是内联可以看看这篇文章 <div class="preview"> <div class="post-inser post box-shadow-wrap-normal"> <a href="https://gvoidy.cn/index.php/archives/306/" target="_blank" class="post_inser_a no-external-link no-underline-link"> <div class="inner-image bg" style="background-image: url(https://gvoidy.cn/usr/uploads/2021/07/1305760700.gif);background-size: cover;"></div> <div class="inner-content" > <p class="inser-title">通过游戏解释内联优化</p> <div class="inster-summary text-muted"> 什么是内联优化内联是一个基本的编译器优化,他会将一些简单的函数展开放入程序主题中,减少调用函数本身的开销。内联优化... </div> </div> </a> <!-- .inner-content #####--> </div> <!-- .post-inser ####--> </div> # Preference [维基百科-鸭子类型](https://zh.wikipedia.org/wiki/%E9%B8%AD%E5%AD%90%E7%B1%BB%E5%9E%8B) [深度解密Go语言之关于interface的 10 个问题](https://qcrao.com/2019/04/25/dive-into-go-interface/) [深度解密Go语言之反射](https://qcrao.com/2019/05/07/dive-into-go-reflection/) [Go 语言设计与实现 4.2 接口](https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-interface/) [InterfaceSlice](https://github.com/golang/go/wiki/InterfaceSlice) [Go interface原理详解及断言效率分析](https://zhuanlan.zhihu.com/p/136949285) [探索 Go 中接口的性能](https://juejin.cn/post/6844904148446478344#heading-8) 最后修改:2021 年 08 月 03 日 08 : 54 PM © 允许规范转载 赞赏 如果觉得我的文章对你有用,请随意赞赏 赞赏作者 支付宝微信