Golang---基本類型(interface)


    摘要:今天我們來學習 Golang 中的 interface 類型。

 

Go 的 5 個關鍵點

interface 是一種類型

type Animal interface {
    SetName(string)
    GetName() string
}

  首先 interface 是一種類型,從它的定義中就可以看出用了 type 關鍵字,更准確的說 interface 是一種具有一組方法的類型,這些方法定義了 interface 的行為。Go 允許不帶任何方法的 interface, 這種類型的 interface 叫 empty interface。如果一個類型實現了一個 interface 中所有的方法,我們說該類型實現了該 interface, 所以所有類型都實現了 empty interface, Go 沒有顯式的關鍵字用來實現 interface, 只需要實現 interface 包含的方法即可。

interface 變量存儲的是實現者的值

package main

import (
    "fmt"
)

type Animal interface {
    SetName(string)
    GetName() string
}

type Cat struct {
    Name string
}

func (c Cat) SetName(name string) {
    fmt.Println("c addr in: ", c)
    c.Name = name
    fmt.Println(c.GetName())
}

func (c Cat) GetName() string {
    return c.Name
}

func main() {
    // c := &Cat{Name: "Cat"}
    // fmt.Println("c addr out: ", c)
    // c.SetName("DogCat")
    // fmt.Println(c.GetName())
    c := Cat{}
    var i Animal
    i = &c  //把變量賦值給一個 interface
    fmt.Println(i.GetName())
    
}
interface

   如果有多種類型實現了某個 interface, 這些類型的值都可以直接使用 interface 的變量存儲。不難看出 interface 的變量中存儲的是實現了 interface 的類型的對象值,這種能力是 duck typing。在使用 interface 時,不需要顯式在 struct 上聲明要實現哪個 interface, 只需要實現對應的 interface 中的方法即可,Go 會在運行時執行從其它類型到 interface 的自動轉換。

如何判斷 interface 變量存儲的是哪種類型的值

  當一個 interface 被多個類型實現時, 有時候我們需要區分 interface 的變量究竟存儲的是哪種類型的值, Go 可以使用 comma, ok 的形式做區分 value, ok := em.(T) : em 是 interface 類型的變量, T 代表要斷言的類型, value 是 interface 變量存儲的值, ok 是 bool 類型標識是否為該斷言的類型 T。

c := Cat{Name: "cat"}
var i Animal i = &c //把變量賦值給一個 interface if t, ok := i.(*Cat); ok { fmt.Println("c implement i:", t) }
//當然我們也可以用 switch 語句
switch t := i.(*Cat) {
case *S:
fmt.Println("i store *S", t)
case *R:
fmt.Println("i store *R", t)
}

空 interface

  interface{} 是一個空的 interface 類型, 空的 interface 沒有方法,所以可以認為所有的類型都實現了 interface{}。如果定義一個函數參數是 interface{} 類型, 這個函數應該可以接受任何類型作為它的參數。

func doSomething(v interface{}) {
    //do something
}

注意:在函數內部 v 並不是任何類型,在函數參數傳遞的過程中,任何類型都被自動轉換為 interface{}。 關於 Go 是如何轉換的,可以參考這里

另外:既然空的 interface 可以接受任何類型的參數,那么一個 interface{} 類型的 slice 是不是就可以接受任何類型的 slice?

func printAll(vals []interface{}) { //1
    for _, val := range vals {
        fmt.Println(val)
    }
}

func main(){
    names := []string{"stanley", "david", "oscar"}
    printAll(names)
}

//err:cannot use names (type []string) as type []interface {} in argument to printAll

  上述示例代碼中,我們將 []string 轉換為 []interface{}, 但是我們編譯的時候報錯,這說明 Go 並沒有幫助我們自動把 slice 轉換為 interface{} 類型的 slice, 所以出錯了。為什么不幫我們自動轉換,相關說明在這里查看。但是我們可以手動進行轉換來達到我們的目的:

var dataSlice []int = foo()
var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
for i, v := range dataSlice {
    interfaceSlice[i] = v
}

選擇 interface 的實現者

package main

import (
    "fmt"
)

type Animal interface {
    SetName(string)
    GetName() string
}

type Cat struct {
    Name string
}

func (c Cat) SetName(name string) {
    fmt.Println("c addr in: ", c)
    c.Name = name
    fmt.Println(c.GetName())
}

func (c Cat) GetName() string {
    return c.Name
}


func main() {
    c := &Cat{Name: "Cat"}  //指針調用
    //cc := Cat{Name: "Cat"}  //值調用
    fmt.Println("c addr out: ", c)
    c.SetName("DogCat")
    fmt.Println(c.GetName())
    c := Cat{Name: "cat"}

    fmt.Println(i.GetName())

}

  上面代碼中,接受者是個 value receiver。但是 interface 定義時並沒有嚴格規定實現者的方法 receiver 是個 value receiver 還是 pointer receiver。如果接收者是 value receiver, 那么在方法內對這個接收者所做的修改都不會影響到調用者,這和 C++ 中的 “值傳遞” 類似,例如:

