深度解密Go语言之关于 interface 的 10 个问题(一)
文章目录
Go 语言与鸭子类型的关系
先直接来看*里的定义:
If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
-
翻译过来就是:
- 如果某个东西长得像鸭子,像鸭子一样游泳,像鸭子一样嘎嘎叫,那它就可以被看成是一只鸭子
-
Duck Typing
,鸭子类型,是动态编程语言的一种对象推断策略- 它更关注对象能如何被使用
- 而不是对象的类型本身
-
Go 语言作为一门静态语言
- 它通过接口的方式完美支持鸭子类型
例如,在动态语言 python 中,定义一个这样的函数:
def hello_world(coder):
coder.say_hello()
-
当调用此函数的时候
- 可以传入任意类型
- 只要它实现了
say_hello()
函数就可以
-
如果没有实现,运行过程中会出现错误
-
而在静态语言如 Java, C++ 中
- 必须要显示地声明实现了某个接口
- 之后,才能用在任何需要这个接口的地方
- 如果你在程序中调用
hello_world
函数- 却传入了一个根本就没有实现
say_hello()
的类型 - 那在编译阶段就不会通过
- 却传入了一个根本就没有实现
-
这也是静态语言比动态语言更安全的原因
-
动态语言和静态语言的差别在此就有所体现
-
静态语言在编译期间就能发现类型不匹配的错误
- 不像动态语言,必须要运行到那一行代码才会报错
-
当然
- 静态语言要求程序员在编码阶段就要按照规定来编写程序
- 为每个变量规定数据类型
- 这在某种程度上
- 加大了工作量
- 也加长了代码量
- 动态语言则没有这些要求
- 可以让人更专注在业务上
- 代码也更短
- 写起来更快
- 可以让人更专注在业务上
- 静态语言要求程序员在编码阶段就要按照规定来编写程序
-
这一点,写 python 的同学比较清楚
-
Go 语言作为一门现代静态语言
- 是有后发优势的
-
它引入了动态语言的便利
- 同时又会进行静态语言的类型检查
-
写起来是非常 Happy 的
-
Go 采用了折中的做法:
- 不要求类型显示地声明实现了某个接口
- 只要实现了相关的方法即可
- 编译器就能检测到
-
来看个例子:
package main
import "fmt"
// 先定义一个接口,和使用此接口作为参数的函数:
type IGreeting interface {
sayHello()
}
func sayHello(i IGreeting) {
i.sayHello()
}
// 再来定义两个结构体:
type Go struct {
}
func (g Go) sayHello() {
fmt.Println("Hi, I am Go")
}
type PHP struct {
}
func (p PHP) sayHello() {
fmt.Println("Hi, I am PHP!")
}
func main() {
golang := Go{}
php := PHP{}
sayHello(golang)
sayHello(php)
}
```
type IGreeting interface {
sayHello()
}
func sayHello(i IGreeting) {
i.sayHello()
}
type Go struct {
}
func (g Go) sayHello() {
fmt.Println("Hi, I am Go")
}
type PHP struct {
}
func (p PHP) sayHello() {
fmt.Println("Hi, I am PHP!")
}
// 最后,在 main 函数里调用 sayHello() 函数:
func main() {
golang := Go{}
php := PHP{}
sayHello(golang)
sayHello(php)
}
// 程序输出:
Hi, I am GO!
Hi, I am PHP!
- 在 main 函数中
- 调用 sayHello() 函数时
- 传入了
golang, php
对象 - 它们并没有显式地声明实现了 IGreeting 类型
- 只是实现了接口所规定的 sayHello() 函数
- 传入了
- 实际上
- 编译器在调用 sayHello() 函数时
- 会隐式地将
golang, php
对象转换成 IGreeting 类型 - 这也是静态语言的类型检查功能
- 会隐式地将
顺带再提一下动态语言的特点:
变量绑定的类型是不确定的
在运行期间才能确定
函数和方法可以接收任何类型的参数
且调用时不检查参数类型
不需要实现接口
- 总结一下
- 鸭子类型是一种动态语言的风格
- 在这种风格中
- 一个对象有效的语义
- 不是由继承自特定的类或实现特定的接口
- 而是由它"当前方法和属性的集合"决定
- 在这种风格中
- Go 作为一种静态语言
- 通过接口实现了
鸭子类型
- 实际上是 Go 的编译器在其中作了隐匿的转换工作
- 通过接口实现了
值接收者和指针接收者的区别
方法
-
方法能给用户自定义的类型添加新的行为
-
它和函数的区别在于方法有一个接收者
- 给一个函数添加一个接收者
- 那么它就变成了方法
-
接收者可以是
值接收者
,也可以是指针接收者
。 -
在调用方法的时候
- 值类型
- 既可以调用
值接收者
的方法 - 也可以调用
指针接收者
的方法
- 既可以调用
- 指针类型
- 既可以调用
指针接收者
的方法 - 也可以调用
值接收者
的方法
- 既可以调用
- 值类型
-
也就是说
- 不管方法的接收者是什么类型
- 该类型的值和指针都可以调用
- 不必严格符合接收者的类型
来看个例子:
package main
import "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())
// ----------------------
// 值类型 调用接收者也是值类型的方法
fmt.Println(qcrao.howOld())
// 值类型 调用接收者是指针类型的方法
qcrao.growUp()
fmt.Println(qcrao.howOld())
// ----------------------
}
// 上例子的输出结果是:
18
19
100
101
-
调用了
growUp
函数后- 不管调用者是值类型还是指针类型
- 它的
Age
值都改变了
-
实际上
- 当类型和方法的接收者类型不同时
-
其实是编译器在背后做了一些工作
- 用一个表格来呈现:
- | 值接收者 | 指针接收者 |
---|---|---|
值类型调用者 | 方法会使用调用者的一个副本,类似于“传值” | 使用值的引用来调用方法,上例中,qcrao.growUp() 实际上是 (&qcrao).growUp()
|
指针类型调用者 | 指针被解引用为值,上例中,stefno.howOld() 实际上是 (*stefno).howOld()
|
实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针 |
值接收者和指针接收者
-
前面说过
- 不管接收者类型是值类型还是指针类型
- 都可以通过值类型或指针类型调用
- 这里面实际上通过语法糖起作用的
- 不管接收者类型是值类型还是指针类型
-
先说结论:
- 实现了接收者是值类型的方法相当于自动实现了接收者是指针类型的方法
- 而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法
来看一个例子,就会完全明白:
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:24:6: cannot use Programmer literal (type Programmer) as type coder in assignment:
Programmer does not implement coder (debug method has pointer receiver)
-
看出这两处代码的差别了吗?
- 第一次是将
&Gopher
赋给了coder
- 第二次则是将
Gopher
赋给了coder
。
- 第一次是将
-
第二次报错是说
-
Gopher
没有实现coder
-
-
很明显了吧
- 因为
Gopher
类型并没有实现debug
方法 - 表面上看,
*Gopher
类型也没有实现code
方法 - 但是因为
Gopher
类型实现了code
方法- 所以让
*Gopher
类型自动拥有了code
方法
- 所以让
- 因为
-
当然,上面的说法有一个简单的解释:
- 接收者是指针类型的方法
- 很可能在方法中会对接收者的属性进行更改操作
- 从而影响接收者
- 而对于接收者是值类型的方法
- 在方法中不会对接收者本身产生影响
- 接收者是指针类型的方法
-
所以,当实现了一个接收者是值类型的方法
- 就可以自动生成一个接收者是对应指针类型的方法
- 因为两者都不会影响接收者
-
但是,当实现了一个接收者是指针类型的方法
- 如果此时自动生成一个接收者是值类型的方法
- 原本期望对接收者的改变(通过指针实现)
- 现在无法实现
- 因为值类型会产生一个拷贝
- 不会真正影响调用者
最后,只要记住下面这点就可以了:
如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法
两者分别在何时使用
-
如果方法的接收者是值类型
- 无论调用者是对象还是对象指针
- 修改的都是对象的副本
- 不影响调用者
- 无论调用者是对象还是对象指针
-
如果方法的接收者是指针类型
- 则调用者修改的是指针指向的对象本身
-
使用指针作为方法的接收者的理由:
- 方法能够修改接收者指向的值
- 避免在每次调用方法时复制该值
- 在值的类型为大型结构体时
- 这样做会更加高效
-
使用值接收者还是指针接收者
- 不是由该方法是否修改了调用者(也就是接收者)来决定
- 而是应该基于该类型的
本质
-
如果类型具备“原始的本质”
-
也就是说它的成员都是由 Go 语言里内置的原始类型
- 如字符串,整型值等
- 那就定义值接收者类型的方法
- 如字符串,整型值等
-
像内置的引用类型
- 如 slice,map,interface,channel
- 这些类型比较特殊,声明他们的时候,实际上是创建了一个
header
- 对于他们也是直接定义值接收者类型的方法
- 这些类型比较特殊,声明他们的时候,实际上是创建了一个
- 这样,调用函数时
- 是直接 copy 了这些类型的
header
- 而
header
本身就是为复制设计的。
- 是直接 copy 了这些类型的
- 如 slice,map,interface,channel
-
如果类型具备非原始的本质
- 不能被安全地复制
- 这种类型总是应该被共享
- 那就定义指针接收者的方法
- 比如 go 源码里的文件结构体(struct File)就不应该被复制
- 应该只有一份
实体
- 不能被安全地复制
-
这一段说的比较绕,大家可以去看《Go 语言实战》5.3 那一节
iface 和 eface 的区别是什么
-
iface
和eface
都是 Go 中描述接口的底层结构体 - 区别在于
-
iface
描述的接口包含方法 -
eface
则是不包含任何方法的空接口:interface{}
-
从源码层面看一下:
type iface struct {
tab *itab
data unsafe.Pointer
}
type itab struct {
inter *interfacetype
_type *_type
link *itab
hash uint32 // copy of _type.hash. Used for type switches.
bad bool // type does not implement interface
inhash bool // has this itab been added to hash?
unused [2]byte
fun [1]uintptr // variable sized
}
-
iface
内部维护两个指针-
tab
指向一个itab
实体- 它表示接口的类型以及赋给这个接口的实体类型
-
data
则指向接口具体的值- 一般而言是一个指向堆内存的指针
-
-
再来仔细看一下
itab
结构体:-
_type
字段描述了实体的类型- 包括内存对齐方式,大小等
-
inter
字段则描述了接口的类型 -
fun
字段放置和接口方法对应的具体数据类型的方法地址- 实现接口调用方法的动态分派
- 一般在每次给接口赋值发生转换时会更新此表
- 或者直接拿缓存的 itab
-
-
这里只会列出实体类型和接口相关的方法
-
实体类型的其他方法并不会出现在这里
- 如果你学过 C++ 的话,这里可以类比虚函数的概念
-
另外,你可能会觉得奇怪,为什么
fun
数组的大小为 1 -
要是接口定义了多个方法可怎么办?
- 实际上,这里存储的是第一个方法的函数指针
- 如果有更多的方法,在它之后的内存空间里继续存储
- 从汇编角度来看
- 通过增加地址就能获取到这些函数指针,没什么影响
- 顺便提一句,这些方法是按照函数名称的字典序进行排列的
再看一下 interfacetype
类型,它描述的是接口的类型:
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
- 可以看到
- 它包装了
_type
类型-
_type
实际上是描述 Go 语言中各种数据类型的结构体 - 我们注意到
- 这里还包含一个
mhdr
字段- 表示接口所定义的函数列表
-
pkgpath
记录定义了接口的包名
-
这里通过一张图来看下 iface
结构体的全貌:
接着来看一下 eface
的源码:
type eface struct {
_type *_type
data unsafe.Pointer
}
- 相比
iface
-
eface
就比较简单了- 只维护了一个
_type
字段- 表示空接口所承载的具体的实体类型
-
data
描述了具体的值
- 只维护了一个
我们来看个例子:
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() {
x := 200
var any interface{} = x
fmt.Println(any)
g := Gopher{"Go"}
var c coder = g
fmt.Println(c)
}
// 执行命令,打印出汇编语言:
go tool compile -S ./src/main.go
// 可以看到,main 函数里调用了两个函数:
func convT2E64(t *_type, elem unsafe.Pointer) (e eface)
func convT2I(tab *itab, elem unsafe.Pointer) (i iface)
-
上面两个函数的参数和
iface
及eface
结构体的字段是可以联系起来的:- 两个函数都是
- 将参数
组装
一下 - 形成最终的接口
- 将参数
- 两个函数都是
-
作为补充,我们最后再来看下
_type
结构体:
type _type struct {
// 类型大小
size uintptr
ptrdata uintptr
// 类型的 hash 值
hash uint32
// 类型的 flag,和反射相关
tflag tflag
// 内存对齐相关
align uint8
fieldalign uint8
// 类型的编号,有bool, slice, struct 等等等等
kind uint8
alg *typeAlg
// gc 相关
gcdata *byte
str nameOff
ptrToThis typeOff
}
- Go 语言各种数据类型都是在
_type
字段的基础上,增加一些额外的字段来进行管理的:
type arraytype struct {
typ _type
elem *_type
slice *_type
len uintptr
}
type chantype struct {
typ _type
elem *_type
dir uintptr
}
type slicetype struct {
typ _type
elem *_type
}
type structtype struct {
typ _type
pkgPath name
fields []structfield
}
- 这些数据类型的结构体定义,是反射实现的基础
接口的动态类型和动态值
- 从源码里可以看到:
iface
包含两个字段:-
tab
是接口表指针- 指向类型信息
-
data
是数据指针- 指向具体的数据
- 它们分别被称为
动态类型
和动态值
- 而接口值包括
动态类型
和动态值
-
【引申1】接口类型和 nil
作比较
- 接口值的零值是指
动态类型
和动态值
都为nil
- 当仅且当这两部分的值都为
nil
的情况下 - 这个接口值就才会被认为
接口值 == nil
- 当仅且当这两部分的值都为
来看个例子:
package main
import "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 Coder
fmt.Println(c == nil)
fmt.Printf("c: %T, %v\n", c, c)
var g *Gopher
fmt.Println(g == nil)
c = g
fmt.Println(c == nil)
fmt.Printf("c: %T, %v\n", c, c)
}
// 输出:
true
c: <nil>, <nil>
true
false
c: *main.Gopher, <nil>
- 一开始
-
c
的 动态类型和动态值都为nil
-
g
也为nil
- 当把
g
赋值给c
后-
c
的动态类型变成了*main.Gopher
-
- 仅管
c
的动态值仍为nil
- 但是当
c
和nil
作比较的时候- 结果就是
false
了
- 结果就是
-
【引申2】 来看一个例子,看一下它的输出:
package main
import "fmt"
type MyError struct {}
func (i MyError) Error() string {
return "MyError"
}
func main() {
err := Process()
fmt.Println(err)
fmt.Println(err == nil)
}
func Process() error {
var err *MyError = nil
return err
}
// 函数运行结果:
<nil>
false
- 这里先定义了一个
MyError
结构体- 实现了
Error
函数 - 也就实现了
error
接口
- 实现了
-
Process
函数返回了一个error
接口- 这块隐含了类型转换
- 所以
- 虽然它的值是
nil
- 其实它的类型是
*MyError
- 最后和
nil
比较的时候,结果为false
- 虽然它的值是
【引申3】如何打印出接口的动态类型和值?
package main
import (
"unsafe"
"fmt"
)
type iface struct {
itab, data uintptr
}
func main() {
var a interface{} = nil
var b interface{} = (*int)(nil)
x := 5
var 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)))
}
// 运行结果如下:
{0 0} {17426912 0} {17426912 842350714568}
5
-
代码里直接定义了一个
iface
结构体- 用两个指针来描述
itab
和data
- 之后将 a, b, c 在内存中的内容强制解释成我们自定义的
iface
- 最后就可以打印出动态类型和动态值的地址
- 用两个指针来描述
-
a 的动态类型和动态值的地址均为 0,也就是 nil
-
b 的动态类型和 c 的动态类型一致,都是
*int
-
c 的动态值为 5
编译器自动检测类型是否实现接口
经常看到一些开源库里会有一些类似下面这种奇怪的用法:
var _ io.Writer = (*myWriter)(nil)
- 这时候会有点懵
- 不知道作者想要干什么
- 实际上这就是此问题的答案
- 编译器会由此检查
*myWriter
类型是否实现了io.Writer
接口
来看一个例子:
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{}
}
// 注释掉为 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{}