Go學(xué)習(xí)筆記-接口

接口是一個(gè)對(duì)象的對(duì)外能力的展現(xiàn),我們使用一個(gè)對(duì)象時(shí),往往不需要知道一個(gè)對(duì)象的內(nèi)部復(fù)雜實(shí)現(xiàn),通過它暴露出來的接口,就知道了這個(gè)對(duì)象具備哪些能力以及如何使用這個(gè)能力。

我們常說「佛有千面」,不同的人看到的佛并不一樣。一個(gè)復(fù)雜的復(fù)合對(duì)象常常也可以是一個(gè)多面手,它具備多種能力,在形式上實(shí)現(xiàn)了多種接口?!溉跛?,只取一瓢」,使用時(shí)我們根據(jù)不同的場(chǎng)合來挑選滿足需要的接口能力來使用這個(gè)對(duì)象即可。

Go 語言的接口類型非常特別,它的作用和 Java 語言的接口一樣,但是在形式上有很大的差別。Java 語言需要在類的定義上顯式實(shí)現(xiàn)了某些接口,才可以說這個(gè)類具備了接口定義的能力。但是 Go 語言的接口是隱式的,只要結(jié)構(gòu)體上定義的方法在形式上(名稱、參數(shù)和返回值)和接口定義的一樣,那么這個(gè)結(jié)構(gòu)體就自動(dòng)實(shí)現(xiàn)了這個(gè)接口,我們就可以使用這個(gè)接口變量來指向這個(gè)結(jié)構(gòu)體對(duì)象。下面我們看個(gè)例子

package main

import "fmt"

// 可以聞
type Smellable interface {
  smell()
}

// 可以吃
type Eatable interface {
  eat()
}

// 蘋果既可能聞?dòng)帜艹?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 結(jié)構(gòu)體同時(shí)實(shí)現(xiàn)了這兩個(gè)接口,而 Flower 結(jié)構(gòu)體只實(shí)現(xiàn)了 Smellable 接口。我們并沒有使用類似于 Java 語言的 implements 關(guān)鍵字,結(jié)構(gòu)體和接口就自動(dòng)產(chǎn)生了關(guān)聯(lián)。

空接口

如果一個(gè)接口里面沒有定義任何方法,那么它就是空接口,任意結(jié)構(gòu)體都隱式地實(shí)現(xiàn)了空接口。

Go 語言為了避免用戶重復(fù)定義很多空接口,它自己內(nèi)置了一個(gè),這個(gè)空接口的名字特別奇怪,叫 interface{} ,初學(xué)者會(huì)非常不習(xí)慣。之所以這個(gè)類型名帶上了大括號(hào),那是在告訴用戶括號(hào)里什么也沒有。我始終認(rèn)為這種名字很古怪,它讓代碼看起來有點(diǎn)丑陋。

空接口里面沒有方法,所以它也不具有任何能力,其作用相當(dāng)于 Java 的 Object 類型,可以容納任意對(duì)象,它是一個(gè)萬能容器。比如一個(gè)字典的 key 是字符串,但是希望 value 可以容納任意類型的對(duì)象,類似于 Java 語言的 Map 類型,這時(shí)候就可以使用空接口類型 interface{}。

package main

import "fmt"

