Go:搞懂interface(接口)

1. 爲什麼要有接口

我們先來假設一個場景:你們公司有個財務小姐姐很不錯,你想追她。觀察一陣子後,你覺得可以幫她寫個程序來降低她的日常工作量。這個程序是這樣的,計算每一個員工薪水,然後統計所有員工的總薪水,這樣就可以讓老闆知道一共要發多少薪水了,不用讓財務小姐姐拿着計算器算的要死。
但是有個問題就是每一個員工計算工資的方式是不一樣的,比如最高級別的如總監不僅有基本薪水,還有獎金和股票分紅。對於組長或者隊長之類的只有基本薪水和獎金了。最後就是新來的底層員工只有基本薪水了。當然基本同一級別的員工計算規則是一樣的,但是可能數據是不一樣的,比如兩個不同部門總監拿的股票是不太一樣的,一個拿了五十萬,一個拿了一百萬。所以我們對於每一個級別的(同一級別的計算規則是一樣的),定義了自己的計算器(CalculatorForOne方法)。
最後我們只需要再寫一個可以統計全部員工薪水的計算器(CalculatorForAll)就好了。
但是,你以爲這樣就結束了了嗎?大錯特錯,我們可以給統計全部員工薪水的計算器(CalculatorForAll)傳遞一組員工的信息,然後讓計算器去算,但是問題就在於如果財務小姐姐不小心傳遞的不是員工的信息,而是一個顧客的信息,這要讓計算器怎麼算?
解決方法就是在傳遞參數時候就進行約束,比如公司員工一進來都會分級,一旦分級就會有自己的計算薪水的計算器(CalculatorForOne)。只需要限制傳遞進來的參數實現了CalculatorForOne的方法就可以認爲是公司的員工了。
而接口就是這樣一組約定,規則。


2、 接口的定義以及格式

接口的定義與格式

type Calculator interface {
	CalculatorForOne()  float32
}

通用格式:

type 接口名 interface {
	函數名(參數列表) 返回值列表
}

對於第1部分的描述,先上完整代碼,各位可以先看看。

// Employee 一個員工基類,每個員工都有自己的名字和編號
type Employee struct {
	Name string
	id uint8
}

// T1 最高級別的員工,有普通薪水,獎金,股票分紅
type T1 struct {
	Employee
	Salary int
	Bonus int
	Stock int
}

// 每一種員工都有自己薪水的計算器
// 對於T1級別的是基本薪水加上5倍年終獎以及百分之二十的股票分紅
func (t *T1) CalculatorForOne()  float32 {
	return float32(t.Salary + 5*t.Bonus) + 0.2*float32(t.Stock)
}

// T2級別的員工只有薪水和獎金
type T2 struct {
	Employee
	Salary int
	Bonus int
}

// 對於T2級別的是基本薪水加上3倍年終獎
func (t *T2) CalculatorForOne()  float32 {
	return float32(t.Salary + 3*t.Bonus)
}

// T2級別的員工只有薪水和獎金
type T3 struct {
	Employee
	Salary int
}

// 對於T2級別的是基本薪水加上3倍年終獎
func (t *T3) CalculatorForOne()  float32 {
	return float32(t.Salary)
}

type Calculator interface {
	CalculatorForOne()  float32
}

// 一個能夠計算全體員工薪水的計算器
func CalculatorForAll(c ...Calculator) (sum float32) {
	for _, employee := range c {
		sum += employee.CalculatorForOne()
	}
	return
}

func main()  {
	e1 := T1{Employee{"Tom", 1}, 10000, 20000, 100000}
	e2 := T2{Employee{"Jack", 2}, 8000, 15000}
	e3 := T3{Employee{"Mary", 3}, 5000}
	fmt.Println(CalculatorForAll(&e1, &e2, &e3))
}

3、 接口實現以及實現的條件條件

3.1、接口實現
只要一個類型實現了接口聲明的所有方法,那麼就稱這個類型實現了這個接口。回到前面的代碼,比如T1類型實現了CalculatorForOne方法,那麼就可以說T1實現了Calculator接口。

3.2、實現的條件

  1. 接口的方法與實現接口的類型方法格式一致(方法名、參數類型、返回值類型一致)。
  2. 接口中所有方法均被實現。

對於第一句話應該這麼理解。
假設我們定義了這麼一個接口:

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

這個Writer接口規定有一個Write方法,這個方法必須接收一個byte切片,並返回一個interror類型的值。
但是我們自己定義的一個類A實現的Write方法接受的是一個rune切片,並返回一個interror類型的值。那麼這個類A就不能說是實現了Writer,因此就不能賦值給Writer
對於第二點的就是一個接口中可以有有多個方法,那麼要實現這個在這裏插入代碼片接口,其中的所有方法都要一個個去實現,缺一不可。


4、接口嵌套

和結構體一樣,接口是可以嵌套的。
嵌套方法如下:

// 可以只是用函數簽名
type Reader interface {
	Read(s []byte) string
}

type Writer interface {
	Write(s []byte) (int, string)
}

// 可以使用多個接口嵌套
type ReadWriter interface {
	Writer
	Reader
}

// 也可以混合使用
type WriteReader interface {
	Write(s []byte) (int, string)
	Reader
}

5、空接口

空接口是指沒有定義任何接口方法的接口。沒有定義任何接口方法,意味着Go中的任意對象都已經實現空接口(因爲沒方法需要實現),只要實現接口的對象都可以被接口保存,所以任意對象都可以保存到空接口實例變量中。
利用空接口可以方便我們很多操作,比如我們可以實現能夠存放任何類型的切片

func main()  {
	var stack []interface{}
	stack = append(stack, "你好啊", 11, true)
	fmt.Println(stack)   // [你好啊 11 true]
}

