首頁 > 軟體

Golang介面使用教學詳解

2022-10-02 14:00:38

前言

go語言並沒有物件導向的相關概念,go語言提到的介面和java、c++等語言提到的介面不同,它不會顯示的說明實現了介面,沒有繼承、子類、implements關鍵詞。

一、概述

在 Go 語言中介面包含兩種含義:它既是方法的集合, 同時還是一種型別。在Go 語言中是隱式實現的,意思就是對於一個具體的型別,不需要宣告它實現了哪些介面,只需要提供介面所必需的方法。

go語言通過隱性的方式實現了介面功能,相對比較靈活。

Go語言介面的特點

  • interface 是方法或行為宣告的集合
  • interface介面方式實現比較隱性,任何型別的物件實現interface所包含的全部方法,則表明該型別實現了該介面。
  • interface還可以作為一種通用的型別,其他型別變數可以給interface宣告的變數賦值。
  • interface 可以作為一種資料型別,實現了該介面的任何物件都可以給對應的介面型別變數賦值。

二、介面型別

2.1 介面的定義

每個介面型別由任意個方法簽名組成,介面的定義格式如下:

type 介面型別名 interface{
    方法名1( 參數列1 ) 返回值列表1
    方法名2( 參數列2 ) 返回值列表2
    …
}

說明

  • 介面型別名:使用 type 將介面定義為自定義的型別名。Go語言的介面在命名時,一般會在單詞後面新增 er,如有寫操作的介面叫 Writer,有字串功能的介面叫 Stringer,有關閉功能的介面叫 Closer 等。介面名最好要能突出該介面的型別含義。
  • 方法名:當方法名首字母是大寫時,且這個介面型別名首字母也是大寫時,這個方法可以被介面所在的包(package)之外的程式碼存取。
  • 參數列、返回值列表:參數列和返回值列表中的引數變數名可以被忽略。

舉個例子,定義一個包含Write方法的Writer介面。

type writer interface{
    Write([]byte) error
}

2.2 實現介面的條件

介面就是規定了一個需要實現的方法列表,在 Go 語言中一個型別只要實現了介面中規定的所有方法,那麼我們就稱它實現了這個介面。

範例

定義的Eater介面型別,它包含一個Eat方法。

// Eater 介面
type Eater interface {
	Eat()
}

有一個Dog結構體型別如下。

type Dog struct {}

因為Eater介面只包含一個Eat方法,所以只需要給Dog結構體新增一個Eat方法就可以滿足Eater介面的要求。

//Dog型別的Eat方法
func (d Dog) Eat() {
	fmt.Println("吃骨頭!")
}

這樣就稱為Dog實現了Eater介面。

完整程式碼

// Eater 介面
type Eater interface {
	Eat()
}

type Dog struct {}

//Dog型別的Eat方法
func (d Dog) Eat() {
	fmt.Println("吃骨頭!")
}

func main() {
	dog := Dog{}
	dog.Eat()
}

2.3 為什麼需要介面

多數情況下,資料可能包含不同的型別,卻會有一個或者多個共同點,這些共同點就是抽象的基礎。

範例

// Eater 介面
type Eater interface {
	Eat()
}

type Dog struct {}

//Dog型別的Eat方法
func (d Dog) Eat() {
	fmt.Println("狗狗喜歡吃骨頭!")
}

type Cat struct {}

func (c Cat) Eat(){
	fmt.Println("小貓喜歡吃魚!")
}

func main() {
	dog := Dog{}
	dog.Eat()
	cat := Cat{}
	cat.Eat()
}

從動物身上,可以抽象出來一個eat方法,這樣即使在擴充套件其它動物進來,也只需要實現Eater 介面中的Eat()方法就可以完成對這個動作的呼叫。

介面可以理解為某一個方面的抽象,可以是多對一的(多個型別實現一個介面),這也是多型的體現。

2.4 介面型別變數

一個介面型別的變數能夠儲存所有實現了該介面的型別變數。

例如在上面的範例中,DogCat型別均實現了Eater介面,此時一個Eater型別的變數就能夠接收CatDog型別的變數。

var x Eater // 宣告一個Eater型別的變數x
a := Cat{}  // 宣告一個Cat型別變數a
b := Dog{}  // 宣告一個Dog型別變數b
x = a       // 可以把Cat型別變數直接賦值給x
x.Eat()     // 小貓喜歡吃魚!
x = b       // 可以把Dog型別變數直接賦值給x
x.Eat()     // 狗狗喜歡吃骨頭!

三、值接收者和指標接收者

通過下方一個範例來演示實現介面使用值接收者和使用指標接收者有什麼區別。

定義一個Mover介面,它包含一個Move方法。

// Mover 定義一個介面型別
type Mover interface {
	Move()
}

3.1 值接收者實現介面

我們定義一個Dog結構體型別,並使用值接收者為其定義一個Move方法。

// Dog 狗結構體型別
type Dog struct{}

// Move 使用值接收者定義Move方法實現Mover介面
func (d Dog) Move() {
	fmt.Println("狗會動")
}

此時實現Mover介面的是Dog型別。

var x Mover    // 宣告一個Mover型別的變數x

var d1 = Dog{} // d1是Dog型別
x = d1         // 可以將d1賦值給變數x
x.Move()

var d2 = &Dog{} // d2是Dog指標型別
x = d2          // 也可以將d2賦值給變數x
x.Move()

從上面的程式碼中我們可以發現,使用值接收者實現介面之後,不管是結構體型別還是對應的結構體指標型別的變數都可以賦值給該介面變數。

3.2 指標接收者實現介面

我們再來測試一下使用指標接收者實現介面有什麼區別。

// Cat 貓結構體型別
type Cat struct{}

// Move 使用指標接收者定義Move方法實現Mover介面
func (c *Cat) Move() {
	fmt.Println("貓會動")
}

此時實現Mover介面的是*Cat型別,我們可以將*Cat型別的變數直接賦值給Mover介面型別的變數x

var c1 = &Cat{} // c1是*Cat型別
x = c1          // 可以將c1當成Mover型別
x.Move()

但是不能給將Cat型別的變數賦值給Mover介面型別的變數x

// 下面的程式碼無法通過編譯
var c2 = Cat{} // c2是Cat型別
x = c2         // 不能將c2當成Mover型別

由於Go語言中有對指標求值的語法糖,對於值接收者實現的介面,無論使用值型別還是指標型別都沒有問題。但是我們並不總是能對一個值求址,所以對於指標接收者實現的介面要額外注意。

四、型別與介面的關係

4.1 一個型別實現多個介面

一個型別可以同時實現多個介面,而介面間彼此獨立,不知道對方的實現。

範例

動物不僅有吃的屬性,還有動的屬性,可以通過定義兩個介面,讓同一個動物分別實現這兩種屬性

// Eater 介面
type Eater interface {
	Eat()
}

// Mover 介面
type Mover interface {
	Move()
}

type Dog struct {}

//Dog型別的Eat方法
func (d Dog) Eat() {
	fmt.Println("狗狗喜歡吃骨頭!")
}

//Dog型別的Move方法
func (d Dog) Move(){
	fmt.Println("狗狗喜歡玩耍!")
}

func main() {
	//初始化結構體
	dog := Dog{}

	//dog實現了Eater和Mover兩個介面
	eat := dog
	move := dog

	eat.Eat()	//對Eater型別呼叫Eat方法
	move.Move()	//對Mover型別呼叫Move方法
}

程式中的結構體Dog分別實現了Eater和Mover兩個介面中的方法。

4.2 多種型別實現同一介面

Go語言中不同的型別還可以實現同一介面。

一個介面的所有方法,不一定需要由一個型別完全實現,介面的方法可以通過在型別中嵌入其他型別或者結構體來實現。

// WashingMachine 洗衣機
type WashingMachine interface {
	wash()
	dry()
}

// 甩幹器
type dryer struct{}

// 實現WashingMachine介面的dry()方法
func (d dryer) dry() {
	fmt.Println("甩一甩")
}

// 洗衣機
type haier struct {
	dryer //嵌入甩幹器
}

// 實現WashingMachine介面的wash()方法
func (h haier) wash() {
	fmt.Println("洗刷刷")
}

func main() {
	h := haier{}
	h.dry()
	h.wash()
}

五、介面巢狀

介面與介面之間可以通過互相巢狀形成新的介面型別。例如Go標準庫io原始碼中就有很多介面之間互相組合的範例。

// src/io/io.go

type Reader interface {
	Read(p []byte) (n int, err error)
}

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

type Closer interface {
	Close() error
}

// ReadWriter 是組合Reader介面和Writer介面形成的新介面型別
type ReadWriter interface {
	Reader
	Writer
}

// ReadCloser 是組合Reader介面和Closer介面形成的新介面型別
type ReadCloser interface {
	Reader
	Closer
}

// WriteCloser 是組合Writer介面和Closer介面形成的新介面型別
type WriteCloser interface {
	Writer
	Closer
}

對於這種由多個介面型別組合形成的新介面型別,同樣只需要實現新介面型別中規定的所有方法就算實現了該介面型別。

介面也可以作為結構體的一個欄位,我們來看一段Go標準庫sort原始碼中的範例。

// src/sort/sort.go

// Interface 定義通過索引對元素排序的介面型別
type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}


// reverse 結構體中嵌入了Interface介面
type reverse struct {
    Interface
}

通過在結構體中嵌入一個介面型別,從而讓該結構體型別實現了該介面型別,並且還可以改寫該介面的方法。

// Less 為reverse型別新增Less方法,重寫原Interface介面型別的Less方法
func (r reverse) Less(i, j int) bool {
	return r.Interface.Less(j, i)
}

Interface型別原本的Less方法簽名為Less(i, j int) bool,此處重寫為r.Interface.Less(j, i),即通過將索引引數交換位置實現反轉。

在這個範例中還有一個需要注意的地方是reverse結構體本身是不可匯出的(結構體型別名稱首字母小寫),sort.go中通過定義一個可匯出的Reverse函數來讓使用者建立reverse結構體範例。

func Reverse(data Interface) Interface {
	return &reverse{data}
}

這樣做的目的是保證得到的reverse結構體中的Interface屬性一定不為nil,否者r.Interface.Less(j, i)就會出現空指標panic。

六、空介面

Golang 中的介面可以不定義任何方法,沒有定義任何方法的介面就是空介面。空介面表示沒有任何約束,因此任何型別變數都可以實現空介面。

空介面在實際專案中用的是非常多的,用空介面可以表示任意資料型別。

範例

func main() {
	//定義一個空介面x,x變數可以接收任意的資料型別
	var x interface{}
	str := "Hello Go"
	x = str
	fmt.Printf("type:%T,value:%vn",x,x)

	num := 10
	x = num
	fmt.Printf("type:%T,value:%vn",x,x)

	bool := true
	x = bool
	fmt.Printf("type:%T,value:%vn",x,x)
}

執行結果

type:string,value:Hello Go
type:int,value:10
type:bool,value:true

1、空介面作為函數的引數

// 空介面作為函數引數
func show(a interface{}) {
	fmt.Printf("type:%T value:%vn", a, a)
}

func main() {
	show(1)
	show(true)
	show(3.14)
	var mapStr = make(map[string]string)
	mapStr["name"] = "Leefs"
	mapStr["age"] = "12"
	show(mapStr)
}

執行結果

type:int value:1
type:bool value:true
type:float64 value:3.14
type:map[string]string value:map[age:12 name:Leefs]

2、map的值實現空介面

func main() {
	// 空介面作為 map 值
	var studentInfo = make(map[string]interface{})
	studentInfo["name"] = "Jeyoo"
	studentInfo["age"] = 18
	studentInfo["married"] = false
	fmt.Println(studentInfo)
}

執行結果

map[age:18 married:false name:Jeyoo]

3、切片實現空介面

func main() {
	var slice = []interface{}{"Jeyoo", 20, true, 32.2}
	fmt.Println(slice)
}

執行結果

[Jeyoo 20 true 32.2]

七、型別斷言

一個介面的值(簡稱介面值)是由一個具體型別和具體型別的值兩部分組成的。這兩部分分別稱為介面的動態型別和動態值。

如果我們想要判斷空介面中值的型別,那麼這個時候就可以使用型別斷言,其語法格式:

x.(T)

說明

  • x: 表示型別為 interface{}的變數
  • T: 表示斷言 x 可能是的型別

該語法返回兩個引數,第一個引數是 x 轉化為 T 型別後的變數,第二個值是一個布林值,若為 true 則表示斷言成功,為 false 則表示斷言失敗。

範例

func main() {
	var x interface{}
	x = "Hello GO"
	v, ok := x.(string)
	if ok {
		fmt.Println(v)
	} else {
		fmt.Println("型別斷言失敗")
	}
}

上面的範例中如果要斷言多次就需要寫多個 if 判斷,這個時候我們可以使用 switch 語句來 實現:

注意:型別.(type)只能結合 switch 語句使用

// justifyType 對傳入的空介面型別變數x進行型別斷言
func justifyType(x interface{}) {
	switch v := x.(type) {
	case string:
		fmt.Printf("x is a string,value is %vn", v)
	case int:
		fmt.Printf("x is a int is %vn", v)
	case bool:
		fmt.Printf("x is a bool is %vn", v)
	default:
		fmt.Println("unsupport type!")
	}
}

由於介面型別變數能夠動態儲存不同型別值的特點,所以很多初學者會濫用介面型別(特別是空介面)來實現編碼過程中的便捷。

只有當有兩個或兩個以上的具體型別必須以相同的方式進行處理時才需要定義介面。切記不要為了使用介面型別而增加不必要的抽象,導致不必要的執行時損耗。

總結

在 Go 語言中介面是一個非常重要的概念和特性,使用介面型別能夠實現程式碼的抽象和解耦,也可以隱藏某個功能的內部實現,但是缺點就是在檢視原始碼的時候,不太方便查詢到具體實現介面的型別。

相信很多讀者在剛接觸到介面型別時都會有很多疑惑,請牢記介面是一種型別,一種抽象的型別。區別於我們在之前章節提到的那些具體型別(整型、陣列、結構體型別等),它是一個只要求實現特定方法的抽象型別。

以上就是Golang介面使用教學詳解的詳細內容,更多關於Golang介面的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com