主頁 > 後端開發 > golang拾遺:指標和介面

golang拾遺:指標和介面

2020-10-11 04:14:40 後端開發

這是本系列的第一篇文章,golang拾遺主要是用來記錄一些遺忘了的、平時從沒注意過的golang相關知識,想做本系列的貧訓其實是因為疫情閑著在家無聊,網上沖浪的時候發現了zhuihu上的go語言愛好者周刊和Go 101,讀之如醍醐灌頂,受益匪淺,于是本系列的文章就誕生了,拾遺主要是收集和golang相關的瑣碎知識,當然也會對周刊和101的內容做一些補充說明,好了,題外話就此打住,下面該進入今天的正題了,

指標和介面

golang的型別系統其實很有意思,有意思的地方就在于型別系統表面上看起來眾生平等,然而實際上卻要分成普通型別(types)和介面(interfaces)來看待,普通型別也包含了所謂的參考型別,例如slicemap,雖然他們和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的參考型別,mm2明顯是兩個不同的型別,不過實際上在底層他們是一樣的,不信我們用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參考的資料的型別,優化自然也無從談起了,

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

golang的指標

指標也是一個極有探討價值的話題,特別是指標在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就是先檢查指標隨后解參考了,同時要注意,這里的方法呼叫是已經在編譯期確定了的,

指向interface的指標

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

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的方法去處理它而不是直接把記憶體里的東西尋址后塞進暫存器,

總結

其實也沒什么好總結的,只有兩點需要記住,一是interface是有自己對應的物體資料結構的,二是盡量不要用指標去指向interface,因為golang對指標自動解參考的處理會帶來陷阱,

如果你對interface的實作很感興趣的話,這里有個reflect+暴力窮舉實作的乞丐版,

理解了乞丐版的基礎上如果有興趣還可以看看真正的golang實作,資料的層次結構上更細化,而且有使用指標和記憶體偏移等的聰明辦法,不說是否會有識訓,起碼研究起來不會無聊:P,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/167266.html

標籤:Go

上一篇:2020-10-10:OOM都有哪些,說出幾種?

下一篇:大量類加載器創建導致詭異FullGC

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more