更方便的是可以在函數的參數列表中定義一個空接口,給函數傳遞任意類型。如傳遞任意數量的任意類型參數可以使用下面的方法:

func Foo(values ...interface{})   {}

題外話:空接口可以使得Go實現類似泛型的功能。這裏就不展開贅述。


6、類型判斷以及類型斷言

空接口的好處上一節也說了,以上一節最後一段代碼爲例,給Foo函數傳遞任意數量的任意類型參數,那麼函數要怎麼判斷傳遞進來的是什麼樣的類型呢?因爲得知道具體類型才能夠採用不同的操作。
那怎麼怎樣才能知道傳遞的類型呢?
有兩種解決方式:

  1. 類型判斷
  2. 類型斷言

6.1、 類型判斷
格式一般如下

value := intfs.(type)

一定要和switch配合使用,一定要和switch配合使用,一定要和switch配合使用::

func Foo(values ...interface{})   {
	for _, v := range values {
		switch value := v.(type)  {
		case int:
			fmt.Println("這是一個整數,對它加倍:", 2*value)
		case string:
			fmt.Println("這是一個字符串,對它打招呼:", "hello " + value)
		case bool:
			fmt.Println("這是一個布爾類型,對它進行取反:", !value)
		}
	}
}

func main()  {
	var stack []interface{}
	stack = append(stack, "你好啊", 11, true)
	Foo(stack...)
}

結果:

這是一個字符串,對它打招呼: hello 你好啊
這是一個整數,對它加倍: 22
這是一個布爾類型,對它進行取反: false

6.2、類型斷言
有時候我們只想要簡單的判斷下類型是否是某類型的時候,我們可以使用類型斷言,具體格式如下:

value := intfs.(TypeName)

使用方式如下:

func main()  {
	var intfs interface{}
	i := 10
	intfs = i
	value := intfs.(int)
	fmt.Println(value)
}

但是上面代碼中有一個危險,就是判斷的類型和實際類型不一致的時候會觸發panic。如上面改爲value := intfs.(bool),進行編譯,會報錯:

panic: interface conversion: interface {} is int, not bool

考慮到這點,就可以使用更一般的形式:

value, ok := intfs.(TypeName)

如果接口中的原類型和TypeName是一樣的,那麼value會是原來的類型值,ok則是true
如果接口中的原類型和TypeName是一樣的,那麼value會是原來的類型值,ok則是flase

func main()  {
	var intfs interface{}
	i := 10
	intfs = i
	value1, ok := intfs.(bool)
	fmt.Println(value1, ok)     // false false
	value2, ok := intfs.(int)
	fmt.Println(value2, ok)    	// 10 true
}

7、指針和接口

如果仔細觀察開頭我給的那段完整代碼,會發現最後一行我將e1,e2,e3傳遞給CalculatorForAll的時候是傳遞他們的地址的。
如果直接傳遞他們的值,編譯器將會報錯。

對於聲明瞭指針接受者方法的接口來說,是不能將一個值類型賦值給接口的。

因爲在賦值給接口(包括傳參),通過底層的瞭解,我們可以知道,其實將一個類型賦值給一個接口是對這個類型進行值拷貝。比如我們執行以下代碼:

var intfs Calculator 
e := T3{Employee{"Mary", 3}, 5000}
intfs = e  // 錯誤,不能這麼做

上面最後一行代碼,編譯器是不通過的。但是爲了方便解釋,先假定能夠通過,看看會發生什麼樣的後果。
首先賦值給接口intfse是一個值類型,所以賦值的時候會進行值拷貝,將e底層的數據重新拷貝一份給intfs,倘若我們通過intfs來執行一個方法(如CalculatorForOne),且這個方法是指針接受者,會修改intfs指向的那一份拷貝的e。由於修改的是拷貝的e這樣,原先的e並不會被修改。這並不是我們想看到了(因爲我們使用指針接受者方法的初衷就是爲了能修改接受者)。
如果我們想要修改一個值類型,但是由於進行了值拷貝使得修改的只是拷貝的對象。所以對於這種做法是不允許的。然而如果傳遞的是指針卻不存在這樣的問題。


8、實例分析

我們來分析下一個標準庫的實例,來感受下這個接口的魅力。

在標準庫net/http中有這麼一個實例Handle方法

func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

Handle接受一個string類型和一個Handler類型。我們繼續看下Handler源碼:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

原來Handler是一個接口類型,這個接口類型告訴我們要使用這個接口就必須實現ServeHTTP這個方法。
所以我們可以定義一個計數器實現這個方法(也就實現了這個接口):

// 簡單的計數器服務器。
type Counter struct {
    n int
}

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    ctr.n++
    fmt.Fprintf(w, "counter = %d\n", ctr.n)
}

然後就可以將聲明瞭該接口的一個對象傳遞給Handle:

ctr := new(Counter)
http.Handle("/counter", ctr)

但是如果想直接傳遞一個函數而不是一個實現了該方法的對象,那該怎麼做?
答案是: 可以給一個函數實現ServeHTTP方法。驚了吧,函數還可以聲明方法。要知道函數在Go中是第一等公民,所以對其他類型的操作,對函數同等使用,爲此net/http定義了一個HandlerFunc類型,可以將func(ResponseWriter, *Request)這類函數轉換爲HandlerFunc,而HandlerFunc實現了ServeHTTP方法。

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

使用方式如下:

func test(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintln(w, os.Args)
}
http.Handle("/args", http.HandlerFunc(test))

9、源碼分析

能力不足,日後再更…


參考文獻:
《Effective Go》
《理解Golang中的interface和interface{}》


撩我?搜我微信公衆號:Kyda

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章