Golang - 深入理解 interface 常見用法

此篇文章介紹在 Golang 中 interface 的常見用法,interface 在 Golang 中是一個很重要的環節。interface 可以拿來實現多種用途,請看介紹。

interface 定義

interface 又稱接口,其實功能有點類似於 Java 中的 interface,但是在一些地方完全不同於 Java 中 interface 的設計。

在 Golang 中,interface 其中一個功能就是可以使用 interface 定義行為,也就是說 interface 中可以定義一些方法來表示一個對象的行為,而當我們有自定義的型態假設想要擁有這些行為,就是去實踐 interface 裡面的方法。

interface 定義行為

來看個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import "fmt"

type Animal interface {
Eat()
Run()
}

type Dog struct {
Name string
}

func (d *Dog) Eat() {
fmt.Printf("%s is eating\n", d.Name)
}

func (d *Dog) Run() {
fmt.Printf("%s is running\n", d.Name)
}

func ShowEat(animal Animal) {
animal.Eat()
}

func ShowRun(animal Animal) {
animal.Run()
}

func main() {
dog := Dog{Name:"Kenny"}
ShowEat(&dog)
ShowRun(&dog)
}
  1. 建立一個 Animal 型態的 interface,其定義了 Eat () 跟 Run (),來表達動物都會擁有的行為。
  2. 建立一個 Dog Struct,並且實踐 interface 裡面的 Eat () 跟 Run ()。要注意的是,由於在實作 EatRun 方法時,都是用指標 (d *Dog) ,這個所代表的意思是透過傳遞指標來操控同一個 Struct 實例,如果沒有用指標則會導致淺複製的行為,並不是操控同一個 Struct。
  3. 建立 ShowEat、ShowRun 方法,並且參數型態用 Animal。

以上的程式碼可以得知以下兩件事情:

  1. 在 Golang 中如果自定義型態實現了 interface 的所有方法,那麼它就會認定該自定義型態也是 interface 型態的一種。也就是所謂的鴨子型別 (Duck typing) 的實現。只要你有符合這些種種的行為,即使你不是真的鴨子,那麼還是會認定你是一隻鴨子。
  2. 透過 ShowRun 跟 ShowEat () 得知實現了多型的行為。所謂多型的意思是相同的訊息給予不同的物件會引發不同的動作,因為參數型態用 Animal 所以,每個動物會有各自的吃跟跑的行為,執行出來的結果也會各自不一樣。

interface 型態與值

將 main 裡面程式碼改成這樣:

1
2
3
4
func main() {
var animal Animal
fmt.Println(animal)
}

這樣輸出會是 nil。

  1. 這代表著 animal 在底層儲存的型態為 nil。interface 類型默認是一個指針 (引用類型),如果沒有對 interface 初始化就使用,那麼會輸出 nil

  2. 但是我們可以指定自定義型態給 nil interface,如果該自定義型態有實現該 interface 方法即可。

    1
    2
    3
    4
    5
    func main() {
    var animal Animal
    animal = &Dog{Name:"Kenny"}
    fmt.Println(animal)
    }

    運行結果為 &{Kenny}

    也就是說 animal 底層儲存的型態會 *Dog,而值是 Dog 結構實例的位址值。

interface 繼承

一個自定義型態是可以實現多個 interface 的。此外,interface 也可以繼承別的 interface 的行為:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import "fmt"

type Eater interface {
Eat()
}

type Runner interface {
Run()
}

type Animal interface {
Eater
Runner
}

type Dog struct {
Name string
}

func (d *Dog) Eat() {
fmt.Printf("%s is eating\n", d.Name)
}

func (d *Dog) Run() {
fmt.Printf("%s is running\n", d.Name)
}

func ShowEat(animal Animal) {
animal.Eat()
}

func ShowRun(animal Animal) {
animal.Run()
}

func ShowEat2(eater Eater) {
eater.Eat()
}

func ShowRun2(runner Runner) {
runner.Run()
}

func main() {
dog := Dog{Name:"Kenny"}
ShowEat(&dog)
ShowRun(&dog)
ShowEat2(&dog)
ShowRun2(&dog)
}

在 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package main

import "fmt"

type Eater interface {
Eat()
}

type Runner interface {
Run()
}

type Animal interface {
Eater
Runner
}

type Dog struct {
Name string
}

func (d *Dog) Eat() {
fmt.Printf("%s is eating\n", d.Name)
}

func (d *Dog) Run() {
fmt.Printf("%s is running\n", d.Name)
}

type Cat struct {
Name string
}

func (c *Cat) Eat() {
fmt.Printf("%s is eating\n", c.Name)
}

func (c *Cat) Run() {
fmt.Printf("%s is running\n", c.Name)
}

