Go推薦使用面向組合編程,而interface是實現組合編程的重要組成部分,本篇文章将對interface底層實現原理,interface如何設計,如何正确使用接口類型。
什麼是接口?接口就是規範好一些行為但是不具體去實現,就好像編程一樣任何語言都有自己的語法規範,但是具體怎麼寫還是得碼農去做。
Go中的接口具有動态和靜态兩種特性
動态特性就是在程序運行時存儲在接口類型變量中的真實類型,這樣可以實現不同數據類型調用同一個interface實現不同的功能來支持多态的特性。
接口類型變量被賦值之後檢查賦值的類型是否實現了interface定義的所有方法
interface底層怎麼實現的?那必須得閱讀go runtime的源碼找到interface的結構體類型, 先看下源碼具體的代碼
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct{
_type *_type
data unsafe.Pointer
}
從代碼中可以看出接口有iface跟eface兩個不一樣的結構體類型,eface表示空接口(即接口類型沒有定義方法),那iface就表示帶有方法的接口類型。這兩種結構的共同點是都有兩個指針字段,并且第二個指針字段的功用相同,都指向當前賦值給該接口類型變量的動态類型變量的值。
不同的是eface表示的空接口類型并無方法列表,因此它的第一個指針字段指向一個_type類型結構,該結構為該接口類型變量的動态類型的信息,下面是源碼_type類型的結構體
iface除了要存儲動态類型信息之外,還要存儲接口本身的信息(接口的類型信息、方法列表信息等)以及動态類型所實現的方法的信息,因此iface的第一個字段指向一個itab類型結構,下面是iface第一個字段itab的類型結構
上面itabl結構中的第一個字段inter指向interfacetype類型結構,這個結構存儲着改接口類型自身的信息,interfacetype類型定義如下。
package main
import "fmt"
type efaceType struct {
n int
s string
}
func main() {
var t = efaceType{
n: 20,
s: "jack",
}
var i interface{} = t
fmt.Println(i)
}
// result {20 jack}
package main
import "fmt"
type ifaceType struct {
n int
s string
}
func (i ifaceType) method1() {}
func (i ifaceType) method2() {}
type ifaceTypeInterface interface {
method1()
method2()
}
func main() {
var t = ifaceType{
n: 20,
s: "jack",
}
var i ifaceTypeInterface = t
fmt.Println(i)
}
與結構體struct 類似,我們需要創建一個派生類型來使用關鍵字interface簡化接口聲明。
type Shape interface {
Area() float64
Perimeter() float64
}
在上面的代碼示例中,定義了Shape接口,它有兩個方法 Area 和 Permeter,它們不接受任何參數并返回 float64值。實現這些方法的任何類型(具有精确的方法簽名)也将實現塑形接口。由于接口是類似于 struct 的類型,所以我們可以創建其類型的變量。在上面的例子中,我們可以創建一個類型為接口形狀的變量 s。
接口有兩種類型。接口的靜态類型是接口本身,例如上面程序中的形狀接口沒有靜态值,而是指向動态值。接口類型的變量可以保存實現該接口的類型的值。該類型的值成為接口的動态值,該類型成為接口的動态類型。從上面的示例中,我們可以看到接口的值和類型為零。這是因為,此時,我們已經聲明了變量 s 的形狀類型,但沒有賦予任何值。當我們使用帶接口參數的 fmt 包中的 Println 函數時,它指向接口的動态值,而 Printf 函數中的%T 語法指的是接口的動态類型。
實現接口我們使用Shape接口提供的簽名來聲明 Area 和 Permeter 方法。另外讓我們創建 Rect struct 并讓它實現 Shape接口方法。
在上面的程序中,我們創建了Shape接口和結構類型 Rect。然後我們定義了屬于 Rect 類型的面積和周長方法,因此 Rect 實現了這些方法。這些方法是由Shape接口定義的,所以 Rect 結構類型實現了Shape接口。因為我們還沒有強制 Rect 實現Shape接口,所以這一切都是自動發生的。因此我們可以說 Go 中的接口是隐式實現的。
當類型實現接口時,該類型的變量也可以表示為接口的類型。我們可以确認,通過創建一個 nil 接口的形狀類型和指定一個結構類型 Rect。
因為 Rect 實現了Shape接口,所以這是完全有效的。從上面的結果,我們可以看到,動态類型的 s 現在是 Rect 和動态值的 s 是結構 Rect 的值是{5,4}。
動态類型的接口也稱為具體類型,因為當我們訪問接口類型時,它返回其底層動态值的類型,而其靜态類型保持隐藏。
我們可以在 變量s 上調用 Area 方法,因為接口Shape定義了 Area 方法,而 s 的具體類型是 Rect,它實現了 Area 方法。此方法将在動态值接口的持有者上調用。
另外我們可以看到比較 s 和 r,因為這兩個變量擁有相同的動态類型(Rect 類型的結構)和動态值{5,4}。
在上面的程序中,我們移除了Perimeter方法。這個程序不會編譯,編譯器會抛出一個錯誤。
program.go:22: cannot use Rect literal (type Rect) as type Shape in assignment:
Rect does not implement Shape (missing Perimeter method)
從上面的錯誤可以明顯看出,為了成功地實現一個接口,您需要實現該接口聲明的所有的方法。
空接口當一個接口的方法為零時,它被稱為空接口。這由interface{}表示。由于空接口沒有方法,所以所有類型都隐式實現此接口。fmt 内置包的 Println 函數如何接受不同類型的值作為參數?因為它是一個空接口。讓我們看看 Println 函數。
func Println(a ...interface{}) (n int, err error)
在上面的程序中,我們創建了一個自定義字符串類型 MyString 和一個結構類型 Rect。explain函數接受空接口類型的參數,因此可以傳遞 MyString、 Rect 或其他類型的變量。
多接口一個類型可以實現多個接口。
在上面的程序中,我們創建了面積方法的Shape接口和Area方法的對象接口。由于 struct 類型 Cube 實現了這兩個方法,因此它實現了這兩個接口。因此我們可以将 struct 類型 Cube 的值賦給 Shape 或 Object 類型的變量。
如果我們使用變量s去調用接口Object裡面的Volume方法,變量o去調用Shape接口裡面的Area方法會發生什麼?
program.go:31: s.Volume undefined (type Shape has no field or method Volume)
program.go:32: o.Area undefined (type Object has no field or method Area)
程序不能編譯,因為 s 的靜态類型是 Shape,o 的靜态類型是 Object。由于Shape沒有定義的Volume方法和Object沒有定義的Area方法,我們得到上述錯誤。
為了讓代碼順利編譯,我們需要以某種方式提取這些接口的動态值可以通過使用類型斷言來完成。
類型斷言我們可以使用語法 i.(Type)查找接口的底層動态值,其中 i 是類型接口的變量,Type 是實現接口的類型。Go 将檢查動态類型 i 是否與 Type 相同。類型斷言用于斷言判斷一個變量是某種類型。類型斷言隻能在接口上發生。
斷言interface i的值類型是T,如果是,t則為interface i的值,并且t的類型也為interface i的值類型;如果不是,則panic報錯,類似:panic: interface conversion: interface {} is int, not float64
t, ok := i.(T)
package main
import "fmt"
func main() {
var i interface{} = 100
s, ok := i.(int)
fmt.Println(s, ok)
fmt.Printf("%T\n", s)
f, ok := i.(float64)
fmt.Println(f, ok)
fmt.Printf("%T\n", f)
}
// result
// 100 true
// int
// 0 false
// float64
注意:我們需要使用類型斷言來獲取接口的動态值,這樣我們就可以訪問該動态值的屬性。例如,您不能訪問類型接口對象上的結構字段,即使它具有結構的動态值。簡而言之,訪問接口類型沒有表示的任何内容都會導緻運行時panic。因此,确保在需要時使用類型斷言。
類型轉換類型切換的語法類似于類型斷言,它是 i.(type) ,其中 i 是接口,type 是固定的關鍵字。使用它我們可以得到接口的動态類型,而不是動态值。
在上面的程序中,explain函數以使用類型轉換。當使用任何類型調用explain函數時,i 接收它的動态值和動态類型。
通過在 switch 中使用 i.(type)語句,我們可以訪問該動态類型。使用switch塊中的case,我們可以根據接口 i 的動态類型執行條件操作。
接口嵌套在 Go 中,一個接口不能實現或擴展其他接口,但是我們可以通過合并兩個或多個接口來創建一個新接口。讓我們重寫我們的Shape-Cube程序。
在上面的程序中,由于 Cube 實現了 Area 方法和Volume方法,所以它實現了Shape和Object接口。但是由于接口 Material 是這些接口的嵌入式接口,Cube 也必須實現Shape跟Object所有方法。匿名嵌套結構一樣,嵌套接口的所有方法都被提升到父接口。
指針與值接收器
在上面的程序中,Area 方法屬于 Rect 指針類型,因此它的接收者将獲得 Area 類型變量的指針。但是上面的程序不會編譯,Go 會抛出編譯錯誤。
program.go:27: cannot use Rect literal (type Rect) as type Shape in
assignment: Rect does not implement Shape (Area method has pointer receiver)
上面的代碼編碼報錯是因為發生了值傳遞才會導緻出現這個問題。實際上不管接收者類型是值類型還是指針類型,都可以通過值類型或指針類型調用,這裡面實際上通過語法糖起作用的。實現了接收者是值類型的方法,相當于自動實現了接收者是指針類型的方法;而實現了接收者是指針類型的方法,不會自動生成對應接收者是值類型的方法。
上面的代碼我們使用指向 r 的指針,而不是 r 的值,這樣 s 的動态類型就變成了 * Rect,它實現了 Area 方法。然而,即使使用 * Area 沒有實現 Permeter,s.Permeter ()調用也沒有失敗。是因為如果實現了接收者是值類型的方法,會隐含地也實現了接收者是指針類型的方法
接口對比可以将兩個接口使用== 和!= 運算符進行對比。如果底層動态值為 nil,則兩個接口始終相等,這意味着兩個 nil 接口始終相等,因此 = = 返回 true。
var a, b interface{}
fmt.Println( a == b ) // true
如果這些接口不為空,那麼它們的動态類型(具體值的類型)應該相同,具體值應該相等。
如果接口的動态類型不具有可比性,比如切片、映射和函數,或者接口的具體值是包含這些不可比性值的複雜數據結構,比如切片或數組,那麼 == 或!= 操作将導緻運行時panic。如果一個接口為 nil,那麼 == 操作将總是返回 false。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!