func main() {
 // 連續(xù)兩個(gè)大括號(hào),是不是看起來很別扭
    var user = map[string]interface{}{
        "age": 30,
        "address": "Beijing Tongzhou",
        "married": true,
    }
    fmt.Println(user)
    // 類型轉(zhuǎn)換語法來了
 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{},從這個(gè)字典中直接讀取得到的 value 類型是 interface{},需要通過類型轉(zhuǎn)換才能得到期望的變量。

接口變量的本質(zhì)

在使用接口時(shí),我們要將接口看成一個(gè)特殊的容器,這個(gè)容器只能容納一個(gè)對(duì)象,只有實(shí)現(xiàn)了這個(gè)接口類型的對(duì)象才可以放進(jìn)去。

image.png

接口變量作為變量來說它也是需要占據(jù)內(nèi)存空間的,通過翻閱 Go 語言的源碼可以發(fā)現(xiàn),接口變量也是由結(jié)構(gòu)體來定義的,這個(gè)結(jié)構(gòu)體包含兩個(gè)指針字段,一個(gè)字段指向被容納的對(duì)象內(nèi)存,另一個(gè)字段指向一個(gè)特殊的結(jié)構(gòu)體 itab,這個(gè)特殊的結(jié)構(gòu)體包含了接口的類型信息和被容納對(duì)象的數(shù)據(jù)類型信息。

// interface structure
type iface struct {
  tab *itab  // 類型指針
  data unsafe.Pointer  // 數(shù)據(jù)指針
}

type itab struct {
  inter *interfacetype // 接口類型信息
  _type *_type // 數(shù)據(jù)類型信息
  ...
}

既然接口變量只包含兩個(gè)指針字段,那么它的內(nèi)存占用應(yīng)該是 2 個(gè)機(jī)器字,下面我們來編寫代碼驗(yàn)證一下

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

數(shù)組的內(nèi)存占用是 10 個(gè)機(jī)器字,但是這絲毫不會(huì)影響到接口變量的內(nèi)存占用。

用接口來模擬多態(tài)

前面我們說到,接口是一種特殊的容器,它可以容納多種不同的對(duì)象,只要這些對(duì)象都同樣實(shí)現(xiàn)了接口定義的方法。如果我們將容納的對(duì)象替換成另一個(gè)對(duì)象,那不就可以完成上一節(jié)我們沒有完成的多態(tài)功能了么?好,順著這個(gè)思路,下面我們就來模擬一下多態(tài)

package main

import "fmt"

type Fruitable interface {
    eat()
}

type Fruit struct {
    Name string  // 屬性變量
    Fruitable  // 匿名內(nèi)嵌接口變量
}

func (f Fruit) want() {
    fmt.Printf("I like ")
    f.eat() // 外結(jié)構(gòu)體會(huì)自動(dòng)繼承匿名內(nèi)嵌變量的方法
}

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

使用這種方式模擬多態(tài)本質(zhì)上是通過組合屬性變量(Name)和接口變量(Fruitable)來做到的,屬性變量是對(duì)象的數(shù)據(jù),而接口變量是對(duì)象的功能,將它們組合到一塊就形成了一個(gè)完整的多態(tài)性的結(jié)構(gòu)體。

接口的組合繼承

接口的定義也支持組合繼承,比如我們可以將兩個(gè)接口定義合并為一個(gè)接口如下

type Smellable interface {
  smell()
}

type Eatable interface {
  eat()
}

type Fruitable interface {
  Smellable
  Eatable
}

這時(shí) Fruitable 接口就自動(dòng)包含了 smell() 和 eat() 兩個(gè)方法,它和下面的定義是等價(jià)的。

type Fruitable interface {
  smell()
  eat()
}

接口變量的賦值

變量賦值本質(zhì)上是一次內(nèi)存淺拷貝,切片的賦值是拷貝了切片頭,字符串的賦值是拷貝了字符串的頭部,而數(shù)組的賦值呢是直接拷貝整個(gè)數(shù)組。接口變量的賦值會(huì)不會(huì)不一樣呢?接下來我們做一個(gè)實(shí)驗(yàn)

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}

從上面的輸出結(jié)果中可以推斷出結(jié)構(gòu)體的內(nèi)存發(fā)生了復(fù)制,這個(gè)復(fù)制可能是因?yàn)橘x值(a = r)也可能是因?yàn)轭愋娃D(zhuǎn)換(rx = a.(Rect)),也可能是兩者都進(jìn)行了內(nèi)存復(fù)制。那能不能判斷出究竟在接口變量賦值時(shí)有沒有發(fā)生內(nèi)存復(fù)制呢?不好意思,就目前來說我們學(xué)到的知識(shí)點(diǎn)還辦不到。到后面的高級(jí)階段我們將會(huì)使用 unsafe 包來洞悉其中的更多細(xì)節(jié)。不過我可以提前告訴你們答案是什么,那就是兩者都會(huì)發(fā)生數(shù)據(jù)內(nèi)存的復(fù)制 —— 淺拷貝。

指向指針的接口變量

如果將上面的例子改成指針,將接口變量指向結(jié)構(gòu)體指針,那結(jié)果就不一樣了

package main

import "fmt"

type Rect struct {
    Width int
    Height int
}

func main() {
    var a interface {}
    var r = Rect{50, 50}
    a = &r // 指向了結(jié)構(gòu)體指針

    var rx = a.(*Rect) // 轉(zhuǎn)換成指針類型
    r.Width = 100
    r.Height = 100
    fmt.Println(rx)
}

-------
{100 100}

從輸出結(jié)果中可以看出指針變量 rx 指向的內(nèi)存和變量 r 的內(nèi)存是同一份。因?yàn)樵陬愋娃D(zhuǎn)換的過程中只發(fā)生了指針變量的內(nèi)存復(fù)制,而指針變量指向的內(nèi)存是共享的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容