func ShowEat(animal Animal) {
animal.Eat()
}

func ShowRun(animal Animal) {
animal.Run()
}

func ShowEat2(eater Eater) {
eater.Eat()
}

func ShowRun2(runner Runner) {
runner.Run()
}

func main() {
animals := [...]Animal{
&Dog{Name:"Kenny"},
&Cat{Name:"Nicole"},
}

for _, animal := range animals {
fmt.Println(animal)
}

instances := [...]interface{}{
123,
"Hello World",
&Dog{Name:"Kenny"},
&Cat{Name:"Nicole"},
}

for _, instance := range instances {
fmt.Println(instance)
}
}

在這邊多定義了 Cat Struct,並且同樣實現了 Animal interface。

因此在前面可以建立 Animal 型態的陣列,裡面可以放不同結構體的實例,只要裡面放置的結構體有實現 Animal interface 行為,就會被當作 Animal 實例。

而第二個例子是利用空接口型態,裡面可以放置各種型態的元素,以這個例子來看既能放 int、string、Dog Struct、Cat Struct。

也因為空接口的特性也是實現泛型的重要關鍵。

型態斷言

但是根據以上的例子會發現一個問題:

假設利用自定義型態為 Animal interface 指定型態的話,該型態就能存取 interface 的行為,並不能存取自定義型態的屬性及其它自定義型態的方法。

這時候可以利用 Golang 提供的型態斷言的特性,請看:

1
2
3
4
5
6
7
8
9
10
func main() {
var animal Animal
animal = &Dog{Name:"Kenny"}
dog := animal.(*Dog)
fmt.Println(dog.Name)

animal = &Cat{Name:"Nicole"}
cat := animal.(*Dog)
fmt.Println(cat.Name)
}

透過.(type) 的方式來斷定該接口實際上是存放哪個實例。但是有個缺點是如果型態判斷錯誤,會直接造成 panic。

出現以下錯誤訊息:

1
panic: interface conversion: main.Animal is *main.Cat, not *main.Dog

這是因為後面的 animal 是指定 Cat 實例,結果後面型態斷言用 Dog,會造成執行時期的錯誤。

要怎麼避免呢?

可以透過 switch 的方式一一去判斷型態:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
animals := [...]Animal{
&Dog{Name:"Kenny"},
&Cat{Name:"Nicole"},
}

for _, animal := range animals {
switch animal.(type) {
case *Dog:
fmt.Println(animal.(*Dog).Name)
case *Cat:
fmt.Println(animal.(*Cat).Name)
default:
fmt.Println("you are not animal!!")
}
}
}

透過.(type) 來一一比對出對的型態。

此外,Golang 型態斷言也提供了檢測機制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
animals := [...]Animal{
&Dog{Name:"Kenny"},
&Cat{Name:"Nicole"},
}

for _, animal := range animals {
if dog, ok := animal.(*Dog); ok {
fmt.Println(dog.Name)
}

if cat, ok := animal.(*Cat); ok {
fmt.Println(cat.Name)
}
}
}

當然了,如果斷言的形態越多用 switch 相對可讀性會較高。

空 interface 的限制

根據以上的例子可以空接口提供很多便利性,但是也有其限制:

一個空的接口會隱藏值對應的表示方式和所有的公開的方法,必須使用類型斷言才能來來訪問內部的值,如果事先不知道空接口指向的值的具體類型,就無法操作。

為此,才需要 reflect 機制,可以知道一個接口類型的變量具體是什麼(什麼類型),有什麼能力(有哪些方法)。這也是在寫 Golang 程式庫常常會用到的特性,因為有 interface 可以實現泛型的特性,有了泛型的特性又可以透過 reflect 機制來促發其不同型態的屬性及方法。

總結

  • Golang interface 重點是「行為」,不管定義的介面型態是什麼,只要行為符合就屬於該介面型態的一種。
  • Golang interface 可以說是動態語言鴨子型別的展現。
  • 利用 interface 可實現泛型、多型的功能,從而可以調用同一個函數名的函數但實現完全不同的功能。

所以根據以上 interface 的特點,在看看 Golang 的標準程式庫裡面運用大量的 interface 的特性來完成,例如標準程式庫定義檔案讀寫的 Reader、Writer interface:

1
2
3
4
5
6
7
type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

利用 os.File 實現了 Reader、Writer interface,來實作檔案讀寫的實現。

下篇文章帶來 reflect 介紹以及如何搭配 interface!

最後最後!請聽我一言!

如果你還沒有註冊 Like Coin,你可以在文章最下方看到 Like 的按鈕,點下去後即可申請帳號,透過申請帳號後可以幫我的文章按下 Like,而 Like 最多可以點五次,而你不用付出任何一塊錢,就能給我寫這篇文章的最大的回饋!