接口是一个对象的对外能力的展示,咱们使用一个对象时,每每不须要知道一个对象的内部复杂实现,经过它暴露出来的接口,就知道了这个对象具有哪些能力以及如何使用这个能力。数组
咱们常说「佛有千面」,不一样的人看到的佛并不同。一个复杂的复合对象经常也能够是一个多面手,它具有多种能力,在形式上实现了多种接口。「弱水三千,只取一瓢」,使用时咱们根据不一样的场合来挑选知足须要的接口能力来使用这个对象便可。bash
Go 语言的接口类型很是特别,它的做用和 Java 语言的接口同样,可是在形式上有很大的差异。Java 语言须要在类的定义上显式实现了某些接口,才能够说这个类具有了接口定义的能力。可是 Go 语言的接口是隐式的,只要结构体上定义的方法在形式上(名称、参数和返回值)和接口定义的同样,那么这个结构体就自动实现了这个接口,咱们就可使用这个接口变量来指向这个结构体对象。下面咱们看个例子微信
package main
import "fmt"
// 能够闻
type Smellable interface {
smell()
}
// 能够吃
type Eatable interface {
eat()
}
// 苹果既可能闻又能吃
type Apple struct {}
func (a Apple) smell() {
fmt.Println("apple can smell")
}
func (a Apple) eat() {
fmt.Println("apple can eat")
}
// 花只能够闻
type Flower struct {}
func (f Flower) smell() {
fmt.Println("flower can smell")
}
func main() {
var s1 Smellable
var s2 Eatable
var apple = Apple{}
var flower = Flower{}
s1 = apple
s1.smell()
s1 = flower
s1.smell()
s2 = apple
s2.eat()
}
--------------------
apple can smell
flower can smell
apple can eat
复制代码
上面的代码定义了两种接口,Apple 结构体同时实现了这两个接口,而 Flower 结构体只实现了 Smellable 接口。咱们并无使用相似于 Java 语言的 implements 关键字,结构体和接口就自动产生了关联。app
若是一个接口里面没有定义任何方法,那么它就是空接口,任意结构体都隐式地实现了空接口。ui
Go 语言为了不用户重复定义不少空接口,它本身内置了一个,这个空接口的名字特别奇怪,叫 interface{} ,初学者会很是不习惯。之因此这个类型名带上了大括号,那是在告诉用户括号里什么也没有。我始终认为这种名字很古怪,它让代码看起来有点丑陋。spa
空接口里面没有方法,因此它也不具备任何能力,其做用至关于 Java 的 Object 类型,能够容纳任意对象,它是一个万能容器。好比一个字典的 key 是字符串,可是但愿 value 能够容纳任意类型的对象,相似于 Java 语言的 Map<String,Object> 类型,这时候就可使用空接口类型 interface{}。指针
package main
import "fmt"
func main() {
// 连续两个大括号,是否是看起来很别扭
var user = map[string]interface{}{
"age": 30,
"address": "Beijing Tongzhou",
"married": true,
}
fmt.Println(user)
// 类型转换语法来了
var age = user["age"].(int)
var address = user["address"].(string)
var married = user["married"].(bool)
fmt.Println(age, address, married)
}
-------------
map[age:30 address:Beijing Tongzhou married:true]
30 Beijing Tongzhou true
复制代码
代码中 user 字典变量的类型是 map[string]interface{},从这个字典中直接读取获得的 value 类型是 interface{},须要经过类型转换才能获得指望的变量。code
在使用接口时,咱们要将接口当作一个特殊的容器,这个容器只能容纳一个对象,只有实现了这个接口类型的对象才能够放进去。cdn
接口变量做为变量来讲它也是须要占据内存空间的,经过翻阅 Go 语言的源码能够发现,接口变量也是由结构体来定义的,这个结构体包含两个指针字段,一个字段指向被容纳的对象内存,另外一个字段指向一个特殊的结构体 itab,这个特殊的结构体包含了接口的类型信息和被容纳对象的数据类型信息。对象
// interface structure
type iface struct {
tab *itab // 类型指针
data unsafe.Pointer // 数据指针
}
type itab struct {
inter *interfacetype // 接口类型信息
_type *_type // 数据类型信息
...
}
复制代码
既然接口变量只包含两个指针字段,那么它的内存占用应该是 2 个机器字,下面咱们来编写代码验证一下
package main
import "fmt"
import "unsafe"
func main() {
var s interface{}
fmt.Println(unsafe.Sizeof(s))
var arr = [10]int {1,2,3,4,5,6,7,8,9,10}
fmt.Println(unsafe.Sizeof(arr))
s = arr
fmt.Println(unsafe.Sizeof(s))
}
----------
16
80
16
复制代码
数组的内存占用是 10 个机器字,可是这丝绝不会影响到接口变量的内存占用。
前面咱们说到,接口是一种特殊的容器,它能够容纳多种不一样的对象,只要这些对象都一样实现了接口定义的方法。若是咱们将容纳的对象替换成另外一个对象,那不就能够完成上一节咱们没有完成的多态功能了么?好,顺着这个思路,下面咱们就来模拟一下多态
package main
import "fmt"
type Fruitable interface {
eat()
}
type Fruit struct {
Name string // 属性变量
Fruitable // 匿名内嵌接口变量
}
func (f Fruit) want() {
fmt.Printf("I like ")
f.eat() // 外结构体会自动继承匿名内嵌变量的方法
}
type Apple struct {}
func (a Apple) eat() {
fmt.Println("eating apple")
}
type Banana struct {}
func (b Banana) eat() {
fmt.Println("eating banana")
}
func main() {
var f1 = Fruit{"Apple", Apple{}}
var f2 = Fruit{"Banana", Banana{}}
f1.want()
f2.want()
}
---------
I like eating apple
I like eating banana
复制代码
使用这种方式模拟多态本质上是经过组合属性变量(Name)和接口变量(Fruitable)来作到的,属性变量是对象的数据,而接口变量是对象的功能,将它们组合到一块就造成了一个完整的多态性的结构体。
接口的定义也支持组合继承,好比咱们能够将两个接口定义合并为一个接口以下
type Smellable interface {
smell()
}
type Eatable interface {
eat()
}
type Fruitable interface {
Smellable
Eatable
}
复制代码
这时 Fruitable 接口就自动包含了 smell() 和 eat() 两个方法,它和下面的定义是等价的。
type Fruitable interface {
smell()
eat()
}
复制代码
变量赋值本质上是一次内存浅拷贝,切片的赋值是拷贝了切片头,字符串的赋值是拷贝了字符串的头部,而数组的赋值呢是直接拷贝整个数组。接口变量的赋值会不会不同呢?接下来咱们作一个实验
package main
import "fmt"
type Rect struct {
Width int
Height int
}
func main() {
var a interface {}
var r = Rect{50, 50}
a = r
var rx = a.(Rect)
r.Width = 100
r.Height = 100
fmt.Println(rx)
}
------
{50 50}
复制代码
从上面的输出结果中能够推断出结构体的内存发生了复制,这个复制多是由于赋值(a = r)也多是由于类型转换(rx = a.(Rect)),也多是二者都进行了内存复制。那能不能判断出究竟在接口变量赋值时有没有发生内存复制呢?很差意思,就目前来讲咱们学到的知识点还办不到。到后面的高级阶段咱们将会使用 unsafe 包来洞悉其中的更多细节。不过我能够提早告诉大家答案是什么,那就是二者都会发生数据内存的复制 —— 浅拷贝。
若是将上面的例子改为指针,将接口变量指向结构体指针,那结果就不同了
package main
import "fmt"
type Rect struct {
Width int
Height int
}
func main() {
var a interface {}
var r = Rect{50, 50}
a = &r // 指向告终构体指针
var rx = a.(*Rect) // 转换成指针类型
r.Width = 100
r.Height = 100
fmt.Println(rx)
}
-------
&{100 100}
复制代码
从输出结果中能够看出指针变量 rx 指向的内存和变量 r 的内存是同一份。由于在类型转换的过程当中只发生了指针变量的内存复制,而指针变量指向的内存是共享的。
微信扫一扫上面的二维码,关注「码洞」阅读《快学 Go 语言》更多章节