Golang筆記-淺談interface

前言

classinterface在高級語言中是很重要的概念。class是對模型的定義和封裝,interface則是對行為的抽象和封裝。Go語言雖然沒有class,但是有structinterface,以另一種方式實現同樣的效果。

本文將談一談Go語言這與別不同的interface的基本概念和一些需要注意的地方。

聲明interface

type Birds interface {
    Twitter() string
    Fly(high int) bool
}

上面這段代碼聲明了一個名為Birds的接口類型(interface),這個接口包含兩個行為TwitterFly
Go語言里面,聲明一個接口類型需要使用type關鍵字、接口類型名稱、interface關鍵字和一組有{}括起來的方法聲明,這些方法聲明只有方法名、參數和返回值,不需要方法體。

Go語言沒有繼承的概念,那如果需要實現繼承的效果怎么辦?Go的方法是嵌入

type Chicken interface {
    Bird
    Walk()
}

上面這段代碼中聲明了一個新的接口類型Chicken,我們希望他能夠共用Birds的行為,于是直接在Chicken的接口類型聲明中,嵌入Birds接口類型,這樣Chicken接口中就有了原屬于BirdsTwitterFly這兩個行為以及新增加的Walk行為,實現了接口繼承的效果。

實現interface

在java中,通過類來實現接口。一個類需要在聲明通過implements顯示說明實現哪些接口,并在類的方法中實現所有的接口方法。Go語言沒有類,也沒有implements,如何來實現一個接口呢?這里就體現了Go與別不同的地方了。

首先,Go語言沒有類但是有struct,通過struct來定義模型結構和方法。

其次,Go語言實現一個接口并不需要顯示聲明,而是只要你實現了接口中的所有方法就認為你實現了這個接口。這稱之為Duck typing

如果它走起步來像鴨子,并且叫聲像鴨子, 那個它一定是一只鴨子.

說道這里,就需要介紹下struct如何實現方法。

type Sparrow struct {
    name string
}

func (s *Sparrow) Fly(hign int) bool {
    // ...
    return true
}

func (s *Sparrow) Twitter() string {
    // ...
    return fmt.Sprintf("%s,jojojo", s.name)
}

上面這段代碼,聲明了一個名為Sparrowstruct,下面聲明了兩個方法。不過這個方法的聲明行為可能略微有點奇怪。

比如func (s *Sparrow) Fly(hign int) bool中,func關鍵字用于聲明方法和函數,后面方法Fly以及參數和返回值。但是在func關鍵字和方法名Fly中間還有s *Sparraw的聲明,這個聲明在Go中稱之為接收者聲明,其中s代表這個方法的接收者,*Sparrow代表這個接收者的類型。

接收者的類型可以為一個數據類型的指針類型,也可以是數據類型本身,比如我們針對Sparrow再實現一個方法:

func (s Sparrow) Walk() {
    // ...
}

接收者為數據類型的方法稱為值方法,接收者為指針類型的方法稱之為指針方法。

這種非侵入式的接口實現方式非常的方便和靈活,不用去管理各種接口依賴,對開發人員來說也更簡潔。

使用interface

利用struct去實現接口之后,我們就可以用這個struct作為接口參數,使用那些接收接口參數的方法完成我們的功能。這也是面向接口編程的方式,我們的功能依據接口來實現,而不用關心實現接口的是什么,這樣大大提供了功能的通用性可擴展性。

func BirdAnimation(bird Birds, high int) {
    fmt.Printf("BirdAnimation of %T\n", bird)
    bird.Twitter()
    bird.Fly(high)
}

func main() {
    var bird Birds
    sparrow := &Sparrow{}
    bird = sparrow
    BirdAnimation(bird, 1000)
    // 或者將sparrow直接作為參數
    BirdAnimation(sparrow, 1000)
}

上面這段代碼中,我們聲明了一個Birds接口類型的變量bird,由于*Sparrow實現了Birds接口的所有方法,所以我們可以將*Sparrow類型的變量sparrow 賦值給bird。或者直接將sparrow作為參數調用BirdAnimation,運行結果如下:

?  go run main.go
BirdAnimation of *main.Sparrow
Sparrow Twitter
Sparrow Fly
BirdAnimation of *main.Sparrow
Sparrow Twitter
Sparrow Fly

深入一步interface

關于空interface

先看一段代碼,猜猜會輸出什么。

func NilInterfaceTest(chicken Chicken) {
    if chicken == nil {
        fmt.Println("Sorry,It’s Nil")
    } else {
        fmt.Println("Animation Start!")
        ChickenAnimation(chicken)
    }
}