package main

import (
    "fmt"
)

type Animal interface {
    SetName(string)
    GetName() string
}

type Cat struct {
    Name string
}

func (c Cat) SetName(name string) {
    fmt.Println("c addr in: ", c)  // c addr in:  {Cat}, 內部會把指針對應的值取出來,進行值調用
    c.Name = name
    fmt.Println(c.GetName()) //print DogCat
}

func (c Cat) GetName() string {
    return c.Name
}

func main() {
    c := &Cat{Name: "Cat"} //指針調用,but receiver is value receiver
    fmt.Println("c addr out: ", c)  // c addr out:  &{Cat} 外部還是指針類型的變量
    c.SetName("DogCat")
    fmt.Println(c.GetName()) //print Cat

}

  注意:如果 receiver 是 pointer receiver, 通過 value 進行調用,則會編譯保持,提示該類型沒有實現這個 interface, 這可以理解為:如果是 pointer 調用,go 會自動進行轉換,因為有了指針總能得到指針指向的值是什么,如果是 value, go 將無法得知 value 的原始值是什么,因為 value 僅僅是份拷貝。go 會把指針進行隱式轉換得到 value, 但反過來不行。

Go interface 的底層實現

interface 底層結構

func foo(x interface{}) {
    if x == nil {
        fmt.Println("empty interface")
        return
    }
    fmt.Println("non-empty interface")
}

func main() {
    var x *int = nil
    foo(x)  //print non-empty interface
}

  通過上述的代碼,我們或許有些疑惑,那就帶着疑惑往下看吧,了解 go 是怎么把一種類型轉換為 interface 類型的。

  首先,根據 interface 是否包含 method, 底層實現上用了兩種不同的 struct 來表示:iface 和 eface。eface 表示不含 method 的 interface 結構,或者叫 empty interface。對於 Golang 中的大部分數據類型都可以抽象成 _type 結構,同時針對不同的類型還會有一些其他信息。

type eface struct {
    _type *_type  //接口指向的數據類型
    data  unsafe.Pointer  //接口指向的數據的值
}

type _type struct {
    size       uintptr  // type size
    ptrdata    uintptr // size of memory prefix holding all pointers
    hash       uint32  // hash of type
    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
}

  iface 表示 non-empty interface 的底層實現。包含一些 method。method 的具體實現存放在 itab.fun 變量里,如果 interface 包含多個 method, 這里只有一個 fun 變量怎么存呢?等會根據具體的例子來說明這個問題。我們先來看一下 iface 這個結構體:

type iface struct {
    tab  *itab  //包含函數的聲明和具體實現
    data unsafe.Pointer  //指向數據的指針
}

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.  //函數的具體實現,如果 fun[0] == 0, 移位着沒有實現 interfacettpe 中聲明的函數
}

type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod  //包含函數的聲明
}

Q1: 一個 [1]uintptr 如何存多個 method ? [此處完全來自文末參考資料]

A1: 我們通過匯編代碼來看一下:

package main

type MyInterface interface {
    Print()
    Hello()
    World()
    AWK()
}

func Foo(me MyInterface) {
    me.Print()
    me.Hello()
    me.World()
    me.AWK()
}

type MyStruct struct {}

func (me MyStruct) Print() {}
func (me MyStruct) Hello() {}
func (me MyStruct) World() {}
func (me MyStruct) AWK() {}

func main() {
    var me MyStruct
    Foo(me)
}
example

通過對其反匯編可以得到:

$ go build -gcflags '-l' -o main main.go
$ go tool objdump -s main
TEXT main.Foo(SB) TEXT main.Foo(SB) C:/Users/sweenzhang/learnGo/main.go
    interface8.go:10    0x104c060   65488b0c25a0080000  GS MOVQ GS:0x8a0, CX
    interface8.go:10    0x104c069   483b6110        CMPQ 0x10(CX), SP
    interface8.go:10    0x104c06d   7668            JBE 0x104c0d7
    interface8.go:10    0x104c06f   4883ec10        SUBQ $0x10, SP
    interface8.go:10    0x104c073   48896c2408      MOVQ BP, 0x8(SP)
    interface8.go:10    0x104c078   488d6c2408      LEAQ 0x8(SP), BP
    interface8.go:11    0x104c07d   488b442418      MOVQ 0x18(SP), AX
    interface8.go:11    0x104c082   488b4830        MOVQ 0x30(AX), CX //取得 Print 函數地址
    interface8.go:11    0x104c086   488b542420      MOVQ 0x20(SP), DX
    interface8.go:11    0x104c08b   48891424        MOVQ DX, 0(SP)
    interface8.go:11    0x104c08f   ffd1            CALL CX     // 調用 Print()
    interface8.go:12    0x104c091   488b442418      MOVQ 0x18(SP), AX
    interface8.go:12    0x104c096   488b4828        MOVQ 0x28(AX), CX //取得 Hello 函數地址
    interface8.go:12    0x104c09a   488b542420      MOVQ 0x20(SP), DX
    interface8.go:12    0x104c09f   48891424        MOVQ DX, 0(SP)
    interface8.go:12    0x104c0a3   ffd1            CALL CX           //調用 Hello()
    interface8.go:13    0x104c0a5   488b442418      MOVQ 0x18(SP), AX
    interface8.go:13    0x104c0aa   488b4838        MOVQ 0x38(AX), CX //取得 World 函數地址
    interface8.go:13    0x104c0ae   488b542420      MOVQ 0x20(SP), DX 
    interface8.go:13    0x104c0b3   48891424        MOVQ DX, 0(SP)
    interface8.go:13    0x104c0b7   ffd1            CALL CX           //調用 World()
    interface8.go:14    0x104c0b9   488b442418      MOVQ 0x18(SP), AX
    interface8.go:14    0x104c0be   488b4020        MOVQ 0x20(AX), AX //取得 AWK 函數地址
    interface8.go:14    0x104c0c2   488b4c2420      MOVQ 0x20(SP), CX
    interface8.go:14    0x104c0c7   48890c24        MOVQ CX, 0(SP)
    interface8.go:14    0x104c0cb   ffd0            CALL AX           //調用 AWK()
    interface8.go:15    0x104c0cd   488b6c2408      MOVQ 0x8(SP), BP
    interface8.go:15    0x104c0d2   4883c410        ADDQ $0x10, SP
    interface8.go:15    0x104c0d6   c3          RET
    interface8.go:10    0x104c0d7   e8f48bffff      CALL runtime.morestack_noctxt(SB)
    interface8.go:10    0x104c0dc   eb82            JMP main.Foo(SB)
反匯編代碼

其中 0x18(SP) 對應的 itab 的值。fun 在 x86-64 機器上對應 itab 內的地址偏移為 8+8+8+4+4 = 32 = 0x20,也就是 0x20(AX) 對應的 fun 的值,此時存放的 AWK 函數地址。然后 0x28(AX) = &Hello,0x30(AX) = &Print,0x38(AX) = &World。對的,每次函數是按字典序排序存放的。

我們再來看一下函數地址究竟是怎么寫入的?首先 Golang 中的 uintptr 一般用來存放指針的值,這里對應的就是函數指針的值(也就是函數的調用地址)。但是這里的 fun 是一個長度為 1 的 uintptr 數組。我們看一下 runtime 包的 additab 函數

func additab(m *itab, locked, canfail bool) {
    ...
    *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
    ...
}

上面的代碼的意思是在 fun[0] 的地址后面依次寫入其他 method 對應的函數指針。熟悉 C++ 的同學可以類比 C++ 的虛函數表指針來看。

Type Assertion(類型斷言)

 我們知道使用 interface type assertion (中文一般叫斷言) 的時候需要注意,不然很容易引入 panic。

func do(v interface{}) {
    n := v.(int)    // might panic
}

func do(v interface{}) {
    n, ok := v.(int)
    if !ok {
        // 斷言失敗處理
    }
}

這個過程體現在下面的幾個函數上:

// The assertXXX functions may fail (either panicking or returning false,
// depending on whether they are 1-result or 2-result).
func assertI2I(inter *interfacetype, i iface) (r iface) {
    tab := i.tab
    if tab == nil {
        // explicit conversions require non-nil interface value.
        panic(&TypeAssertionError{"", "", inter.typ.string(), ""})
    }
    if tab.inter == inter {
        r.tab = tab
        r.data = i.data
        return
    }
    r.tab = getitab(inter, tab._type, false)
    r.data = i.data
    return
}
func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {
    tab := i.tab
    if tab == nil {
        return
    }
    if tab.inter != inter {
        tab = getitab(inter, tab._type, true)
        if tab == nil {
            return
        }
    }
    r.tab = tab
    r.data = i.data
    b = true
    return
}

// 類似
func assertE2I(inter *interfacetype, e eface) (r iface)
func assertE2I2(inter *interfacetype, e eface) (r iface, b bool)

總結

 從某種意義上來說,Golang 的 interface 也是一種多態的體現。對比其他支持多態特性的語言,實現還是略有差異,很難說誰好誰壞

參考資料

https://research.swtch.com/interfaces

http://legendtkl.com/2017/07/01/golang-interface-implement/

https://sanyuesha.com/2017/07/22/how-to-understand-go-interface/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM