Golang 基礎:介面使用、實現原理(eface iface)和設計模式

語言: CN / TW / HK

本文是我學習 Go TourGo 語言第一課 介面相關章節的筆記,如有理解不當之處,懇請留言指出,感謝!

定義介面

  • 接口裡的方法,引數要麼都有名字,要麼都沒有,否則報錯:Method specification has both named and unnamed parameters
  • 同時,方法名稱不能重複,哪怕引數不一樣也不可以,否則會報錯:Duplicate method 'XXX

``` type People interface { M1(int) int; M2(string); }

type KnowledgeMan interface { M3(string); }

type StudentRepo interface { //嵌入 People KnowledgeMan } ```

一個介面可以嵌入其他介面,但要求方法如果重名必須引數一致。

實現介面

``` type KnowledgeMan interface { M3(string); }

type Impl struct {

}

//只要包含相同簽名的方法,就算是實現了介面 func (i *Impl)M3(s string) { fmt.Println(s) }

func main() { //&Impl{}: new 一個 Impl var student KnowledgeMan = &Impl{} student.M3("haha") } ```

如上程式碼所示,只要一個型別中定義了介面的所有方法(相同簽名),就算是實現了介面,就可以賦值給這個介面型別的變數。

空介面

空介面:interface{}

空介面的這個抽象對應的事物集合空間包含了 Go 語言世界的所有事物。

go1.18 增加了 any 關鍵字,用以替代現在的 interface{} 空介面型別:type any = interface{},實際上是 interface{} 的別名。

``` //空型別做引數,引數可以傳遞任意型別 func TestEmptyInterface(i interface{}) { fmt.Println(i) }

func main() { //interface{} 是空介面型別,任意型別都認為實現了空介面 var i interface{} = 15 fmt.Println(i)

//引數型別使用空介面的話,可以當作泛型使用
TestEmptyInterface(111)
TestEmptyInterface("shixin")

} ```

上面的程式碼中,先定義了空介面型別的 i,同時賦值為 15,之所以可以這樣,是因為按照前面介面實現的定義“定義了相同簽名方法就算實現了介面”的邏輯,空介面沒有方法,那所有型別都可以說實現了空介面。

空介面的這種特性,可以用作泛型,比如作為方法引數等場景,這樣可以傳遞不同型別的引數。

型別斷言

型別斷言:判斷變數是否為某種介面的實現。

v, ok := i.(T)

i.(T) 的意思是判斷變數 i 是否為 T 的型別。

這要求 i 的型別必須是介面,否則會報錯: Invalid type assertion: intValue.(int64) (non-interface type int64 on left)

舉個例子:

``` var intValue int64 = 123 var anyType interface{} = intValue

//型別匹配,v 是值,ok 是 boolean
v,ok := anyType.(int64)
fmt.Printf("value:%d, ok:%t, type of v: %T\n", v, ok, v)

//如果不是這個型別,v2
v2, ok := anyType.(string)
fmt.Printf("v2 value:%d, ok:%t, type of v: %T\n", v2, ok, v2)

v3 := anyType.(int64)
fmt.Printf("v3 value:%d, type of v: %T\n", v3, v3)

//型別不對,會直接 panic 報錯
v4 := anyType.([]int)
fmt.Printf("v4 value:%d, type of v: %T\n", v4, v4)

```

上面的程式碼中,定義了一個空介面,賦值為一個 int64 型別的值。然後我們判斷型別是否為 int64,輸出結果符合預期。

用一個其他型別判斷的時候,v 會賦值為異常值,但型別會賦值為用於判斷的型別。

執行結果:

``` value:123, ok:true, type of v: int64 v2 value:%!d(string=), ok:false, type of v: string v3 value:123, type of v: int64 panic: interface conversion: interface {} is int64, not []int

goroutine 1 [running]: main.TestInterface() /Users/simon/go/src/awesomeProject/main.go:258 +0x491 main.main() /Users/simon/go/src/awesomeProject/main.go:278 +0x25 exit status 2 ```

開發建議

  • 介面越大,抽象程度越弱。建議介面越小越好,職責單一(一般建議介面方法數量在 3 個以內)
  • 先抽象,然後再優化為小介面,循序漸進

越偏向業務層,抽象難度就越高,儘量在業務以下多抽象分離

介面型別在執行時是如何實現的 🔥

https://time.geekbang.org/column/article/473414

每個介面型別變數在執行時的表示都是由兩部分組成的,型別和資料。

eface(_type, data)和iface(tab, data):

  1. eface 用於表示沒有方法的空介面(empty interface)型別變數,也就是 interface{}型別的變數;
  2. iface 用於表示其餘擁有方法的介面 interface 型別變數。

```

// $GOROOT/src/runtime/runtime2.go type iface struct { tab *itab data unsafe.Pointer }

type eface struct { _type *_type data unsafe.Pointer }

// $GOROOT/src/runtime/runtime2.go type itab struct { inter interfacetype _type _type hash uint32 // copy of _type.hash. Used for type switches. _ [4]byte fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter. }

// $GOROOT/src/runtime/type.go

type _type struct { size uintptr ptrdata uintptr // size of memory prefix holding all pointers hash uint32 tflag tflag align uint8 fieldAlign uint8 kind uint8 // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? equal func(unsafe.Pointer, unsafe.Pointer) bool // gcdata stores the GC type data for the garbage collector. // If the KindGCProg bit is set in kind, gcdata is a GC program. // Otherwise it is a ptrmask bitmap. See mbitmap.go for details. gcdata *byte str nameOff ptrToThis typeOff }

// $GOROOT/src/runtime/type.go type interfacetype struct { typ _type pkgpath name mhdr []imethod }

```

在這裡插入圖片描述

判斷兩個介面變數是否相同,需要判斷 _type/tab 和 data 指向的記憶體資料是否相同。

只有兩個介面型別變數的型別資訊(eface._type/iface.tab._type)相同,且資料指標(eface.data/iface.data)所指資料相同時,兩個介面型別變數才是相等的。

未顯式初始化的介面型別變數的值為nil,這個變數的 _type/tab 和 data 都為 nil。

空介面或非空型別介面沒有賦值,都為 nil

func TestNilInterface() { var i interface{} var e error println(i) //(0x0,0x0) : 型別資訊、資料值資訊均為空 println(e) fmt.Println(i) //<nil> fmt.Println(e) fmt.Println("i == nil", i == nil) fmt.Println("e == nil", e == nil) fmt.Println("i == e", e == i) }

println 可以打印出介面的型別和資料資訊

輸出:

(0x0,0x0) (0x0,0x0) <nil> <nil> i == nil true e == nil true i == e true

介面型別變數的賦值是一種裝箱操作

介面型別的裝箱實際就是建立一個 eface 或 iface 的過程,需要拷貝記憶體,成本較大。

介面設計的 7 個建議 🔥

1.型別組合: - 介面定義中嵌入其他介面,實現功能更多的介面 - 結構體中嵌入介面,等於實現了這個介面 - 結構體中嵌入其他結構體,後面呼叫嵌入的結構體成員,會被“委派”給嵌入的例項

Go 中沒有繼承父類功能的概念,而是通過型別嵌入的方式,組合不同型別的功能。

被嵌入的類不知道誰嵌入了它,也無法向上向下轉型,所以 Go 中沒有“父子類”的繼承關係。

2.用介面作為“關節(連線點)”:在函式定義時,引數要多用介面型別。

3.在建立某一型別例項時可以: “接受介面,返回結構體(Accept interfaces, return structs)”

``` / $GOROOT/src/log/log.go type Logger struct { mu sync.Mutex prefix string flag int out io.Writer buf []byte }

func New(out io.Writer, prefix string, flag int) *Logger { return &Logger{ out: out, prefix: prefix, flag: flag } } ```

4.包裝器模式:引數與返回值一樣,在函式內部做資料過濾、變換等操作

可以將多個接受同一介面型別引數的包裝函式組合成一條鏈來呼叫:

``` // $GOROOT/src/io/io.go func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }

type LimitedReader struct { R Reader // underlying reader N int64 // max bytes remaining }

func (l *LimitedReader) Read(p []byte) (n int, err error) { // ... ... }

func CapReader(r io.Reader) io.Reader { return &capitalizedReader{r: r} }

type capitalizedReader struct { r io.Reader }

func (r *capitalizedReader) Read(p []byte) (int, error) { n, err := r.r.Read(p) if err != nil { return 0, err }

q := bytes.ToUpper(p)
for i, v := range q {
    p[i] = v
}
return n, err

}

func main() { r := strings.NewReader("hello, gopher!\n") r1 := CapReader(io.LimitReader(r, 4)) //鏈式呼叫 if _, err := io.Copy(os.Stdout, r1); err != nil { log.Fatal(err) } } ```

5.介面卡模式:將函式,轉換成特定型別,成為某個介面的實現

```

func greetings(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Welcome!") } func main() { http.ListenAndServe(":8080", http.HandlerFunc(greetings)) } ```

http.HandlerFunc 把 greetings 轉成了 http.Handler 型別: ``` // $GOROOT/src/net/http/server.go func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() }

type Handler interface { ServeHTTP(ResponseWriter, *Request) }

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) } ```

通過型別轉換,HandlerFunc 讓一個普通函式成為實現 ServeHTTP 方法的物件,從而滿足http.Handler介面。

6.中介軟體

中介軟體就是包裝函式,類似責任鏈模式。

在 Go Web 程式設計中,“中介軟體”常常指的是一個實現了 http.Handler 介面的 http.HandlerFunc 型別例項

func main() { http.ListenAndServe(":8080", logHandler(authHandler(http.HandlerFunc(greetings)))) }

7.儘量不要使用空介面型別,編譯器無法做型別檢查,安全沒有保證。

使用interface{}作為引數型別的函式或方法都有一個共同特點,就是它們面對的都是未知型別的資料,所以在這裡使用具有“泛型”能力的interface{}型別

等 Go 泛型落地後,很多場合下 interface{}就可以被泛型替代了。