Golang - 深入理解 interface 常見用法
此篇文章介紹在 Golang 中 interface 的常見用法,interface 在 Golang 中是一個很重要的環節。interface 可以拿來實現多種用途,請看介紹。
interface 定義
interface 又稱接口,其實功能有點類似於 Java 中的 interface,但是在一些地方完全不同於 Java 中 interface 的設計。
在 Golang 中,interface 其中一個功能就是可以使用 interface 定義行為,也就是說 interface 中可以定義一些方法來表示一個對象的行為,而當我們有自定義的型態假設想要擁有這些行為,就是去實踐 interface 裡面的方法。
interface 定義行為
來看個例子:
1 | package main |
- 建立一個 Animal 型態的 interface,其定義了 Eat () 跟 Run (),來表達動物都會擁有的行為。
- 建立一個 Dog Struct,並且實踐 interface 裡面的 Eat () 跟 Run ()。要注意的是,由於在實作
Eat
與Run
方法時,都是用指標(d *Dog)
,這個所代表的意思是透過傳遞指標來操控同一個 Struct 實例,如果沒有用指標則會導致淺複製的行為,並不是操控同一個 Struct。 - 建立 ShowEat、ShowRun 方法,並且參數型態用 Animal。
以上的程式碼可以得知以下兩件事情:
- 在 Golang 中如果自定義型態實現了 interface 的所有方法,那麼它就會認定該自定義型態也是 interface 型態的一種。也就是所謂的鴨子型別 (Duck typing) 的實現。只要你有符合這些種種的行為,即使你不是真的鴨子,那麼還是會認定你是一隻鴨子。
- 透過 ShowRun 跟 ShowEat () 得知實現了多型的行為。所謂多型的意思是相同的訊息給予不同的物件會引發不同的動作,因為參數型態用 Animal 所以,每個動物會有各自的吃跟跑的行為,執行出來的結果也會各自不一樣。
interface 型態與值
將 main 裡面程式碼改成這樣:
1 | func main() { |
這樣輸出會是 nil。
-
這代表著
animal
在底層儲存的型態為nil
。interface 類型默認是一個指針 (引用類型),如果沒有對 interface 初始化就使用,那麼會輸出nil
。 -
但是我們可以指定自定義型態給 nil interface,如果該自定義型態有實現該 interface 方法即可。
1
2
3
4
5func main() {
var animal Animal
animal = &Dog{Name:"Kenny"}
fmt.Println(animal)
}運行結果為
&{Kenny}
。也就是說
animal
底層儲存的型態會*Dog
,而值是Dog
結構實例的位址值。
interface 繼承
一個自定義型態是可以實現多個 interface 的。此外,interface 也可以繼承別的 interface 的行為:
1 | package main |
在 Animal interface 透過內嵌的方式,將 Eater interface、Runner interface 定義的行為放進去。
這樣的話 Dog Struct 必須都實現 Eat () 跟 Run () 才能是 Animal 的一種。此外,因為這樣做也代表,Dog Struct 也是 Eater 及 Runner 的一種。
所以看到定義的 ShowEat2 () 跟 ShowRun2 () 皆能接受 Dog Struct。
透過 interface 儲存異質陣列或 slice
前面說過 Golang 會檢查類型的實例,是否都有實現 interface 定義的行為,如果是的話就可以接受介面型態是不同型態實例的指定。
透過這種特性,假設我們有個需求是一個陣列或 slice 存放的型態無法事先確定,且每個元素的型態可能都不是一樣,就可以透過 interface 來解決!
1 | package main |
在這邊多定義了 Cat Struct,並且同樣實現了 Animal interface。
因此在前面可以建立 Animal 型態的陣列,裡面可以放不同結構體的實例,只要裡面放置的結構體有實現 Animal interface 行為,就會被當作 Animal 實例。
而第二個例子是利用空接口型態,裡面可以放置各種型態的元素,以這個例子來看既能放 int、string、Dog Struct、Cat Struct。
也因為空接口的特性也是實現泛型的重要關鍵。
型態斷言
但是根據以上的例子會發現一個問題:
假設利用自定義型態為 Animal interface 指定型態的話,該型態就能存取 interface 的行為,並不能存取自定義型態的屬性及其它自定義型態的方法。
這時候可以利用 Golang 提供的型態斷言的特性,請看:
1 | func main() { |
透過.(type)
的方式來斷定該接口實際上是存放哪個實例。但是有個缺點是如果型態判斷錯誤,會直接造成 panic。
出現以下錯誤訊息:
1 | panic: interface conversion: main.Animal is *main.Cat, not *main.Dog |
這是因為後面的 animal 是指定 Cat 實例,結果後面型態斷言用 Dog,會造成執行時期的錯誤。
要怎麼避免呢?
可以透過 switch 的方式一一去判斷型態:
1 | func main() { |
透過.(type)
來一一比對出對的型態。
此外,Golang 型態斷言也提供了檢測機制:
1 | func main() { |
當然了,如果斷言的形態越多用 switch 相對可讀性會較高。
空 interface 的限制
根據以上的例子可以空接口提供很多便利性,但是也有其限制:
一個空的接口會隱藏值對應的表示方式和所有的公開的方法,必須使用類型斷言才能來來訪問內部的值,如果事先不知道空接口指向的值的具體類型,就無法操作。
為此,才需要 reflect 機制,可以知道一個接口類型的變量具體是什麼(什麼類型),有什麼能力(有哪些方法)。這也是在寫 Golang 程式庫常常會用到的特性,因為有 interface 可以實現泛型的特性,有了泛型的特性又可以透過 reflect 機制來促發其不同型態的屬性及方法。
總結
- Golang interface 重點是「行為」,不管定義的介面型態是什麼,只要行為符合就屬於該介面型態的一種。
- Golang interface 可以說是動態語言鴨子型別的展現。
- 利用 interface 可實現泛型、多型的功能,從而可以調用同一個函數名的函數但實現完全不同的功能。
所以根據以上 interface 的特點,在看看 Golang 的標準程式庫裡面運用大量的 interface 的特性來完成,例如標準程式庫定義檔案讀寫的 Reader、Writer interface:
1 | type Reader interface { |
利用 os.File
實現了 Reader、Writer interface,來實作檔案讀寫的實現。
下篇文章帶來 reflect 介紹以及如何搭配 interface!
最後最後!請聽我一言!
如果你還沒有註冊 Like Coin,你可以在文章最下方看到 Like 的按鈕,點下去後即可申請帳號,透過申請帳號後可以幫我的文章按下 Like,而 Like 最多可以點五次,而你不用付出任何一塊錢,就能給我寫這篇文章的最大的回饋!