Go 專欄|介面 interface

語言: CN / TW / HK

原文連結: Go 專欄|介面 interface

Duck Typing,鴨子型別,在維基百科裡是這樣定義的:

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.

翻譯過來就是:如果某個東西長得像鴨子,游泳像鴨子,嘎嘎叫像鴨子,那它就可以被看成是一隻鴨子。

它是動態程式語言的一種物件推斷策略,它更關注物件能做什麼,而不是物件的型別本身。

例如:在動態語言 Python 中,定義一個這樣的函式:

def hello_world(duck):
    duck.say_hello()

當呼叫此函式的時候,可以傳入任意型別,只要它實現了 say_hello() 就可以。如果沒實現,執行過程中會出現錯誤。

Go 語言作為一門靜態語言,它通過介面的方式完美支援鴨子型別。

介面型別

之前介紹的型別都是具體型別,而介面是一種抽象型別,是多個方法宣告的集合。在 Go 中,只要目標型別實現了介面要求的所有方法,我們就說它實現了這個介面。

先來看一個例子:

package main

import "fmt"

// 定義介面,包含 Eat 方法
type Duck interface {
	Eat()
}

// 定義 Cat 結構體,並實現 Eat 方法
type Cat struct{}

func (c *Cat) Eat() {
	fmt.Println("cat eat")
}

// 定義 Dog 結構體,並實現 Eat 方法
type Dog struct{}

func (d *Dog) Eat() {
	fmt.Println("dog eat")
}

func main() {
	var c Duck = &Cat{}
	c.Eat()

	var d Duck = &Dog{}
	d.Eat()

	s := []Duck{
		&Cat{},
		&Dog{},
	}
	for _, n := range s {
		n.Eat()
	}
}

使用 type 關鍵詞定義介面:

type Duck interface {
	Eat()
}

介面包含了一個 Eat() 方法,然後定義兩個結構體型別 CatDog,分別實現了 Eat 方法。

// 定義 Cat 結構體,並實現 Eat 方法
type Cat struct{}

func (c *Cat) Eat() {
	fmt.Println("cat eat")
}

// 定義 Dog 結構體,並實現 Eat 方法
type Dog struct{}

func (d *Dog) Eat() {
	fmt.Println("dog eat")
}

遍歷介面切片,通過介面型別可以直接呼叫對應方法:

s := []Duck{
	&Cat{},
	&Dog{},
}
for _, n := range s {
	n.Eat()
}

// 輸出
// cat eat
// dog eat

介面賦值

介面賦值分兩種情況:

  1. 將物件例項賦值給介面
  2. 將一個介面賦值給另一個介面

下面來分別說說:

將物件例項賦值給介面

還是用上面的例子,因為 Cat 實現了 Eat 介面,所以可以直接將 Cat 例項賦值給介面。

var c Duck = &Cat{}
c.Eat()

在這裡一定要傳結構體指標,如果直接傳結構體會報錯:

var c Duck = Cat{}
c.Eat()
# command-line-arguments
./09_interface.go:25:6: cannot use Cat{} (type Cat) as type Duck in assignment:
	Cat does not implement Duck (Eat method has pointer receiver)

但是如果反過來呢?比如使用結構體來實現介面,使用結構體指標來賦值:

// 定義 Cat 結構體,並實現 Eat 方法
type Cat struct{}

func (c Cat) Eat() {
	fmt.Println("cat eat")
}

var c Duck = &Cat{}
c.Eat() // cat eat

沒有問題,可以正常執行。

將一個介面賦值給另一個介面

還是上面的例子,可以直接將 c 的值直接賦值給 d

var c Duck = &Cat{}
c.Eat()

var d Duck = c
d.Eat()

再來,我再定義一個介面 Duck1,這個介面包含兩個方法 EatWalk,然後結構體 Dog 實現兩個方法,但是 Cat 只實現 Eat 方法。

type Duck1 interface {
	Eat()
	Walk()
}

// 定義 Dog 結構體,並實現 Eat 方法
type Dog struct{}

func (d *Dog) Eat() {
	fmt.Println("dog eat")
}

