Go介面詳談

roc_guo發表於2021-07-21
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 istype of s is

在這裡,引出介面的概念。介面有兩種型別。介面的靜態型別是介面本身,例如上述程式中的Shape。介面沒有靜態值,而是指向動態值。

介面型別的變數可以儲存實現介面的型別的值。該型別的值成為介面的動態值,並且該型別成為介面的動態型別。

從上面的示例開始,我們可以看到零值和介面的型別為nil。這是因為,此刻,我們已宣告型別Shaper的變數s,但未分配任何值。當我們使用帶有介面引數的fmt包中的Println函式時,它指向介面的動態值,Printf功能中的%T語法是指動態型別的介面。實際上,介面靜態型別是Shaper。

當我們使用一個型別去實現該介面後,會是什麼效果。

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 istype of s istype of s is main.Rect 
value of s is {5 4} 
area of rect is 20 
s == r is tru

可以看到此時s變成了動態型別,儲存的是main.Rect,值變成了{5,4}。

有時,動態型別的介面也稱為具體型別,因為當我們訪問介面型別時,它會返回其底層動態值的型別,並且其靜態型別保持隱藏。

我們可以在s上呼叫Area方法,因為介面Shaper定義了Area方法,而s的具體型別是Rect,它實現了Area方法。該方法將在介面儲存的動態值上被呼叫。

此外,我們可以看到我們可以使用s與r進行比較,因為這兩個變數都儲存相同的動態型別(Rect型別的結構)和動態值{5 4}。

我們接著使用圓來實現該介面:

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

這裡進一步理解了介面儲存的動態型別。從切片角度出發,可以說,介面也以類似的方式工作,即動態儲存對底層型別的引用。

當我們刪除掉Perimeter的實現,可以看到如下報錯結果。

./rect.go:34:4: cannot use Rect{...} (type Rect) as type Shaper in assignment: 
Rect does not implement Shaper (missing Perimeter method)

從上面的錯誤應該是顯而易見的,為了成功實現介面,需要實現與完全簽名的介面宣告的所有方法。

2.空介面

當一個介面沒有任何方法時,它被稱為空介面。這由介面{}表示。因為空介面沒有方法,所以所有型別都隱式地實現了這個介面。

空介面的作用之一在於:函式可以接收多個不同型別引數。

例如: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}

可以看到空介面的型別與值是動態的。

3.多個介面

在下面的程式中,我們用Area方法建立了Shape介面,用Volume方法建立了Object介面。因為結構型別Cube實現了這兩個方法,所以它實現了這兩個介面。因此,我們可以將結構型別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())

這種呼叫是沒有問題的,呼叫各自動態型別的方法。

那如果是這樣呢?

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的靜態型別是IShape,而o的靜態型別是Object。因為IShape沒有定義Volume方法,Object也沒有定義Area方法,所以我們得到了上面的錯誤。

要使其工作,我們需要以某種方式提取這些介面的動態值,這是一個立方體型別的結構體,立方體實現了這些方法。這可以使用型別斷言來完成。

4.型別斷言

我們可以透過i.(Type)確定介面i的底層動態值,Go將檢查i的動態型別是否與type相同,並返回可能的動態值。

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沒有實現IShape,那麼報錯:

impossible type assertion: 
Cube does not implement IShape (missing Area method)

如果IShape沒有儲存Cube型別,且Cube實現Shape,那麼報錯:

panic: interface conversion: inter.IShape is nil, not inter.Cub

幸運的是,語法中還有另一個返回值:

value, ok := i.(Type)

在上面的語法中,如果i有具體的type型別或type的動態值,我們可以使用ok變數來檢查。如果不是,那麼ok將為假,value將為Type的零值(nil)。

此外,使用型別斷言可以檢查該介面的動態型別是否實現了其他介面,就像前面的IShape的動態型別是Cube,它實現了IShape、Object介面,如下例子:

vaule1, ok1 := s1.(Object) 
value2, ok2 := s1.(Skin) 
fmt.Printf("IShape s的動態型別值是: %v, 該動態型別是否實現了Object介面: %v\n", vaule1, ok1) 
fmt.Printf("IShape s的動態型別值是: %v, 該動態型別是否實現了Skin介面: %v\n", value2, ok2)

輸出:

IShape s的動態型別值是: {3}, 該動態型別是否實現了Object介面: true 
IShape s的動態型別值是:, 該動態型別是否實現了Skin介面: false

型別斷言不僅用於檢查介面是否具有某個給定型別的具體值,而且還用於將介面型別的給定變數轉換為不同的介面型別。

5.型別Switch

在前面的空介面中,我們知道將一個空介面作為函式引數,那麼該函式可以接受任意型別,那如果我有一個需求是:當傳遞的資料型別是字串時,要求全部變為大寫,其他型別不進行操作?

針對這樣的需求,我們可以採用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中,一個介面不能實現或擴充套件其他介面,但我們可以透過合併兩個或多個介面來建立一個新的介面。

例如:

這裡使用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.介面比較

如果基礎動態值為nil,則兩個介面總是相等的,這意味著兩個nil介面總是相等的,因此== operation返回true。

var a, b interface{} 
fmt.Println( a == b ) // true

如果這些介面不是nil,那麼它們的動態型別(具體值的型別)應該相同,具體值應該相等。

如果介面的動態型別不具有可比性,例如slice、map、function,或者介面的具體值是包含這些不可比較性值的複雜資料結構,如切片或陣列,則==或!=操作將導致執行時panic。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69901823/viewspace-2782455/,如需轉載,請註明出處,否則將追究法律責任。

相關文章