溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Golang指針和接口如何使用

發(fā)布時間:2023-02-24 17:19:44 來源:億速云 閱讀:115 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要介紹“Golang指針和接口如何使用”,在日常操作中,相信很多人在Golang指針和接口如何使用問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Golang指針和接口如何使用”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

指針和接口

golang的類型系統(tǒng)其實很有意思,有意思的地方就在于類型系統(tǒng)表面上看起來眾生平等,然而實際上卻要分成普通類型(types)和接口(interfaces)來看待。普通類型也包含了所謂的引用類型,例如slice和map,雖然他們和interface同為引用類型,但是行為更趨近于普通的內(nèi)置類型和自定義類型,因此只有特立獨行的interface會被單獨歸類。

那我們是依據(jù)什么把golang的類型分成兩類的呢?其實很簡單,看類型能不能在編譯期就確定以及調(diào)用的類型方法是否能在編譯期被確定。

如果覺得上面的解釋太過抽象的可以先看一下下面的例子:

package main
 
import "fmt"
 
func main(){
    m := make(map[int]int)
    m[1] = 1 * 2
    m[2] = 2 * 2
    fmt.Println(m)
    m2 := make(map[string]int)
    m2["python"] = 1
    m2["golang"] = 2
    fmt.Println(m2)
}

首先我們來看非interface的引用類型,m和m2明顯是兩個不同的類型,不過實際上在底層他們是一樣的,不信我們用objdump工具檢查一下:

go tool objdump -s 'main\.main' a
 
TEXT main.main(SB) /tmp/a.go
  a.go:6  CALL runtime.makemap_small(SB)     # m := make(map[int]int)
  ...
  a.go:7  CALL runtime.mapassign_fast64(SB)  # m[1] = 1 * 2
  ...
  a.go:8  CALL runtime.mapassign_fast64(SB)  # m[2] = 2 * 2
  ...
  ...
  a.go:10 CALL runtime.makemap_small(SB)     # m2 := make(map[string]int)
  ...
  a.go:11 CALL runtime.mapassign_faststr(SB) # m2["python"] = 1
  ...
  a.go:12 CALL runtime.mapassign_faststr(SB) # m2["golang"] = 2

省略了一些寄存器的操作和無關(guān)函數(shù)的調(diào)用,順便加上了對應(yīng)的代碼的原文,我們可以清晰地看到盡管類型不同,但map調(diào)用的方法都是相同的而且是編譯期就已經(jīng)確定的。如果是自定義類型呢?

package main
 
import "fmt"
 
type Person struct {
    name string
    age int
}
 
func (p *Person) sayHello() {
    fmt.Printf("Hello, I'm %v, %v year(s) old\n", p.name, p.age)
}
 
func main(){
    p := Person{
        name: "apocelipes",
        age: 100,
    }
    p.sayHello()
}

這次我們創(chuàng)建了一個擁有自定義字段和方法的自定義類型,下面再用objdump檢查一下:

go tool objdump -s 'main\.main' b
 
TEXT main.main(SB) /tmp/b.go
  ...
  b.go:19   CALL main.(*Person).sayHello(SB)
  ...

用字面量創(chuàng)建對象和初始化調(diào)用堆棧的匯編代碼不是重點,重點在于那句CALL,我們可以看到自定義類型的方法也是在編譯期就確定了的。

那反過來看看interface會有什么區(qū)別:

package main
 
import "fmt"
 
type Worker interface {
    Work()
}
 
type Typist struct{}
func (*Typist)Work() {
    fmt.Println("Typing...")
}
 
type Programer struct{}
func (*Programer)Work() {
    fmt.Println("Programming...")
}
 
func main(){
    var w Worker = &Typist{}
    w.Work()
    w = &Programer{}
    w.Work()
}

注意!編譯這個程序需要禁止編譯器進(jìn)行優(yōu)化,否則編譯器會把接口的方法查找直接優(yōu)化為特定類型的方法調(diào)用:

go build -gcflags "-N -l" c.go
go tool objdump -S -s 'main\.main' c
 
TEXT main.main(SB) /tmp/c.go
  ...
  var w Worker = &Typist{}
    LEAQ runtime.zerobase(SB), AX
    MOVQ AX, 0x10(SP)
    MOVQ AX, 0x20(SP)
    LEAQ go.itab.*main.Typist,main.Worker(SB), CX
    MOVQ CX, 0x28(SP)
    MOVQ AX, 0x30(SP)
  w.Work()
    MOVQ 0x28(SP), AX
    TESTB AL, 0(AX)
    MOVQ 0x18(AX), AX
    MOVQ 0x30(SP), CX
    MOVQ CX, 0(SP)
    CALL AX
  w = &Programer{}
    LEAQ runtime.zerobase(SB), AX
    MOVQ AX, 0x8(SP)
    MOVQ AX, 0x18(SP)
    LEAQ go.itab.*main.Programer,main.Worker(SB), CX
    MOVQ CX, 0x28(SP)
    MOVQ AX, 0x30(SP)
  w.Work()
    MOVQ 0x28(SP), AX
    TESTB AL, 0(AX)
    MOVQ 0x18(AX), AX
    MOVQ 0x30(SP), CX
    MOVQ CX, 0(SP)
    CALL AX
  ...

