Go 語言介面詳解(二)

Seekload發表於2019-04-15

這是『就要學習 Go 語言』系列的第 20 篇分享文章

提醒:文末給大家留了小練習,可以先看文章,再做練習,檢驗自己的學習成果!

我們接著上一篇,繼續講介面的其他用法。

實現多個介面

一種型別可以實現多個介面,來看下例子:

type Shape interface {
	Area() float32
}

type Object interface {
	Perimeter() float32
}

type Circle struct {
	radius float32
}

func (c Circle) Area() float32 {
	return math.Pi * (c.radius * c.radius)
}

func (c Circle) Perimeter() float32 {
	return 2 * math.Pi * c.radius
}

func main() {
	c := Circle{3}
	var s Shape = c
	var p Object = c
	fmt.Println("area: ", s.Area())
	fmt.Println("perimeter: ", p.Perimeter())
}
複製程式碼

輸出

area:  28.274334
perimeter:  18.849556
複製程式碼

上面的程式碼,結構體 Circle 分別實現了 Shape 介面和 Object 介面,所以可以將結構體變數 c 賦給變數 s 和 p,此時 s 和 p 具有相同的動態型別和動態值,分別呼叫各自實現的方法 Area() 和 Perimeter()。 我們修改下程式:

fmt.Println("area: ", p.Area())
fmt.Println("perimeter: ", s.Perimeter())
複製程式碼

編譯會出錯:

p.Area undefined (type Object has no field or method Area)
s.Perimeter undefined (type Shape has no field or method Perimeter)
複製程式碼

為什麼?因為 s 的靜態型別是 Shape,而 p 的靜態型別是 Object。那有什麼解決辦法嗎?有的,我們接著看下一節

型別斷言

型別斷言可以用來獲取介面的底層值,通常的語法:i.(Type),其中 i 是介面,Type 是型別或介面。編譯時會自動檢測 i 的動態型別與 Type 是否一致。

type Shape interface {
	Area() float32
}

type Object interface {
	Perimeter() float32
}

type Circle struct {
	radius float32
}

func (c Circle) Area() float32 {
	return math.Pi * (c.radius * c.radius)
}

func (c Circle) Perimeter() float32 {
	return 2 * math.Pi * c.radius
}

func main() {
	var s Shape = Circle{3}
	c := s.(Circle)
	fmt.Printf("%T\n",c)
	fmt.Printf("%v\n",c)
	fmt.Println("area: ", c.Area())
	fmt.Println("perimeter: ", c.Perimeter())
}
複製程式碼

輸出

main.Circle
{3}
area:  28.274334
perimeter:  18.849556
複製程式碼

上面的程式碼,我們可以通過 c 訪問介面 s 的底層值,也可以通過 c 分別呼叫方法 Area() 和 Perimeter(),這就解決了上面遇到的問題。 在語法 i.(Type) 中,如果 Type 沒有實現 i 所屬的介面,編譯的時候會報錯;或者 i 的動態值不是 Type,則會報 panic 錯誤。怎麼解決呢?可以使用下面的語法:

value, ok := i.(Type)
複製程式碼

使用上面的語法,Go 會自動檢測上面提到的兩種情況,我們只需要通過變數 ok 判斷結果是否正確即可。如果正確,ok 為 true,否則為 false,value 為 Type 對應的零值。

型別選擇

型別選擇用於將介面的具體型別與各種 case 語句中指定的多種型別進行匹配比較,有點類似於 switch case 語句,不同的是 case 中指定是型別。 型別選擇的語法有點類似於型別斷言的語法:i.(type),其中 i 是介面,type 是固定關鍵字,使用這個可以獲得介面的具體型別而不是值,每一個 case 中的型別必須實現了 i 介面。

func switchType(i interface{}) {
	switch i.(type) {
	case string:
		fmt.Printf("string and value is %s\n", i.(string))
	case int:
		fmt.Printf("int and value is %d\n", i.(int))
	default:
		fmt.Printf("Unknown type\n")
	}
}
func main() {
	switchType("Seekload")
	switchType(27)
	switchType(true)
}
複製程式碼

輸出:

string and value is Seekload
int and value is 27
Unknown type
複製程式碼

上面的程式碼應該很好理解,i 的型別匹配到哪個 case ,就會執行相應的輸出語句。 注意:只有介面型別才可以進行型別選擇。其他型別,例如 int、string等是不能的:

i := 1
switch i.(type) {
case int:
	println("int type")
default:
	println("unknown type")
}
複製程式碼

報錯:

cannot type switch on non-interface value i (type int)
複製程式碼

介面巢狀

Go 語言中,介面不能去實現別的介面也不能繼承,但是可以通過巢狀介面建立新介面。

type Math interface {
	Shape
	Object
}
type Shape interface {
	Area() float32
}

type Object interface {
	Perimeter() float32
}

type Circle struct {
	radius float32
}

func (c Circle) Area() float32 {
	return math.Pi * (c.radius * c.radius)
}

func (c Circle) Perimeter() float32 {
	return 2 * math.Pi * c.radius
}

func main() {

	c := Circle{3}
	var m Math = c
	fmt.Printf("%T\n", m )
	fmt.Println("area: ", m.Area())
	fmt.Println("perimeter: ", m.Perimeter())
}
複製程式碼

輸出

main.Circle
area:  28.274334
perimeter:  18.849556
複製程式碼

上面的程式碼,通過巢狀介面 Shape 和 Object,建立了新的介面 Math。任何型別如果實現了介面 Shape 和 Object 定義的方法,則說型別也實現了介面 Math,例如我們建立的結構體 Circle。 主函式裡面,定義了介面型別的變數 m,動態型別是結構體 Circle,注意下方法 Area 和 Perimeter 的呼叫方式,類似與訪問巢狀結構體的成員。

使用指標接收者和值接收者實現介面

在前面我們都是通過值接收者去實現介面的,其實還可以通過指標接收者實現介面。實現過程中還是有需要注意的地方,我們來看下:

type Shape interface {
	Area() float32
}

type Circle struct {
	radius float32
}

type Square struct {
	side float32
}

func (c Circle) Area() float32 {
	return math.Pi * (c.radius * c.radius)
}

func (s *Square) Area() float32 {
	return s.side * s.side
}

func main() {
	var s Shape
	c1 := Circle{3}
	s = c1
	fmt.Printf("%v\n",s.Area())

	c2 := Circle{4}
	s = &c2
	fmt.Printf("%v\n",s.Area())

	c3 := Square{3}
	//s = c3
	s = &c3
	fmt.Printf("%v\n",s.Area())

}
複製程式碼

輸出

28.274334
50.265484
9
複製程式碼

上面的程式碼,結構體 Circle 通過值接收者實現了介面 Shape。我們在方法那篇文章中已經討論過了,值接收者的方法可以使用值或者指標呼叫,所以上面的 c1 和 c2 的呼叫方式是合法的。

結構體 Square 通過指標接收者實現了介面 Shape。如果將上方註釋部分開啟的話,編譯就會出錯:

cannot use c3 (type Square) as type Shape in assignment:
Square does not implement Shape (Area method has pointer receiver)
複製程式碼

從報錯提示資訊可以清楚看出,此時我們嘗試將值型別 c3 分配給 s,但 c3 並沒有實現介面 Shape。這可能會令我們有點驚訝,因為在方法中,我們可以直接通過值型別或者指標型別呼叫指標接收者方法。 記住一點:對於指標接受者的方法,用一個指標或者一個可取得地址的值來呼叫都是合法的。但介面儲存的具體值是不可定址的,對於編譯器無法自動獲取 c3 的地址,於是程式報錯。

關於介面的使用方法總結到這,希望這兩篇文章能夠給你帶來幫助!

作業: 文章提到的型別斷言:i.(Type),其中 i 是介面,Type 可以是型別或介面,如果 Type 是介面的話,表示式是什麼意思呢?下面的程式輸出什麼?

type Shape interface {
	Area() float32
}

type Object interface {
	Perimeter() float32
}

type Circle struct {
	radius float32
}

func (c Circle) Area() float32 {
	return math.Pi * (c.radius * c.radius)
}

func main() {
	var s Shape = Circle{3}
	value1,ok1 := s.(Shape)
	value2,ok2 := s.(Object)

	fmt.Println(value1,ok1)
	fmt.Println(value2,ok2)
}
複製程式碼

歡迎大家留言討論!


(全文完)

原創文章,若需轉載請註明出處!
歡迎掃碼關注公眾號「Golang來啦」或者移步 seekload.net ,檢視更多精彩文章。

公眾號「Golang來啦」給你準備了一份神祕學習大禮包,後臺回覆【電子書】領取!

公眾號二維碼

相關文章