您好,登錄后才能下訂單哦!
這篇文章主要介紹“Golang指針和接口如何使用”,在日常操作中,相信很多人在Golang指針和接口如何使用問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Golang指針和接口如何使用”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
golang的類型系統其實很有意思,有意思的地方就在于類型系統表面上看起來眾生平等,然而實際上卻要分成普通類型(types)和接口(interfaces)來看待。普通類型也包含了所謂的引用類型,例如slice和map,雖然他們和interface同為引用類型,但是行為更趨近于普通的內置類型和自定義類型,因此只有特立獨行的interface會被單獨歸類。
那我們是依據什么把golang的類型分成兩類的呢?其實很簡單,看類型能不能在編譯期就確定以及調用的類型方法是否能在編譯期被確定。
如果覺得上面的解釋太過抽象的可以先看一下下面的例子:
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
省略了一些寄存器的操作和無關函數的調用,順便加上了對應的代碼的原文,我們可以清晰地看到盡管類型不同,但map調用的方法都是相同的而且是編譯期就已經確定的。如果是自定義類型呢?
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() }
這次我們創建了一個擁有自定義字段和方法的自定義類型,下面再用objdump檢查一下:
go tool objdump -s 'main\.main' b
TEXT main.main(SB) /tmp/b.go
...
b.go:19 CALL main.(*Person).sayHello(SB)
...
用字面量創建對象和初始化調用堆棧的匯編代碼不是重點,重點在于那句CALL,我們可以看到自定義類型的方法也是在編譯期就確定了的。
那反過來看看interface會有什么區別:
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() }
注意!編譯這個程序需要禁止編譯器進行優化,否則編譯器會把接口的方法查找直接優化為特定類型的方法調用:
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
...
這次我們可以看到調用接口的方法會去在runtime進行查找,隨后CALL找到的地址,而不是像之前那樣在編譯期就能找到對應的函數直接調用。這就是interface為什么特殊的原因:interface是動態變化的類型。
可以動態變化的類型最顯而易見的好處是給予程序高度的靈活性,但靈活性是要付出代價的,主要在兩方面。
一是性能代價。動態的方法查找總是要比編譯期就能確定的方法調用多花費幾條匯編指令(mov和lea通常都是會產生實際指令的),數量累計后就會產生性能影響。不過好消息是通常編譯器對我們的代碼進行了優化,例如c.go中如果我們不關閉編譯器的優化,那么編譯器會在編譯期間就替我們完成方法的查找,實際生產的代碼里不會有動態查找的內容。然而壞消息是這種優化需要編譯器可以在編譯期確定接口引用數據的實際類型,考慮如下代碼:
type Worker interface { Work() } for _, v := workers { v.Work() }
因為只要實現了Worker接口的類型就可以把自己的實例塞進workers切片里,所以編譯器不能確定v引用的數據的類型,優化自然也無從談起了。
而另一個代價,確切地說其實應該叫陷阱,就是接下來我們要探討的主題了。
指針也是一個極有探討價值的話題,特別是指針在reflect以及runtime包里的各種黑科技。不過放輕松,今天我們只用了解下指針的自動解引用。
我們把b.go里的代碼改動一行:
p := &Person{ name: "apocelipes", age: 100, }
p現在是個指針,其余代碼不需要任何改動,程序依舊可以正常編譯執行。對應的匯編是這樣的畫風(當然得關閉優化):
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)
與其說是指針自動解引用,倒不如說是非指針版本先求出了對象的實際地址,隨后傳入了這個地址作為方法的接收器調用了方法。這也沒什么好奇怪的,因為我們的方法是指針接收器: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就是先檢查指針隨后解引用了。同時要注意,這里的方法調用是已經在編譯期確定了的。
鋪墊了這么久,終于該進入正題了。不過在此之前還有一點小小的預備知識需要提一下:
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
換而言之,只要是能取地址的類型就有對應的指針類型,比較巧的是在golang里引用類型是可以取地址的,包括interface。
有了這些鋪墊,現在我們可以看一下我們的說唱歌手程序了:
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這個錯誤你并不陌生,你曾經也犯過用指針指向interface的錯誤,經過一番搜索后你找到了一篇教程,或者是博客,有或者是隨便什么地方的資料,他們都會告訴你不應該用指針去指向接口,接口本身是引用類型無需再用指針去引用。
其實他們只說對了一半,事實上只要把i和p改成接口類型就可以正常編譯運行了。沒說對的一半是指針可以指向接口,也可以使用接口的方法,但是要繞些彎路(當然,用指針引用接口通常是多此一舉,所以聽從經驗之談也沒什么不好的):
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
神奇的一幕出現了,程序不僅沒報錯而且運行得很正常。但是這和golang對指針的自動解引用有什么區別呢?明明看起來都一樣但就是第一種方案會報
找不到Rap方法?
為了方便觀察,我們把調用語句單獨抽出來,然后查看未優化過的匯編碼:
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進行動態查詢是一樣的。真正的問題其實出在自動解引用上:
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其實是把指針指向的內容視作_普通類型_,因此會去靜態查找方法進行調用,而指向的內容是interface的時候,編譯器會去interface本身的數據結構上去查找有沒有Rap這個方法,答案顯然是沒有,所以爆了p.Rap undefined錯誤。
那么interface的真實長相是什么呢,我們看看go1.15.2的實現:
// src/runtime/runtime2.go // 因為這邊沒使用空接口,所以只節選了含數據接口的實現 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 }
沒有給出定義的類型都是對各種整數類型的typing alias。interface實際上就是存儲類型信息和實際數據的struct,自動解引用后編譯器是直接查看內存內容的(見匯編),這時看到的其實是iface這個普通類型,所以靜態查找一個不存在的方法就失敗了。而為什么手動解引用的代碼可以運行?因為我們手動解引用后編譯器可以推導出實際類型是interface,這時候編譯器就很自然地用處理interface的方法去處理它而不是直接把內存里的東西尋址后塞進寄存器。
到此,關于“Golang指針和接口如何使用”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。