這次我們可以看到調(diào)用接口的方法會去在runtime進(jìn)行查找,隨后CALL找到的地址,而不是像之前那樣在編譯期就能找到對應(yīng)的函數(shù)直接調(diào)用。這就是interface為什么特殊的原因:interface是動態(tài)變化的類型。

可以動態(tài)變化的類型最顯而易見的好處是給予程序高度的靈活性,但靈活性是要付出代價的,主要在兩方面。

一是性能代價。動態(tài)的方法查找總是要比編譯期就能確定的方法調(diào)用多花費幾條匯編指令(mov和lea通常都是會產(chǎn)生實際指令的),數(shù)量累計后就會產(chǎn)生性能影響。不過好消息是通常編譯器對我們的代碼進(jìn)行了優(yōu)化,例如c.go中如果我們不關(guān)閉編譯器的優(yōu)化,那么編譯器會在編譯期間就替我們完成方法的查找,實際生產(chǎn)的代碼里不會有動態(tài)查找的內(nèi)容。然而壞消息是這種優(yōu)化需要編譯器可以在編譯期確定接口引用數(shù)據(jù)的實際類型,考慮如下代碼:

type Worker interface {
    Work()
}
 
for _, v := workers {
    v.Work()
}

因為只要實現(xiàn)了Worker接口的類型就可以把自己的實例塞進(jìn)workers切片里,所以編譯器不能確定v引用的數(shù)據(jù)的類型,優(yōu)化自然也無從談起了。

而另一個代價,確切地說其實應(yīng)該叫陷阱,就是接下來我們要探討的主題了。

golang的指針

指針也是一個極有探討價值的話題,特別是指針在reflect以及runtime包里的各種黑科技。不過放輕松,今天我們只用了解下指針的自動解引用。

我們把b.go里的代碼改動一行:

p := &Person{
    name: "apocelipes",
    age: 100,
}

p現(xiàn)在是個指針,其余代碼不需要任何改動,程序依舊可以正常編譯執(zhí)行。對應(yīng)的匯編是這樣的畫風(fēng)(當(dāng)然得關(guān)閉優(yōu)化):

p.sayHello()
    MOVQ AX, 0(SP)
    CALL main.(*Person).sayHello(SB)

對比一下非指針版本:

p.sayHello()
    LEAQ 0x8(SP), AX
    MOVQ AX, 0(SP)
    CALL main.(*Person).sayHello(SB)

與其說是指針自動解引用,倒不如說是非指針版本先求出了對象的實際地址,隨后傳入了這個地址作為方法的接收器調(diào)用了方法。這也沒什么好奇怪的,因為我們的方法是指針接收器:P。

如果把接收器換成值類型接收器:

p.sayHello()
    TESTB AL, 0(AX)
    MOVQ 0x40(SP), AX
    MOVQ 0x48(SP), CX
    MOVQ 0x50(SP), DX
    MOVQ AX, 0x28(SP)
    MOVQ CX, 0x30(SP)
    MOVQ DX, 0x38(SP)
    MOVQ AX, 0(SP)
    MOVQ CX, 0x8(SP)
    MOVQ DX, 0x10(SP)
    CALL main.Person.sayHello(SB)

作為對比:

p.sayHello()
    MOVQ AX, 0(SP)
    MOVQ $0xa, 0x8(SP)
    MOVQ $0x64, 0x10(SP)
    CALL main.Person.sayHello(SB)

這時候golang就是先檢查指針隨后解引用了。同時要注意,這里的方法調(diào)用是已經(jīng)在編譯期確定了的。

指向interface的指針

鋪墊了這么久,終于該進(jìn)入正題了。不過在此之前還有一點小小的預(yù)備知識需要提一下:

A pointer type denotes the set of all pointers to variables of a given type, called the base type of the pointer. --- go language spec

換而言之,只要是能取地址的類型就有對應(yīng)的指針類型,比較巧的是在golang里引用類型是可以取地址的,包括interface。

有了這些鋪墊,現(xiàn)在我們可以看一下我們的說唱歌手程序了:

package main
 
import "fmt"
 
type Rapper interface {
    Rap() string
}
 
type Dean struct {}
 
func (_ Dean) Rap() string {
    return "Im a rapper"
}
 
