Golang 接口 (interface) 用法介紹


Golang interface 用法

作者:閃電豹貓 轉載注明出處

1. 接口 (interface) 介紹

接口是 Go 語言提供的數據類型之一,它把所有具有共性的方法 (注意與函數區別開) 定義在一起,任何其它類型只要一一實現這些方法的話,我們就稱這個類型實現了這個接口。Go 語言的接口與 C++ 的虛函數有共通之處,提高了語言的靈活性的同時也彌補了語言自身的一些不足。

Go 語言的接口與其它面向對象語言的接口不同,Go 的接口只是用來對方法進行一個收束,而正是這個收束,使得 Go 這個面向過程的語言擁有了面向對象的特征。

一般來說,Go 接口的主要功能有:

  1. 作為方法的收束器,進行具有 “面向對象程序設計特色” 的程序設計。
  2. 作為各種數據的承載者,可以用來為函數接收各類不同數量的函數參數,這也是 Go 提倡的接口編程。

2. 接口的定義和使用

2.1 定義

比如一個完整方法的接口的定義:

// 這是接口,接口內只有方法的定義,沒有具體實現
type 接口類型名 interface {
	方法名1( 參數列表1 )  返回值列表1
	方法名2( 參數列表2 )  返回值列表2
	...
}

// 定義結構體
type 結構體名 struct {
	變量名1 類型1
	變量名2 類型2
	...
}

// 實現接口方法
func ( 結構體變量1 結構體名 ) 方法名1( 參數列表1 ) 返回值列表1 {
	//方法實現
}
func ( 結構體變量2 結構體名 ) 方法名2( 參數列表2 ) 返回值列表2 {
	//方法實現
}
func ( 結構體變量n 結構體名 ) 方法名n( 參數列表n ) 返回值列表n {
	//方法實現
}

在實踐中,我們一般將接口命名為 “什么什么er”,比如寫操作的接口可以叫Writer,讀取字符串的接口可以叫做StringReader。和變量的命名規則一樣,接口名的命名也是不能以數字開頭、只允許出現一種特殊字符_,開頭大寫則包外可見,開頭小寫則方法在包外不可見等等。

對於接口內的方法名,也是一樣的。只有接口名和方法名的首字母都大寫,才可以在包外調用這個接口的這個方法。

2.2 使用

一個接口只要全部實現了接口中聲明的方法,那么就是實現了這個接口。換句話講,接口就是一個需要具體實現的方法的列表。
下面給出一個示例代碼

// 定義接口
type Canteen interface {
	MakeRice()
	MakeNoodles()
}

// 定義結構體
type ZhuYuan struct {}
type HaiTang struct {}
type DingXiang struct {}

// 挨個實現接口里所聲明的方法
func (a ZhuYuan) MakeRice() {
	fmt.Println("竹園餐廳的米飯")
}
func (a ZhuYuan) MakeNoodles() {
	fmt.Println("竹園餐廳的面條")
}
func (a HaiTang) MakeRice() {
	fmt.Println("海棠餐廳的米飯")
}
func (a HaiTang) MakeNoodles() {
	fmt.Println("海棠餐廳的面條")
}
func (pa *DingXIang) MakeRice() {
	fmt.Println("丁香餐廳的米飯")
}
func (pa *DingXiang) MakeNoodles() {
	fmt.Println("丁香餐廳的面條")
}

該示例中,我們將竹園和海棠用結構體對象實現,而丁香是用指向結構體的指針實現的。這樣,我們在接口實例化時:

var a Canteen = ZhuYuan{}		// 接受結構體,且傳入的也是結構體,可以通過編譯
var b Canteen = &ZhuYaun{}		// 接受結構體,傳入的是指針,可以通過編譯,這很重要

var c Canteen = DingXiang{}		// 接受指針,傳入的卻是結構體,編譯當然會失敗
var d Canteen = &DingXiang{}		// 接受指針,傳入的也是指針,可以通過編譯

記住,Go 中的所有東西都是按值傳遞的。每次調用函數時,傳入的數據都會被復制。對於具有值接收者的方法,在調用該方法時將復制該值。對於上面四行代碼中的 ad ,沒啥好解釋的,接受啥類型就給它啥類型嘛。對於 b ,編譯器會對指針 &ZhuYuan{} 進行拷貝,相關方法調用時,會對拷貝后的指針進行隱式解引用獲取指針指向的結構體。這也就能解釋為啥上面的var b Canteen = &ZhuYuan{}為什么能通過編譯。打個比方,只給你一個 int8 類型的值 123 ,你無法知道這個 123 的內存地址;而給你一個指針 *int8 ,你既能獲知該 int8 數的內存地址,又能知道該數是多少。

總而言之,當我們用指針實現方法時,只有指針類型的變量才可以實現接口;當我們用結構體實現方法時,結構體類型和指針類型都可以實現接口。不過,在實際開發中,這個性質沒那么重要,這里講開了是為了解釋現象背后的原理。

補充

Go 的接口是隱式實現的,也就是說,在接口的定義里的一條條方法只是聲明,具體有沒有方法的實現,Go 不在乎。
因為是隱式實現的,所以不實現方法也是可以通過編譯的,只要程序別遇到需要對未實現的方法進行傳參、返參和變量賦值,編譯器就不會檢查,程序就不會嗝屁。

如下代碼完全可以編譯運行,控制台輸出 12 並退出:

package main

import (
	"fmt"
)
type hhher interface {
	AAA(int, int)
	PrintAge()
	CCCC(string, map[int]string) (int, int)
}

type People struct {
	Age int
} 

func (human People) PrintAge() {
	fmt.Println(human.Age)
}

func main() {
	fmt.Println("Hello, playground")
	alex := &People{Age:12,}		// 這里就是接收結構體而傳入指針,是可行的
	alex.PrintAge()
}

這里不推薦沒有把接口里的方法全部實現的做法。

2.3 數據承擔者

一個空接口 interface{} 什么方法 (method) 也沒有實現,是一個能裝入任意數量、任意數據類型的數據容器。

為什么這樣說呢?是這樣的。空接口 interface{} 也是接口,不過是沒有實現方法的接口罷了。回顧接口的定義:接口是一組方法的集合,是一種數據類型,任何其他類型如果能實現接口內所有的方法的話,我們稱那個數據類型實現了這個接口。咱們再來看空接口:里面連一個方法也沒有,不也就是任意數據類型都能實現這個接口了嘛。這就和 “空集能被任意集合包含” 一樣嘛,空接口能被任意數據類型實現。

與 C 語言的 void * 可以轉換成任意其它類型的指針 (如 char *int * 等) 不同的是,Go 語言的 interface{} 不是一個任意數據類型,interface{} 的類型就是 interface{} 類型,不能轉換成其他接口類型,更不能轉換成其他什么類型 (比如[]intstring等等) ,只不過是 interface{}裝入任意數據罷了。

把其它類型的變量轉換成interface{}類型后,在程序運行時 (runtime) 內,該變量的數據類型將會發生變化,但是如果這時候要求獲取該變量的數據類型,我們會得到interface{}類型。這是為啥子呢?

在 Golang 的源代碼中,用runtime.iface表示非空接口類型,用runtime.eface表示空接口類型interface{}。雖然它們倆都用一個interface聲明,但是后者在 Golang 的源代碼更加常見,所以在實現interface{}時,使用了特殊的類型。具體的你得看 Golang 源代碼和 Go 手冊了。

  1. 用空接口可以讓函數和方法接受任意類型、任意數量的函數參數:
func show(a interface{}) {
	fmt.Printf("a的類型是%T,a的值是%v\n", a, a)
}

空接口切片還可以用於函數的可選參數,比如:

func main() {
	kkk(234, "qwerty", [5]int64{1,2,4}, false, nil)
	kkk(236)
}

func kkk(key int, a ...interface{}) {
	// 必選參數是一個 int 類型,可選參數用空接口切片表示
	// 其類型為 []interface{},這個 a 是可以下表訪問的,而每個 a 的元素都是個空接口
	if key == 234 {
		fmt.Println((a[1])) // 這里需要保證a[1]下標不越界,我這里沒有進行判斷
		switch ttt := a[0].(type) {
			case string:	fmt.Println("0th element is string interface{}")
			default: fmt.Printf("idk wtf is this: %T", ttt)
		}
		switch ttt := a[1].(type) {
			case string:	fmt.Println("1st element is string interface{}")
			default: fmt.Printf("idk wtf is this: %T\n", ttt)
		}
	} else {
		fmt.Println("key wrong")
	}
}

程序輸出如下:

[1 2 4 0 0]
0th element is string interface{}
idk wtf is this: [5]int64
key wrong
  1. 空接口還可以作為函數的返回值,但是極不推薦這樣干,因為代碼的維護、拓展與重構將會變得極為痛苦。

  2. 空接口可以實現保存任意類型值的字典 (map):

var alexInfo = make( map[string]interface{} )
alexInfo["name"] = "Alex"
alexInfo["age"] = 12
alexInfo["score"] = [4]int{150, 150, 150, 300}
fmt.Println(alexInfo)

控制台輸出

map[age:12 name:Alex score:[150 150 150 300]]

3. 接口類型轉換

接口 (包括空接口) 可以存儲所有的值,那么自然會涉及到類型轉換這個話題。我們將分兩部分來討論接口類型轉換,分別是以結構體實現的接口和以指針實現的接口。

3.1 指向結構體的指針實現的接口

這里挖個坑,以后再填

3.2 結構體實現的接口

這里挖個坑,以后再填

3.3 類型斷言

在 2.3.1 節中,我們的代碼已經用到了類型斷言,下面來具體介紹一下類型斷言。

如何把一個接口類型轉換成具體類型 T ?

x.(T)
  1. 對於非空接口:
package main

import (
	"fmt"
)

type OptionForMeat interface {
	Boil(int)
	Fry(int)
}

type Meat struct {
	Name string
}

func (a Meat) Boil(minute int) {
	fmt.Printf("煮了%d分鍾的%s了\n", minute, a.Name)
}

func (a Meat) Fry(minute int) {
	fmt.Printf("煎了%d分鍾的%s了\n", minute, a.Name)
}

func main() {
	var aaa OptionForMeat = &Meat{Name:"Porkchop",}
	switch aaa.(type) {				// 斷言
		case *Meat:
			w := aaa.(*Meat)
			w.Boil(5)
			//aaa.Fry(6)		// 這行會輸出 “煎了6分鍾的Porkchop了”
	}
}

控制台輸出:

煮了5分鍾的Porkchop了

Go 語言的編譯器對這種情況進行了優化,switch 語句生成的匯編代碼會將目標類型的Hash與接口的itab.Hash進行比較。

  1. 對於空接口而言
// 只是換一行代碼
var aaa interface{} = &Meat{Name:"Porkchop",}

控制台輸出是一樣的。上述代碼在斷言時並不是直接獲取runtime._type,而是從eface._type獲取類型值,匯編指令仍會使用目標類型的Hash與變量的類型比較。

4. 總結

接口是個抽象數據類型,不要為了寫接口而寫接口,有些不需要接口的地方硬是搞成接口模式,只會帶來不必要的損耗。

希望這篇文章能對你在學習接口的過程中有所幫助。碼字不易,轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM