摘要
本文内容转自网络,个人学习记录使用,请勿传播
接口是一个对象的对外能力的展现,我们使用一个对象时,往往不需要知道一个对象的内部复杂实现,通过它暴露出来的接口,就知道了这个对象具备哪些能力以及如何使用这个能力。
一个复杂的复合对象常常也可以是一个多面手,它具备多种能力,在形式上实现了多种接口。使用时我们根据不同的场合来挑选满足需要的接口能力来使用这个对象即可。
Go 语言的接口类型非常特别,Go 语言的接口是隐式的,只要结构体上定义的方法在形式上(名称、参数和返回值)和接口定义的一样,那么这个结构体就自动实现了这个接口,我们就可以使用这个接口变量来指向这个结构体对象。
接口
- 先创建接口
- 然后实现接口
1 | package main |
上面的代码定义了两种接口,Apple 结构体同时实现了这两个接口,而 Flower 结构体只实现了 Smellable 接口。结构体和接口自动产生了关联。
空接口
如果一个接口里面没有定义任何方法,那么它就是空接口,任意结构体都隐式地实现了空接口。
Go 语言为了避免用户重复定义很多空接口,它自己内置了一个,这个空接口的名字特别奇怪,叫 interface{}
,初学者会非常不习惯。之所以这个类型名带上了大括号,那是在告诉用户括号里什么也没有。
空接口里面没有方法,所以它也不具有任何能力,可以容纳任意对象,它是一个万能容器。比如一个字典的 key 是字符串,但是希望 value 可以容纳任意类型的对象,这时候就可以使用空接口类型 interface{}
。
1 | package main |
代码中 user 字典变量的类型是 map[string]interface{}
,从这个字典中直接读取得到的 value 类型是 interface{}
,需要通过类型转换才能得到期望的变量。
接口变量的本质
在使用接口时,我们要将接口看成一个特殊的容器,这个容器只能容纳一个对象,只有实现了这个接口类型的对象才可以放进去。
接口变量作为变量来说它也是需要占据内存空间的,通过翻阅 Go 语言的源码可以发现,接口变量也是由结构体来定义的,这个结构体包含两个指针字段,一个字段指向被容纳的对象内存,另一个字段指向一个特殊的结构体 itab,这个特殊的结构体包含了接口的类型信息和被容纳对象的数据类型信息。
1 | // interface structure |
既然接口变量只包含两个指针字段,那么它的内存占用应该是 2 个机器字,下面我们来编写代码验证一下
1 | package main |
数组的内存占用是 10 个机器字,但是这丝毫不会影响到接口变量的内存占用。
用接口来模拟多态
接口是一种特殊的容器,它可以容纳多种不同的对象,只要这些对象都同样实现了接口定义的方法。如果我们将容纳的对象替换成另一个对象,那不就可以实现多态的功能了么?
1 | package main |
使用这种方式模拟多态本质上是通过组合属性变量(Name)和接口变量(Fruitable)来做到的,属性变量是对象的数据,而接口变量是对象的功能,将它们组合到一块就形成了一个完整的多态性的结构体。
1 | package main |
接口的组合继承
接口的定义也支持组合继承,比如我们可以将两个接口定义合并为一个接口
1 | type Smellable interface { |
这时 Fruitable 接口就自动包含了 smell() 和 eat() 两个方法,它和下面的定义是等价的。
1 | type Fruitable interface { |
接口变量的赋值
变量赋值本质上是一次内存浅拷贝,切片的赋值是拷贝了切片头,字符串的赋值是拷贝了字符串的头部,而数组的赋值呢是直接拷贝整个数组。接口变量的赋值会不会不一样呢?
1 | package main |
从上面的输出结果中可以推断出结构体的内存发生了复制
- 这个复制可能是因为赋值
(a = r)
- 也可能是因为类型转换
(rx = a.(Rect))
- 也可能是两者都进行了内存复制
那能不能判断出究竟在接口变量赋值时有没有发生内存复制呢?
不好意思,就目前来说我们学到的知识点还办不到。到后面的高级阶段我们将会使用 unsafe 包来洞悉其中的更多细节。不过我可以提前告诉你们答案是什么,那就是两者都会发生数据内存的复制 —— 浅拷贝。
指向指针的接口变量
如果将上面的例子改成指针,将接口变量指向结构体指针,那结果就不一样了
1 | package main |
从输出结果中可以看出指针变量 rx 指向的内存和变量 r 的内存是同一份。因为在类型转换的过程中只发生了指针变量的内存复制,而指针变量指向的内存是共享的。