1.一个接口类型的值,简称接口值
2.接口值 有两部分组成 一个具体类型 和 该类型的一个值。二者成为接口的动态类型和动态值
3.接口的零值 也可以说是初始值 是 动态类型 和动态值 都为 nil
4.一个nil的接口 即 nil接口值 取决于 动态类型 是否为 nil (这个时候动态值应该也是nil)
5.对于一个接口值,类型部分就用对应的类型描述符来表示
var w io.writer
上面声明了w,也是一个nil接口值,如图
w=os.Stdout
上面os.Stdout是*os.File类型,意思是把这个类型赋给了w,所以接口的动态类型会设置为指针类型*os.File 的类型描述符,动态值会设置为os.stdout的副本,如下
6.一般在编译的时候,我们无法知道一个接口值的动态类型会是什么,都是实际执行的时候才能确定,所以通过接口来做调用必须使用 动态分发。编译时必须生成一段代码来从类型描述符拿到对应类型的方法,这里就是write方法,在间接调用该方法地址。调用的接收者就是接口的动态值,这里就是os.stdout,所以实际效果和直接调用等价
os.Stdout.Write([]byte("hello")) // "hello"
7.当一个表达式 实现了接口的方法,就可以赋值给该接口,下面给接口赋值另外一种类型
w=new(bytes.Buffer)
动态类型变成 *bytes.Buffer ,动态值则是一个指向新分配缓冲区的指针,如下
针对第6点,这里通过类型描述符拿到*bytes.Buffer对应的write方法,调用的接收者就是缓冲区的地址,方法执行就是把hello写到这个缓冲区内
下面这个赋值,是把动态类型和动态值都设置为nil,恢复到声明时的状态
w = nil
8.接口值 可以用 == 操作符来比较。如果 接口值都为nil 或者 二者的动态类型完全一致,动态值相等(使用动态类型的**==操作符来比较**),那么两个接口是相等的。这里的动态值是指针指向内容的值,不是指针地址的值
9.对于动态值的比较,像slice,map这种不能使用==比较的类型就没办法比较接口,会崩溃,所以判断接口的时候要注意 动态类型是什么,不是所有的接口都是安全比较的。必须确定动态值是可以比较的,才可以比较接口。
10.因为接口是可以比较的,所以接口可以作为map的键,也可以作为switch语句的操作数,但是要注意第9点
11.一个nil接口值和 动态值为nil的接口值 是不一样的 ,前者是动态类型和值都为nil ,后者是一个含有空指针的非空接口
12.判断一个接口是否 为nil,是判断这个接口是否是nil接口,即动态类型和值都为nil,只有动态值为nil的话,只是一个含有空指针的非空接口,所以 接口 != nil
const debug = false
func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer) // enable collection of output
}
f(buf) // NOTE: subtly incorrect!
if debug { // ...use buf... } } // If out is non-nil, output will be written to it.
func f(out io.Writer) { // ...do something...
if out != nil {
out.Write([]byte("done!\n")) }
}
如果理解了11点 那么就会知道这段代码会崩溃
var buf *bytes.Buffer 配置了动态类型,debug为true内部代码是给buf设置了动态值,false不走这里,所以动态值仍为nil,方法f里面的 判断out,因为类型有值,不是nil接口,所以会进入里面逻辑,执行动态类型对应的方法,但是方法接口者即动态值是nil,所以调用的时候会崩溃
12.针对上面的崩溃需要说明,并不是说方法接收者是nil就一定会崩溃,如果方法内部允许接收者是nil,就不会崩溃,视情况而定,上面*bytes.Buffer的write方法因为在调用时尝试访问缓冲区,nil导致崩溃了,下面这种情况就允许接收者为nil,不会崩溃
type IntList struct {
Value int Tail *IntList
}
//Sum returns the sum of the list elements.
func (list *IntList) Sum() int {
if list == nil {
return 0
}
return list.Value + list.Tail.Sum()
}
13.结构体如果内嵌接口,就可以使用接口的所有方法,方法由内嵌的接口隐式提供,具体方法的地址在接口赋值的时候由接口的动态类型决定,结构体还可以重写某些方法,执行的时候会优先查找重写方法并执行,go原生包中有个例子可以参考,sort包里面的reverse结构体就是内嵌了sort.Interface接口,详见juejin.cn/post/711447…
所以当方法f的参数是接口的时候,内嵌接口的结构体B也可以作为实例参数传递,不会报错
package main
import "fmt"
type A interface {
say()
}
type B struct {
A
}
func f(a A) {
fmt.Println("h")
}
func main() {
b := B{}
f(b)//'h'
}