func (d *Dog) Walk() {
	fmt.Println("dog walk")
}

那麼在賦值時,使用 Duck1 賦值給 Duck 是可以的,反過來就會報錯。

var c1 Duck1 = &Dog{}
var c2 Duck = c1
c2.Eat()

所以,已經初始化的介面變數 c1 直接賦值給另一個介面變數 c2,要求 c2 的方法集是 c1 的方法集的子集。

空介面

具有 0 個方法的介面稱為空介面,它表示為 interface {}。由於空介面有 0 個方法,所以所有型別都實現了空介面。

func main() {
	// interface 形參
	s1 := "Hello World"
	i := 50
	strt := struct {
		name string
	}{
		name: "AlwaysBeta",
	}
	test(s1)
	test(i)
	test(strt)
}

func test(i interface{}) {
	fmt.Printf("Type = %T, value = %v\n", i, i)
}

型別斷言

型別斷言是作用在介面值上的操作,語法如下:

x.(T)

其中 x 是介面型別的表示式,T 是斷言型別。

作用是判斷運算元的動態型別是否滿足指定的斷言型別。

有兩種情況:

  1. T 是具體型別
  2. T 是介面型別

下面來分別舉例說明:

具體型別

型別斷言會檢查 x 的動態型別是否為 T,如果是,則輸出 x 的值;如果不是,程式直接 panic

func main() {
	// 型別斷言
	var n interface{} = 55
	assert(n) // 55
	var n1 interface{} = "hello"
	assert(n1) // panic: interface conversion: interface {} is string, not int
}

func assert(i interface{}) {
	s := i.(int)
	fmt.Println(s)
}

介面型別

型別斷言會檢查 x 的動態型別是否滿足介面型別 T,如果滿足,則輸出 x 的值,這個值可能是繫結例項的副本,也可能是指標的副本;如果不滿足,程式直接 panic

func main() {
	// 型別斷言
	assertInterface(c) // &{}
}

func assertInterface(i interface{}) {
	s := i.(Duck)
	fmt.Println(s)
}

如果有兩個接收值,那麼斷言不會在失敗時崩潰,而是會多返回一個布林值,一般命名為 ok,來表示斷言是否成功。

func main() {
	// 型別斷言
	var n1 interface{} = "hello"
	assertFlag(n1)
}

func assertFlag(i interface{}) {
	if s, ok := i.(int); ok {
		fmt.Println(s)
	}
}

型別查詢

語法類似型別斷言,只需將 T 直接用關鍵詞 type 替代。

作用主要有兩個:

  1. 查詢一個介面變數繫結的底層變數型別
  2. 查詢一個介面變數的底層變數是否還實現了其他介面
func main() {
	// 型別查詢
	SearchType(50)         // Int: 50
	SearchType("zhangsan") // String: zhangsan
	SearchType(c)          // dog eat
	SearchType(50.1)       // Unknown type
}

func SearchType(i interface{}) {
	switch v := i.(type) {
	case string:
		fmt.Printf("String: %s\n", i.(string))
	case int:
		fmt.Printf("Int: %d\n", i.(int))
	case Duck:
		v.Eat()
	default:
		fmt.Printf("Unknown type\n")
	}
}

總結

本文從鴨子型別引出 Go 的介面,然後用一個例子簡單展示了介面型別的用法,接著又介紹了介面賦值,空介面,型別斷言和型別查詢。

相信通過本篇文章大家能對介面有了整體的概念,並掌握了基本用法。


文章中的腦圖和原始碼都上傳到了 GitHub,有需要的同學可自行下載。

地址: http://github.com/yongxinz/gopher/tree/main/sc

Go 專欄文章列表:

  1. 開發環境搭建以及開發工具 VS Code 配置

  2. 變數和常量的宣告與賦值

  3. 基礎資料型別:整數、浮點數、複數、布林值和字串

  4. 複合資料型別:陣列和切片 slice

  5. 複合資料型別:字典 map 和 結構體 struct

  6. 流程控制,一網打盡

  7. 函式那些事

  8. 錯誤處理:defer,panic 和 recover

  9. 說說方法