func main() {
  var sparrow3 *Sparrow
  NilInterfaceTest(sparrow3)
}

我們聲明了一個*Sparrow的變量sparrow3,但是我們并沒有對其進行初始化,是一個nil值,然后我們直接將它作為參數調用NilInterfaceTest(),我們預期的結果是希望NilInterfaceTest方法檢測出nil值,避免出錯。然而實際結果是這樣的:

?  go run main.go
Animation Start!
ChickenAnimation of *main.Sparrow
panic: value method main.Sparrow.Walk called using nil *Sparrow pointer

goroutine 1 [running]:
...

NilInterfaceTest方法并沒有檢測到我們傳的是一個nil的sparrow,正常去使用最終導致了程序panic。

也許這里很讓人迷惑,其實這里應該認識到雖然我們可以將實現了接口所有方法的接收者當做接口來使用,但是兩者并不是完全等同。在Go語言中,interface的底層結構其實是比較復雜的,簡要來說,一個interface結構包含兩部分:1.這個接口值的類型;2.指向這個接口值的指針。我們稍微在NilInterfaceTest代碼中加點東西看看:

func NilInterfaceTest(chicken Chicken) {
    if chicken == nil {
        fmt.Println("Sorry,It’s Nil")
    } else {
        fmt.Println("Animation Start!")
        fmt.Printf("type:%v,value:%v\n", reflect.TypeOf(chicken), reflect.ValueOf(chicken))
        ChickenAnimation(chicken)
    }
}

我們增加了第6行的代碼,將bird變量的類型和值分別輸出,得到結果如下:

?  go run main.go
Animation Start!
type:*main.Sparrow,value:<nil>
ChickenAnimation of *main.Sparrow
panic: value method main.Sparrow.Walk called using nil *Sparrow pointer
...

我們可以看到bird的type為*main.Sparrow,而value為nil。也就是說,我們將一個nil的*Sparrow賦值給bird后,這個bird的type部分就已經有值了,只不過他的value部分是nil,所以bird并不是nil

關于方法列表

再看一段代碼:

func ChickenAnimation(chicken Chicken) {
    fmt.Printf("ChickenAnimation of %T\n", chicken)
    chicken.Walk()
    chicken.Twitter()
}

func main() {
    var chicken Chicken
    sparrow2 := Sparrow{}
    chicken = sparrow2
    ChickenAnimation(chicken)
}

其運行結果如下:

?  go run main.go
# command-line-arguments
./main.go:70:10: cannot use sparrow2 (type Sparrow) as type Chicken in assignment:
        Sparrow does not implement Chicken (Fly method has pointer receiver)

編譯器編譯報錯,它說Sparrow并沒有實現Chicken接口,因為Fly方法的接受者是指針接收者,而我們給的是Sparrow

我們將程序做一點小小的調整就可以了,將第10行代碼修改為:

chicken = &sparrow2

也許你會問:"Chicken接口的Walk方法的接收者是非指針的Sparrow,我們把*Sparrow賦值給Chicken接口變量為什么可以通過?"。

這里就要講到方法列表的概念。

首先,一個指針類型的方法列表必然包含所有接收者為指針接收者的方法,同理非指針類型的方法列表也包含所有接收者為非指針類型的方法。在我們例子中*Sparrow首先包含:FlyTwitterSparrow包含Walk

其次,當我們擁有一個指針類型的時候,因為有了這個變量的地址,我們得到這個具體的變量,所以一個指針類型的方法列表還可以包含其非指針類型作為接收者的方法。在我們的例子中就是*Sparrow的方法列表為:FlyTwitterWalk,所以chicken = &sparrow2可以通過。

但是一個非指針類型卻并不總是能取到它的地址,從而獲取它接收者為指針接收者的方法。所以非指針類型的方法列表中只有接收者為非指針類型的方法。如果它的方法列表不能完全覆蓋這個接口,是不算實現了這個接口的。

舉個簡單的例子:

type TestInt int

func main() {
  &TestInt(7)
}

編譯報錯,無法取址:

?  go run main.go
# command-line-arguments
./main.go:77:2: cannot take the address of TestInt(7)
./main.go:77:2: &TestInt(7) evaluated but not used

又或者:

func main() {
    sparrow4 := Sparrow{}
    sparrow4.Twitter()
}

這樣可以正常運行,但是稍微改改:

func main() {
    Sparrow{}.Twitter()
}

則編譯報錯:

?  go run main.go
# command-line-arguments
./main.go:80:11: cannot call pointer method on Sparrow literal
./main.go:80:11: cannot take the address of Sparrow literal

字面量也無法取址。
因此在使用接口時,我們要注意不同類型的方法列表,是否實現接口。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容