接口是一個對象的對外能力的展示,咱們使用一個對象時,每每不須要知道一個對象的內部複雜實現,經過它暴露出來的接口,就知道了這個對象具有哪些能力以及如何使用這個能力。數組
咱們常說「佛有千面」,不一樣的人看到的佛並不同。一個複雜的複合對象經常也能夠是一個多面手,它具有多種能力,在形式上實現了多種接口。「弱水三千,只取一瓢」,使用時咱們根據不一樣的場合來挑選知足須要的接口能力來使用這個對象便可。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 語言》更多章節