Go語言的介面與反射
Go語言的介面與反射
go總體而言是一門比較好入門的語言,許多特性都很精簡易懂,但是介面與反射除外。他們真的讓人頭疼,不知道是自身資質問題還是怎麼著,總是覺得很多書上寫的不夠精簡明瞭。。而我,亞楠老獵人,今天就是要受苦試著把它給攻克了。
介面
你可以用很多詞語來形容golang,但“傳統”肯定不能用。因為,它裡面沒有類和繼承的概念。
你覺得這簡直不可思議,怎麼可能這樣,那不是意味著海量的重複程式碼。並沒有,Go通過很靈活的一個概念,實現了很多面向物件的行為。沒錯,這個概念就是“介面”。
我們來看看介面的特性。
介面被隱式實現
型別不需要顯式宣告它實現了某個介面,介面是被隱式地實現的。
什麼意思?就是說只要你把介面宣告的方法都實現了,那麼就認為你實現了這個介面了。無需像其他語言那樣在顯眼的地方表明 implements 介面名稱
,比如php中你可能需要這樣子:
<?php interface Cinema { public function show(Order $show,$num); } // 顯示正常 class Order implements Cinema { public $number='0011排'; public function show(Order $show,$num) { echo $show->number.$num; } } $face= new Order(); $face->show(new Order,$num='3人');//輸出 0011排3人
而在golang中,你只需要這個樣子:
// 一個簡單的求正方形面積的例子 package main import "fmt" // 形狀介面 type Shape interface { Area() float32 } // 輸出形狀面積 func PrintArea(shape Shape) { fmt.Printf("The square has area: %f\n", shape.Area()) } // 正方形結構體 type Square struct { side float32 } // 正方形面積 func (sq *Square) Area() float32 { return sq.side * sq.side } func main() { square := new(Square) square.side = 5 PrintArea(square) }
上面的程式定義了一個結構體 Square 和一個介面 Shape,介面有一個方法 Area(),而Square實現了這個方法,雖然沒有顯示宣告。
這時你發現,PrintArea
這個函式居然可以直接接受了Square型別的引數,儘管函式定義裡,引數是Shape介面型別的。
也就是說,golang認為你已經用Square結構體實現了Shape介面。
如果,我們對程式碼稍作修改,給介面定義中增加周長(Perimeter)方法
// 形狀介面
type Shape interface {
Area() float32
Perimeter() float32
}
其他不作改動,你就會發現編譯器報錯了
cannot use square (type *Square) as type Shape in argument to DescArea:
*Square does not implement Shape (missing Perimeter method)
報錯資訊說的很明瞭,Shape還有個方法Perimeter,但是Square卻未實現它。雖然還沒有人去呼叫這個方法,但是編譯器也會提前給出錯誤。
下面我們準備開始瞭解繼承與多型,在開始之前,我們記住這句話
一個介面可以由多種型別實現,一種型別也可以實現多個介面。
介面實現繼承
雖然Go語言沒有繼承的概念,但為了便於理解,如果一個struct A 實現了 interface B的所有方法時,我們稱之為“繼承”。
一個介面可以包含一個或者多個其他的介面,這相當於直接把這些內嵌介面的方法列舉在外層介面中一樣。
比如,還是那個Shape的例子,我們這次增加一個要素,顏色,來生成多彩的正方形。
package main
import "fmt"
// 形狀介面
type Shape interface {
Area() float32
}
// 顏色介面
type Color interface {
Colors() []string
}
// 多彩的形狀介面
type ColorfulShape interface {
Shape
Color
Name()
}
比如上面的例子,最後的ColorfulShape
就包含了Shape和Color介面,此外還有自身特有的Name()方法。
介面實現多型
我們很容易擴充套件之前的程式碼,比如你可以聯想到正方形的好兄弟,長方形,於是..
package main
import "fmt"
// 形狀介面
type Shape interface {
Area() float32
}
// 輸出形狀面積
func PrintArea(shape Shape) {
fmt.Printf("The square has area: %f\n", shape.Area())
}
// 正方形結構體
type Square struct {
side float32
}
// 正方形面積
func (sq *Square) Area() float32 {
return sq.side * sq.side
}
// 長方形結構體
type Rectangle struct {
length, width float32
}
// 長方形面積
func (r Rectangle) Area() float32 {
return r.length * r.width
}
func main() {
r := Rectangle{5, 3}
q := &Square{5}
shapes := []Shape{r, q}
fmt.Println("Looping through shapes for area ...")
for key, _ := range shapes {
fmt.Println("Shape details: ", shapes[key])
fmt.Println("Area of this shape is: ", shapes[key].Area())
}
}
在main方法的for迴圈中,雖然只知道shapes[key]是一個Shape物件,但是它卻能自動變成Square或者Rectangle物件,還可以呼叫各自的Area方法。是不是很厲害?
通過上面的例子,我們可以發現:
- 介面其實像一種契約,實現型別必須滿足它(實現其定義的方法)。
- 介面描述了型別的行為,規定型別可以做什麼。
- 介面徹底將型別能做什麼,以及如何做分離開來。
- 這些特點使得相同介面的變數在不同的時刻表現出不同的行為,這就是多型的本質。
使用介面使程式碼更具有普適性。
型別斷言
前面用介面實現多型時,在最後main方法的for迴圈裡,介面型別變數
shapes[key]中可以包含任何型別的值,那麼如何檢測當前的物件是什麼型別的呢?
答案就是使用型別斷言。比如
v := var.(型別名)
這裡的var必需得是介面變數,比如shapes[key]。
如果我們直接這麼寫
v := shapes[key].(*Square)
那肯是會報錯的,因為shapes[key]也可能是Rectangle型別的,為了避免錯誤發生,我們可以使用更安全的方法進行斷言:
if v, ok := shapes[key].(*Square); ok {
// 相關操作
}
如果轉換合法,v 是 shapes[key] 轉換到型別 Square 的值,ok 會是 true;否則 v 是型別 Square 的零值,ok 是 false,也沒有執行時錯誤發生。
備註: 不要忽略
shapes[key].(*Square)
中的*
號,否則會導致編譯錯誤:impossible type assertion: Square does not implement Shape (Area method has pointer receiver)。
方法集與介面
Go 語言規範定義了介面方法集的呼叫規則:
- 型別 T 的可呼叫方法集包含接受者為 T 或 T 的所有方法集
- 型別 T 的可呼叫方法集包含接受者為 T 的所有方法
- 型別 T 的可呼叫方法集不包含接受者為 *T 的方法
舉例說明
package main
import (
"fmt"
)
type List []int
func (l List) Len() int {
return len(l)
}
func (l *List) Append(val int) {
*l = append(*l, val)
}
type Appender interface {
Append(int)
}
func CountInto(a Appender, start, end int) {
for i := start; i <= end; i++ {
a.Append(i)
}
}
type Lener interface {
Len() int
}
func LongEnough(l Lener) bool {
return l.Len()*10 > 42
}
func main() {
// A bare value
var lst List
// compiler error:
// cannot use lst (type List) as type Appender in argument to CountInto:
// List does not implement Appender (Append method has pointer receiver)
// CountInto(lst, 1, 10)
if LongEnough(lst) { // VALID:Identical receiver type
fmt.Printf("- lst is long enough\n")
}
// A pointer value
plst := new(List)
CountInto(plst, 1, 10) //VALID:Identical receiver type
if LongEnough(plst) {
// VALID: a *List can be dereferenced for the receiver
fmt.Printf("- plst is long enough\n")
}
}
在 lst 上呼叫 CountInto 時會導致一個編譯器錯誤,因為 CountInto 需要一個 Appender,而它的方法 Append 只定義在指標上。 在 lst 上呼叫 LongEnough 是可以的,因為 Len 定義在值上。
在 plst 上呼叫 CountInto 是可以的,因為 CountInto 需要一個 Appender,並且它的方法 Append 定義在指標上。 在 plst 上呼叫 LongEnough 也是可以的,因為指標會被自動解引用。
反射
Reflection(反射)在計算機中表示 程式能夠檢查自身結構的能力,尤其是型別。它是超程式設計的一種形式,也是最容易讓人迷惑的一部分。