讓我一起談談Go中接口
本文轉載自微信公眾號「光城」,作者lightcity。轉載本文請聯(lián)系光城公眾號。
1.接口
在Go中使用interface關鍵字聲明一個接口:
- type Shaper interface {
- Area() float64
- Perimeter() float64
- }
如果我們直接使用fmt庫進行輸出,會得到什么結果呢?
- func main() {
- var s Shaper
- fmt.Println("value of s is ", s)
- fmt.Printf("type of s is %T\n", s)
- }
輸出:
- value of s is <nil>
- type of s is <nil>
在這里,引出接口的概念。接口有兩種類型。接口的靜態(tài)類型是接口本身,例如上述程序中的Shape。接口沒有靜態(tài)值,而是指向動態(tài)值。
接口類型的變量可以保存實現(xiàn)接口的類型的值。該類型的值成為接口的動態(tài)值,并且該類型成為接口的動態(tài)類型。
從上面的示例開始,我們可以看到零值和接口的類型為nil。這是因為,此刻,我們已聲明類型Shaper的變量s,但未分配任何值。當我們使用帶有接口參數的fmt包中的Println函數時,它指向接口的動態(tài)值,Printf功能中的%T語法是指動態(tài)類型的接口。實際上,接口靜態(tài)類型是Shaper。
當我們使用一個類型去實現(xiàn)該接口后,會是什么效果。
- type Rect struct {
- width float64
- height float64
- }
- func (r Rect) Area() float64 {
- return r.width * r.height
- }
- func (r Rect) Perimeter() float64 {
- return 2 * (r.width + r.height)
- }
- // main
- func main() {
- var s Shaper
- fmt.Println("value of s is ", s)
- fmt.Printf("type of s is %T\n", s)
- s = Rect{5.0, 4.0}
- r := Rect{5.0, 4.0}
- fmt.Printf("type of s is %T\n", s)
- fmt.Printf("value of s is %v\n", s)
- fmt.Printf("area of rect is %v\n", s.Area())
- fmt.Println("s == r is", s == r)
- }
輸出:
- value of s is <nil>
- type of s is <nil>
- type of s is main.Rect
- value of s is {5 4}
- area of rect is 20
- s == r is tru
可以看到此時s變成了動態(tài)類型,存儲的是main.Rect,值變成了{5,4}。
有時,動態(tài)類型的接口也稱為具體類型,因為當我們訪問接口類型時,它會返回其底層動態(tài)值的類型,并且其靜態(tài)類型保持隱藏。
我們可以在s上調用Area方法,因為接口Shaper定義了Area方法,而s的具體類型是Rect,它實現(xiàn)了Area方法。該方法將在接口保存的動態(tài)值上被調用。
此外,我們可以看到我們可以使用s與r進行比較,因為這兩個變量都保存相同的動態(tài)類型(Rect類型的結構)和動態(tài)值{5 4}。
我們接著使用圓來實現(xiàn)該接口:
- type Circle struct {
- radius float64
- }
- func (c Circle) Area() float64 {
- return 3.14 * c.radius * c.radius
- }
- func (c Circle) Perimeter() float64 {
- return 2 * 3.14 * c.radius
- }
- // main
- s = Circle{10}
- fmt.Printf("type of s is %T\n", s)
- fmt.Printf("value of s is %v\n", s)
- fmt.Printf("area of rect is %v\n", s.Area())
此時輸出:
- type of s is main.Circle
- value of s is {10}
- area of rect is 314
這里進一步理解了接口保存的動態(tài)類型。從切片角度出發(fā),可以說,接口也以類似的方式工作,即動態(tài)保存對底層類型的引用。
當我們刪除掉Perimeter的實現(xiàn),可以看到如下報錯結果。
- ./rect.go:34:4: cannot use Rect{...} (type Rect) as type Shaper in assignment:
- Rect does not implement Shaper (missing Perimeter method)
從上面的錯誤應該是顯而易見的,為了成功實現(xiàn)接口,需要實現(xiàn)與完全簽名的接口聲明的所有方法。
2.空接口
當一個接口沒有任何方法時,它被稱為空接口。這由接口{}表示。因為空接口沒有方法,所以所有類型都隱式地實現(xiàn)了這個接口。
空接口的作用之一在于:函數可以接收多個不同類型參數。
例如:fmt的Println函數。
- func Println(a ...interface{}) (n int, err error)
Println是一個可變函數,它接受interface{}類型的參數。
例如:
- type MyString string
- func explain(i interface{}) {
- fmt.Printf("type: %T, value: %v\n", i, i)
- }
- // main
- s := MyString("hello")
- explain(s)
- r := Rect{1, 2}
- explain(r)
輸出:
- type: inter.MyString, value: hello
- type: inter.Rect, value: {1 2}
可以看到空接口的類型與值是動態(tài)的。
3.多個接口
在下面的程序中,我們用Area方法創(chuàng)建了Shape接口,用Volume方法創(chuàng)建了Object接口。因為結構類型Cube實現(xiàn)了這兩個方法,所以它實現(xiàn)了這兩個接口。因此,我們可以將結構類型Cube的值賦給類型為Shape或Object的變量。
- type IShape interface {
- Area() float64
- }
- type Object interface {
- Volume() float64
- }
- type Cube struct {
- side float64
- }
- func (c Cube) Area() float64 {
- return 6 * c.side * c.side
- }
- func (c Cube) Volume() float64 {
- return c.side * c.side * c.side
- }
- // main
- c := Cube{3}
- var s IShape = c
- var o Object = c
- fmt.Println("area is", s.Area())
- fmt.Println("Volume is", o.Volume())
這種調用是沒有問題的,調用各自動態(tài)類型的方法。
那如果是這樣呢?
- fmt.Println("area of s of interface type IShape is", s.Volume())
- fmt.Println("volume of o of interface type Object is", o.Area())
輸出:
- s.Volume undefined (type Shape has no field or method Volume)
- o.Area undefined (type Object has no field or method Area)
這個程序無法編譯,因為s的靜態(tài)類型是IShape,而o的靜態(tài)類型是Object。因為IShape沒有定義Volume方法,Object也沒有定義Area方法,所以我們得到了上面的錯誤。
要使其工作,我們需要以某種方式提取這些接口的動態(tài)值,這是一個立方體類型的結構體,立方體實現(xiàn)了這些方法。這可以使用類型斷言來完成。
4.類型斷言
我們可以通過i.(Type)確定接口i的底層動態(tài)值,Go將檢查i的動態(tài)類型是否與type相同,并返回可能的動態(tài)值。
- var s1 IShape = Cube{3}
- c1 := s1.(Cube)
- fmt.Println("area of s of interface type IShape is", c1.Volume())
- fmt.Println("volume of o of interface type Object is", c1.Area())
這樣便可以正常工作了。
如果IShape沒有存儲Cube類型,且Cube沒有實現(xiàn)IShape,那么報錯:
- impossible type assertion:
- Cube does not implement IShape (missing Area method)
如果IShape沒有存儲Cube類型,且Cube實現(xiàn)Shape,那么報錯:
- panic: interface conversion: inter.IShape is nil, not inter.Cub
幸運的是,語法中還有另一個返回值:
- value, ok := i.(Type)
在上面的語法中,如果i有具體的type類型或type的動態(tài)值,我們可以使用ok變量來檢查。如果不是,那么ok將為假,value將為Type的零值(nil)。
此外,使用類型斷言可以檢查該接口的動態(tài)類型是否實現(xiàn)了其他接口,就像前面的IShape的動態(tài)類型是Cube,它實現(xiàn)了IShape、Object接口,如下例子:
- vaule1, ok1 := s1.(Object)
- value2, ok2 := s1.(Skin)
- fmt.Printf("IShape s的動態(tài)類型值是: %v, 該動態(tài)類型是否實現(xiàn)了Object接口: %v\n", vaule1, ok1)
- fmt.Printf("IShape s的動態(tài)類型值是: %v, 該動態(tài)類型是否實現(xiàn)了Skin接口: %v\n", value2, ok2)
輸出:
- IShape s的動態(tài)類型值是: {3}, 該動態(tài)類型是否實現(xiàn)了Object接口: true
- IShape s的動態(tài)類型值是: <nil>, 該動態(tài)類型是否實現(xiàn)了Skin接口: false
類型斷言不僅用于檢查接口是否具有某個給定類型的具體值,而且還用于將接口類型的給定變量轉換為不同的接口類型。
5.類型Switch
在前面的空接口中,我們知道將一個空接口作為函數參數,那么該函數可以接受任意類型,那如果我有一個需求是:當傳遞的數據類型是字符串時,要求全部變?yōu)榇髮?,其他類型不進行操作?
針對這樣的需求,我們可以采用Type Switch,即:i.(type)+switch。
- func switchProcess(i interface{}) {
- switch i.(type) {
- case string:
- fmt.Println("process string")
- case int:
- fmt.Println("process int")
- default:
- fmt.Printf("type is %T\n", i)
- }
- }
輸出:
- process int
- process string
6.嵌入接口
在Go中,一個接口不能實現(xiàn)或擴展其他接口,但我們可以通過合并兩個或多個接口來創(chuàng)建一個新的接口。
例如:
這里使用Runner與Eater兩個接口,組合成了一個新接口RunEater,該接口為Embedding interfaces。
- type Runner interface {
- run() string
- }
- type Eater interface {
- eat() string
- }
- type RunEater interface {
- Runner
- Eater
- }
- type Dog struct {
- age int
- }
- func (d Dog) run() string {
- return "run"
- }
- func (d Dog) eat() string {
- return "eat"
- }
- // main
- d := Dog{10}
- var re RunEater = d
- var r Runner = d
- var e Eater = d
- fmt.Printf("RunnEater dynamic type: %T, value: %v\n", re, re)
- fmt.Printf("Runn dynamic type: %T, value: %v\n", r, r)
- fmt.Printf("Eater dynamic type: %T, value: %v\n", e, e)
輸出:
- RunnEater dynamic type: inter.Dog, value: {10}
- Runn dynamic type: inter.Dog, value: {10}
- Eater dynamic type: inter.Dog, value: {10}
7.接口比較
如果基礎動態(tài)值為nil,則兩個接口總是相等的,這意味著兩個nil接口總是相等的,因此== operation返回true。
- var a, b interface{}
- fmt.Println( a == b ) // true
如果這些接口不是nil,那么它們的動態(tài)類型(具體值的類型)應該相同,具體值應該相等。
如果接口的動態(tài)類型不具有可比性,例如slice、map、function,或者接口的具體值是包含這些不可比較性值的復雜數據結構,如切片或數組,則==或!=操作將導致運行時panic。
學習自https://medium.com/rungo/interfaces-in-go-ab1601159b3a