func doRap(p *Rapper) {
    fmt.Println(p.Rap())
}
 
func main(){
    i := new(Rapper)
    *i = Dean{}
    fmt.Println(i.Rap())
    doRap(i)
}

問題來了,小青年Dean能圓自己的說唱夢么?

很遺憾,編譯器給出了反對意見:

# command-line-arguments
./rapper.go:16:18: p.Rap undefined (type *Rapper is pointer to interface, not interface)
./rapper.go:22:18: i.Rap undefined (type *Rapper is pointer to interface, not interface)

也許type *XXX is pointer to interface, not interface這個錯誤你并不陌生,你曾經(jīng)也犯過用指針指向interface的錯誤,經(jīng)過一番搜索后你找到了一篇教程,或者是博客,有或者是隨便什么地方的資料,他們都會告訴你不應(yīng)該用指針去指向接口,接口本身是引用類型無需再用指針去引用。

其實他們只說對了一半,事實上只要把i和p改成接口類型就可以正常編譯運行了。沒說對的一半是指針可以指向接口,也可以使用接口的方法,但是要繞些彎路(當(dāng)然,用指針引用接口通常是多此一舉,所以聽從經(jīng)驗之談也沒什么不好的):

func doRap(p *Rapper) {
    fmt.Println((*p).Rap())
}
 
func main(){
    i := new(Rapper)
    *i = Dean{}
    fmt.Println((*i).Rap())
    doRap(i)
}

go run rapper.go 
 
Im a rapper
Im a rapper

神奇的一幕出現(xiàn)了,程序不僅沒報錯而且運行得很正常。但是這和golang對指針的自動解引用有什么區(qū)別呢?明明看起來都一樣但就是第一種方案會報
找不到Rap方法?

為了方便觀察,我們把調(diào)用語句單獨抽出來,然后查看未優(yōu)化過的匯編碼:

s := (*p).Rap()
  0x498ee1              488b842488000000        MOVQ 0x88(SP), AX
  0x498ee9              8400                    TESTB AL, 0(AX)
  0x498eeb              488b08                  MOVQ 0(AX), CX
  0x498eee              8401                    TESTB AL, 0(CX)
  0x498ef0              488b4008                MOVQ 0x8(AX), AX
  0x498ef4              488b4918                MOVQ 0x18(CX), CX
  0x498ef8              48890424                MOVQ AX, 0(SP)
  0x498efc              ffd1                    CALL CX

拋開手工解引用的部分,后6行其實和直接使用interface進(jìn)行動態(tài)查詢是一樣的。真正的問題其實出在自動解引用上:

p.sayHello()
    TESTB AL, 0(AX)
    MOVQ 0x40(SP), AX
    MOVQ 0x48(SP), CX
    MOVQ 0x50(SP), DX
    MOVQ AX, 0x28(SP)
    MOVQ CX, 0x30(SP)
    MOVQ DX, 0x38(SP)
    MOVQ AX, 0(SP)
    MOVQ CX, 0x8(SP)
    MOVQ DX, 0x10(SP)
    CALL main.Person.sayHello(SB)

不同之處就在于這個CALL上,自動解引用時的CALL其實是把指針指向的內(nèi)容視作_普通類型_,因此會去靜態(tài)查找方法進(jìn)行調(diào)用,而指向的內(nèi)容是interface的時候,編譯器會去interface本身的數(shù)據(jù)結(jié)構(gòu)上去查找有沒有Rap這個方法,答案顯然是沒有,所以爆了p.Rap undefined錯誤。

那么interface的真實長相是什么呢,我們看看go1.15.2的實現(xiàn):

// src/runtime/runtime2.go
// 因為這邊沒使用空接口,所以只節(jié)選了含數(shù)據(jù)接口的實現(xiàn)
type iface struct {
    tab  *itab
    data unsafe.Pointer
}
 
// 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.
}
 
// src/runtime/type.go
type imethod struct {
    name nameOff
    ityp typeOff
}
 
type interfacetype struct {
    typ     _type
    pkgpath name
    mhdr    []imethod // 類型所包含的全部方法
}
 
// 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
}

沒有給出定義的類型都是對各種整數(shù)類型的typing alias。interface實際上就是存儲類型信息和實際數(shù)據(jù)的struct,自動解引用后編譯器是直接查看內(nèi)存內(nèi)容的(見匯編),這時看到的其實是iface這個普通類型,所以靜態(tài)查找一個不存在的方法就失敗了。而為什么手動解引用的代碼可以運行?因為我們手動解引用后編譯器可以推導(dǎo)出實際類型是interface,這時候編譯器就很自然地用處理interface的方法去處理它而不是直接把內(nèi)存里的東西尋址后塞進(jìn)寄存器。

到此,關(guān)于“Golang指針和接口如何使用”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI