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、實現的條件
- 接口的方法與實現接口的類型方法格式一致(方法名、參數類型、返回值類型一致)。
- 接口中所有方法均被實現。
對於第一句話應該這麼理解。
假設我們定義了這麼一個接口:
type Writer interface {
Write(p []byte) (n int, err error)
}
這個Writer
接口規定有一個Write
方法,這個方法必須接收一個byte
切片,並返回一個int
和error
類型的值。
但是我們自己定義的一個類A實現的Write
方法接受的是一個rune
切片,並返回一個int
和error
類型的值。那麼這個類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
函數傳遞任意數量的任意類型參數,那麼函數要怎麼判斷傳遞進來的是什麼樣的類型呢?因爲得知道具體類型才能夠採用不同的操作。
那怎麼怎樣才能知道傳遞的類型呢?
有兩種解決方式:
- 類型判斷
- 類型斷言
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 // 錯誤,不能這麼做
上面最後一行代碼,編譯器是不通過的。但是爲了方便解釋,先假定能夠通過,看看會發生什麼樣的後果。
首先賦值給接口intfs
的e
是一個值類型,所以賦值的時候會進行值拷貝,將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