interface引發的事件真相

(PS:專欄所有的代碼都是基於go version go1.8 darwin/amd64)

流動的水沒有形狀,漂流的風找不到蹤跡,一切代碼都瞭然於心,我們在寫代碼的時候,總是有一種思維定式陪伴左右,在對事物做判斷的時候,往往這種思維定式會往正向或反向做推動作用,在開發的過程中如果不小心忽略,往往就是埋下了陷阱,以下代碼是大多數新手會遇到的坑,

package mainnnimport (nt"fmt"n)nntype People interface {ntName() stringn}ntype Student struct{ name string }nnfunc (stu *Student) Name() string {ntreturn stu.namen}nnfunc getPeople() People {ntvar stu *Studentntreturn stun}nnfunc main() {ntif getPeople() == nil {nttfmt.Println("AAAAA")nt} else {nttfmt.Println("BBBBB")nt}n}n

上面的代碼輸出什麼那?有些人會認為列印AAAAA,因為他們會認為getPeople方法裡面stu是nil 所以返回的就是nil,這樣想就大錯特錯,因為雖然返回的stu是nil 但是函數返回時People介面的結構的本身並不是nil,在我們不了解interface內部結構之前請往下看。

為什麼我會選擇去寫一個關於interface的文章那,我認為他在go語言裡面有這非常重要的地位,僅次於goroutine和channel的地位,我在未接觸go之前一直從事於c#的開發,介面對我來說就是不同組件之間的契約,對這個契約強制你必須去繼承介面,而go語言的設計就非常輕巧,只要實現了介面所要求的所有函數即可,go中的介面分為兩種一種是空的介面類似這樣:

var in interface{}n

例外一種是非空的介面即在介面內部聲明了一些方法:

type People interface {ntName() stringn}n

接下來我就根據上面的例子來對比一下空介面和非空介面內部結構

type eface struct { //空介面nt_type *_type //類型信息ntdata unsafe.Pointer //指向數據的指針(go語言中特殊的指針類型unsafe.Pointer類似於c語言中的void*)n}ntype iface struct { //帶有方法的介面nttab *itab //存儲type信息還有結構實現方法的集合ntdata unsafe.Pointer //指向數據的指針(go語言中特殊的指針類型unsafe.Pointer類似於c語言中的void*)n}n

eface包含一個類型信息,可以為reflect提供幫助

type _type struct {ntsize uintptr //類型大小ntptrdata uintptr //前綴持有所有指針的內存大小nthash uint32 //數據hash值nttflag tflag ntalign uint8 //對齊ntfieldalign uint8 //嵌入結構體時的對齊ntkind uint8 //kind 有些枚舉值kind等於0是無效的ntalg *typeAlg //函數指針數組,類型實現的所有方法ntgcdata *bytentstr nameOffntptrToThis typeOffn}n

iface比eface 中間多了一層itab結構

type itab struct {ntinter *interfacetype //介面類型nt_type *_type //結構類型ntlink *itab ntbad int32ntinhash int32 ntfun [1]uintptr //可變大小 方法集合n}n

itab 存儲_type信息和[]fun方法集,從上面的結構我們就可得出,因為data指向了nil 並不代表interface 是nil,所以返回值並不為空,這裡的fun(方法集)定義了介面的接收規則,在編譯的過程中需要驗證是否實現介面,介面的具體細節你可以閱讀Go Data Structures: Interfaces

接下來是第二個例子:

package mainnnimport (nt"fmt"n)nntype People interface {ntSpeak(string) stringn}nntype Stduent struct{}nnfunc (stu *Stduent) Speak(think string) (talk string) {ntif think == "bitch" {ntttalk = "You are a good boy"nt} else {ntttalk = "hi"nt}ntreturnn}nnfunc main() {ntvar peo People = Stduent{}ntthink := "bitch"ntfmt.Println(peo.Speak(think))n}n

上面的代碼是不能編譯過去的,會提示沒有實現該介面,只要我們把var peo People = Stduent{}修改為var peo People = &Stduent{}就可以了,為什麼會有這種限制,

這是因為介面定義不規定實現者是否應該使用指針接收還是值接收實現介面。當使用介面時,不能保證底層類型是值還是指針。我們上面的例子中,我們定義了指針接受方法,修改為值接受方法:

func (stu Stduent) Speak(think string) (talk string) {ntif think == "bitch" {ntttalk = "You are a good boy"nt} else {ntttalk = "hi"nt}ntreturnn}n

我們再次運行列印:

You are a good boyn

通過上面測試我們得出一個結論使用值傳遞方法,介面賦值使用var peo People = Stduent{}或者var peo People = &Stduent{},如果使用指針作為參數傳遞,則只能使用var peo People = &Stduent{},正是由於interface的靈活性,可以使用golang實現多態的特性,所以我們更要對interface多做深入了解。(本文未來可能會做一些細微的調整)

推薦閱讀:

寫c++好久不用new了,這是好的習慣嗎?
Git筆記——基本功能(上)

TAG:Go语言 | golang最佳实践 | 编程 |