1. 程式人生 > 其它 >struct interface_golang 避坑指南(1)interface 之坑多多 | 文末深圳 Meetup

struct interface_golang 避坑指南(1)interface 之坑多多 | 文末深圳 Meetup

技術標籤:struct interface

點選上方藍色“Go語言中文網”關注我們,領全套Go資料,每天學習Go語言

interface{}和void*/Object 是一樣的嗎?

先來看一段關於 interface 的官方說明

Under the covers, interfaces are implemented as two elements, a type and a value. The value, called the interface’s dynamic value, is an arbitrary concrete value and the type is that of the value. For the int value 3, an interface value contains, schematically, (int, 3).

事實上,interface 的實現分為兩種 eface,iface,它們結構如下:

type iface struct {
tab *itab
data unsafe.Pointer
}

type eface struct {
_type *_type
data unsafe.Pointer
}

iface 為有方法宣告的 interface,eface 為空的 interface 即 interface{}。我們可以看到 eface 結構體中只存兩個指標:一個_type 型別指標用於存資料的實際型別,一個通用指標(unsafe.Pointer)存實際資料;iface 則比較複雜,這裡不做詳細展開了,你只需要記住它也是兩個指標,和 eface 一樣其中一個用來存資料,另一個 itab 指標用來存資料型別以及方法集。因此 interface 型別的變數所佔空間一定為 16。

明白了原理,我們看一段簡單程式碼:

example1

type Student struct {
Name string
}
var b interface{} = Student{
Name: "aaa",
}
var c = b.(Student)
c.Name = "bbb"
fmt.Println(b.(Student).Name)

你覺得輸出是什麼?

如果你的答案是 bbb,恭喜你,你掉坑了!不信你執行試試:

//example1 output
aaa

這個坑道理很簡單,根據我們開頭講的 interface 原理,很多從 java 或 c/c++轉過來的程式設計師都把 interface{}看成了 Object 或`void*``。

的確,很多場景(如傳參、返回值)它們的確很類似;但請注意,在底層實現上它們是完全不同的。java 的 Object 以及 c 語言的void*可以通過通過強轉為某個型別獲取指向原資料的一個目標型別的引用或指標,因此如果在這個引用或指標上進行修改操作,原資料也會被修改;但是 golang 的 interface 和具體型別之間的轉換、賦值是將實際資料複製了一份進行操作的。例如上例中的

var c  = b.(Student)

實際的過程是首先將 b 指向的資料複製一份,然後轉換為 Student 型別賦值給 c。

記住了嗎?好,我們看個類似的例子:

example2

type Student struct {
Name string
}
a := Student{Name:"aaa"}
var b interface{} = a
a.Name = "bbb"
fmt.Println(b.(Student).Name)

這次,輸出結果又會是什麼?

如果你給出的答案是 aaa,恭喜你脫坑了!

遇到 interface 類的返回值你要注意了

看個簡短的例子:

example3

func GetReader(id int64) io.Reader {
var r *MyReader = nil
if id > 0 && id < 10000{
r = openReader(id)
}
return r
}
func main() {
r := GetReader(-2)
if r == nil {
fmt.Println("bad reader")
} else {
fmt.Println("valid reader")
}
}

其中 MyReader 為某個實現了 io.Reader 的結構體型別,openReader 根據傳入引數返回一個 MyReader 結構體指標。

你覺得這段程式會輸出什麼呢?會輸出"bad reader"嗎?

答案是剛好相反:

//example3 output
valid reader

為了解釋這個結果,我們再看兩段簡單的程式碼:

example4

var b interface{} = nil
fmt.Println(b == nil)

example5

var a *Student = nil
var b interface{} = a
fmt.Println(b == nil)

輸出分別為:

//example4 output
true

//example5 output
false

相信仔細對比過後,你應該已經有答案了:當一個指標賦值給 interface 型別時,無論此指標是否為 nil,賦值過的 interface 都不為 nil

ok,結論已經有了,那為什麼是這樣呢?還記得本文開頭介紹的 interface 底層實現嗎?無論是 iface 還是 eface,都有兩個指標,指向資料的是通用指標,還有一個指標用於指定資料型別或方法集;當我們將一個 nil 指標賦值給 interface 時,實際是對 interface 的這兩個指標分別賦值,雖言資料指標 data 為 nil,但是型別指標_type 或 tab 並不是 nil,他將指向你的空指標的型別,因此賦值的結果 interface 肯定不是 nil 啦!

什麼?interface 還能嵌入 struct?

眾所周知,一個新定義的 type 要想實現某個 interface,一定需要將該 interface 的所有方法都實現一遍。對嗎?

老規矩,先上例子:

example6

type Talkable interface {
TalkEnglish(string)
TalkChinese(string)
}

type Student1 struct {
Talkable
Name string
Age int
}

func main(){
a := Student1{Name: "aaa", Age: 12}
var b Talkable = a
fmt.Println(b)
}

以上的程式碼時 100%能編譯執行的。輸出為:

// example6 output
{ aaa 12}

這是一種取巧的方法,將 interface 嵌入結構體,可以使該型別快速實現該 interface。所以,本小節開頭的話並不成立。但是如果我們調一下方法呢?

example7

...
func main(){
a := Student1{Name: "aaa", Age: 12}
a.TalkEnglish("nice to meet you\n")
}

可以預見到的,報錯了:

//example7 output
panic: runtime error: invalid memory address or nil pointer dereference

並沒有實現 interface 的方法,當然會報錯。我們可以只實現 interface 的一部分方法,比如我只需要用到 Talkable 的 TalkEnglish 方法:

func (s *Student1) TalkEnglish(s1 string) {
fmt.Printf("I'm %s,%d years old,%s", s.Name, s.Age, s1)
}

或者只需要講中文:

func (s *Student1) TalkChinese(s1 string) {
fmt.Printf("我是 %s, 今年%d歲,%s", s.Name, s.Age, s1)
}

總而言之,嵌入 interface 的好處就是可以幫在整體型別相容某個介面的前提下,允許你你針對你的應用場景只實現 interface 中的一部分方法。但是在使用時要注意沒有實現的方法在呼叫時會 panic。

總結

interface 時 golang 程式設計中使用得非常頻繁的特性,我們需要明白它的底層結構,以及一些編譯和執行時的特殊之處,能幫我們避免一些不必要的麻煩:

  • interface 很類似void*,但在值型別的變數和 interface 型別變數相互賦值時,會發生資料的複製。
  • 將某個型別的指標賦值給 interface,interface 的值永遠不可能是 nil;
  • interface 可以嵌入結構體,幫型別快速實現介面,但是注意如果呼叫未實現的方法則會 panic;

12 月 15 日Go語言中文網深圳Meetup,免費報名

78440c23-e318-eb11-8da9-e4434bdf6706.png

報名方式點選底部閱讀原文