goalng?結(jié)構(gòu)體?方法集?接口實(shí)例詳解
一 前序
很多時(shí)候我們以為自己懂了,但內(nèi)心深處卻偶有困惑,知識(shí)是嚴(yán)謹(jǐn)?shù)模加欣Щ缶褪遣欢?,很幸運(yùn)通過(guò)大量代碼的磨練,終于看清困惑,并弄懂了。
本篇包括結(jié)構(gòu)體,類型, 及 接口相關(guān)知識(shí),希望對(duì)大家有所啟發(fā)。
二 事出有因
搞golang也有三四個(gè)年頭了,大小項(xiàng)目不少,各種golang書籍資料也閱無(wú)數(shù),今天突然被一個(gè)報(bào)錯(cuò)搞懵了。演示代碼如下:
type MyErr struct{} func(this MyErr)Error()string{ return "myerr" } func main(){ var Err *MyErr errors.As(MyErr{},Err) //這一句 }
errors.As是標(biāo)準(zhǔn)庫(kù)里的判斷錯(cuò)誤類型的一個(gè)簡(jiǎn)單函數(shù),按照如上寫法他運(yùn)行報(bào)錯(cuò),報(bào)錯(cuò)內(nèi)容如下:
panic: errors: target must be a non-nil pointer
goroutine 1 [running]:
errors.As({0x107e280, 0x11523f8}, {0x104f3e0, 0x11523f8})
D:/GO/src/errors/wrap.go:84 +0x3e5
github.com/pkg/errors.As(...)
D:/GO/gopath/pkg/mod/github.com/pkg/errors@v0.9.1/go113.go:31
main.main()
H:/information/demo1/main.go:19 +0x31
exit status 2
errors.As 方法簽名
func As(err error, target interface{}) bool
起初我沒有太關(guān)心報(bào)錯(cuò)結(jié)果,我第一感覺是指針類型實(shí)現(xiàn)接口有問(wèn)題,于是又改實(shí)現(xiàn)方法,又折騰變量,有時(shí)候ide提示方法未實(shí)現(xiàn),有時(shí)候運(yùn)行報(bào)錯(cuò),偶有成功,為啥成功我也不知道。
突然我發(fā)現(xiàn)我對(duì)接口一直都停留在會(huì)用的基礎(chǔ)上,所有結(jié)構(gòu)體方法接受者都用指針,所有結(jié)構(gòu)體實(shí)例都用指針,一方面保證接口方法都能實(shí)現(xiàn),另一方面減少對(duì)象拷貝,減少內(nèi)存用量。
于是帶著這個(gè)問(wèn)題開始了刨根問(wèn)題。在查閱資料中又發(fā)現(xiàn)了新的問(wèn)題。
- 指針方法集包括結(jié)構(gòu)體所有方法,值方法集不包括指針方法集,為啥一個(gè)指針或者一個(gè)值實(shí)例可以調(diào)用所有方法。方法集的本質(zhì)是啥?
type T struct{} func (t T) Get() { fmt.Println("this is Get") } func (t *T) Set() { fmt.Println("this is set") } func main() { var a T a.Set() a.Get() (&a).Get() (&a).Set() }
- 為啥有時(shí)候指針對(duì)象無(wú)法調(diào)用非指針方法?如開始的err例子。
- 嵌入類型的結(jié)構(gòu)體,面對(duì)指針和值實(shí)例,方法集規(guī)律是啥?
- 接口到底是啥?nil又是啥?
- 結(jié)構(gòu)體體結(jié)構(gòu)到底是怎么樣的?
- 實(shí)例結(jié)構(gòu)又如何?怎么通過(guò)實(shí)例找到相應(yīng)的方法?
- 。。。
三 結(jié)構(gòu)體與實(shí)例的數(shù)據(jù)結(jié)構(gòu)
1. 結(jié)構(gòu)體類型
結(jié)構(gòu)體就是一個(gè)模板,用于生成實(shí)例用的,包括最基本的屬性集,值的方法集,指針方法集。
type T struct{ Num int } func (t T) Get() int{ fmt.Println("this is Get") return t.Num } func (t *T) Set(i int) { fmt.Println("this is set") t.Num = i }
這就是一個(gè)定義的結(jié)構(gòu)體。
func (t T) Get() 該方法的接受者 t是一個(gè)實(shí)例值,所以該方法稱為值方法。
func (t *T) Set() 該方法的接受者 t 是一個(gè)指針,所以該方法成為指針方法。
2. 實(shí)例
實(shí)例就是結(jié)構(gòu)體實(shí)例化后的變量,用T類型說(shuō)明。
var a T var b *T var c = T{1} var d = &T{1}
這四種實(shí)例定義發(fā)生了什么?數(shù)據(jù)結(jié)構(gòu)如何?
實(shí)例數(shù)據(jù)結(jié)構(gòu)主要包括三部分。
- 頭部信息,說(shuō)明實(shí)例大小,實(shí)例是指針還是非指針等
- 值,指針時(shí)候是指向?qū)嵗牡刂?,非指針時(shí)候是具體的屬性值
- 類型
實(shí)例a是一個(gè)空結(jié)構(gòu)體實(shí)例,其特點(diǎn)是a雖然沒有顯示賦值,但是會(huì)默認(rèn)創(chuàng)建一個(gè)a實(shí)例,其中的屬性都是"類型零值"。
實(shí)例b是一個(gè)指針類型,特點(diǎn)是沒有被初始化,指針未任何實(shí)例。
實(shí)例c是一個(gè)顯示賦值的實(shí)例,和a區(qū)別就是Num初始化值不再是"類型零值",而是1。
實(shí)例d就有點(diǎn)復(fù)雜了,他會(huì)有個(gè)實(shí)例及指針兩種數(shù)據(jù),指針指向?qū)嵗?。?shí)例初始化非"類型零值"。
關(guān)于圖中地址的說(shuō)明,所有數(shù)據(jù)結(jié)構(gòu)最終都是內(nèi)存中的一段連續(xù)代碼,都有開始地址,其他需要使用該數(shù)據(jù)的地方都是通過(guò)該地址找到這段內(nèi)存信息的。當(dāng)然要說(shuō)到代碼,內(nèi)存,虛擬地址,堆棧,程序運(yùn)行,會(huì)有很多內(nèi)容,這里只要知道通過(guò)地址能找到該數(shù)據(jù)信息即可。
注意,上圖也僅僅只是示意圖,幫助理解。其中類型指針實(shí)現(xiàn)并不是一個(gè)真的指針而是一個(gè)關(guān)于類型元信息的偏移地址。
3 方法調(diào)用
結(jié)合上面的圖,說(shuō)一下方法調(diào)用問(wèn)題。為啥值方法和指針方法都可以調(diào)用所有方法,并且都能成功,并且修改都可以成功。
a.Get() a.Set(2) // b.Get() 編譯器通過(guò) 運(yùn)行不通過(guò) b.Set(3) c.Get() c.Set(4) d.Get() d.Set(5)
3.1 方法表達(dá)式
實(shí)例的方法調(diào)用的本質(zhì)是函數(shù),類似python,編譯器調(diào)用該函數(shù)時(shí)候默認(rèn)的第一個(gè)參數(shù)是實(shí)例值或者實(shí)例指針。
T.Get(a)
(*T).Set(b,2)
通過(guò)類型直接調(diào)用類型中的函數(shù),這就是方法表達(dá)式調(diào)用。真實(shí)的實(shí)例調(diào)用,也是通過(guò)找到類型并調(diào)用類型的方法。關(guān)于"方法表達(dá)式"這個(gè)詞出自《go語(yǔ)言核心編程》第三章,類型系統(tǒng),有興趣的可以看看。
方法表達(dá)式有個(gè)特點(diǎn),就是不會(huì)被自動(dòng)轉(zhuǎn)換,通過(guò)方法表達(dá)式可以清楚知道值方法集或指針方法集是否有該方法。
在沒有說(shuō)到接口之前,判斷一個(gè)方法是否屬于方法集用這個(gè)方法表達(dá)式是比較方便的。
3.2 值實(shí)例調(diào)用所有方法
a和c本質(zhì)是一樣的,只是初始值不一樣。拿c做例子進(jìn)行講解。
c.Get() == T.Get(a)
上邊代碼這個(gè)不用解釋太多,就是c實(shí)例通過(guò)類型信息找到相關(guān)的值方法進(jìn)行調(diào)用。
c.Set() == (&c).Set(4) == (*T).Set(c,4)
上邊代碼 c中對(duì)應(yīng)的T中方法并不包含Set方法。
T.Set() 你會(huì)發(fā)現(xiàn)編譯器會(huì)報(bào)錯(cuò) T中沒有Set方法
但*T中有方法Set,這時(shí)候編譯器會(huì)生成一個(gè)*c,指針對(duì)象,在通過(guò)該對(duì)象調(diào)用Set方法。雖然通過(guò)指針對(duì)象調(diào)用Set但確實(shí)把c對(duì)象中的Num修改成功了,因?yàn)橹羔樦赶虻恼莄實(shí)例。如下圖:
這就是為啥實(shí)例方法集中沒有Set方法,也可以調(diào)用Set方法,編譯器進(jìn)行了自動(dòng)轉(zhuǎn)換,而這樣設(shè)計(jì)是合理的,通過(guò)Set操作,c實(shí)例中的Num確實(shí)變成4,符合預(yù)期。
3.3 指針實(shí)例調(diào)用所有方法
b和d都是指針實(shí)例,看看上圖關(guān)于b和d的數(shù)據(jù)結(jié)構(gòu)示意圖,這兩個(gè)圖里最大的區(qū)別就是有沒有匿名實(shí)例,b因?yàn)槭强罩羔槢]有指向任何實(shí)例,所以只有類型信息。
編譯器知道你是個(gè)指針,查看類型中的所有方法,包括值方法和指針方法,有Set和Get所以編譯通過(guò),但是在運(yùn)行的時(shí)候,因?yàn)槭强罩羔?,無(wú)法找到值的方法Get,所以運(yùn)行時(shí)候報(bào)錯(cuò) panic: errors: target must be a non-nil pointer
d因?yàn)橹赶蛞粋€(gè)實(shí)例,所以順著這個(gè)實(shí)例找到Get方法進(jìn)行調(diào)用,這都是編譯器自動(dòng)進(jìn)行的。
d.Get() == (&d).Get() == (T).Get(*d)
通用使用方法表達(dá)式,也可以知道指針方法集中是沒有Get方法的。
(*T).Get() 編譯器不會(huì)通過(guò) 說(shuō)明指針方法集中確實(shí)沒有Get函數(shù) 所以只能通過(guò)轉(zhuǎn)化成實(shí)例來(lái)調(diào)動(dòng)Get方法
這種自動(dòng)轉(zhuǎn)化及操作的結(jié)果也是符合預(yù)期的,拿到了d指針指向的實(shí)例的數(shù)據(jù)。
3.4 空指針無(wú)法調(diào)用值方法
在回過(guò)頭看最初的err問(wèn)題,原因就出在給了一個(gè)空指針,要通過(guò)一個(gè)空指針找到一個(gè)值方法,但是運(yùn)行時(shí)候無(wú)法找到,所以panic了
四 接口
正常情況下,值實(shí)例還是指針實(shí)例都可以調(diào)用所有方法,并且修改都可以成功,那為什么要區(qū)分值的方法集和指針的方法集,這就不得不提接口。
方法集是給接口準(zhǔn)備。
方法集是"符合預(yù)期"的。
可以說(shuō)因?yàn)榻涌诘男枰艜?huì)有方法集概念,只有接口中的方法與方法集中的方法相匹配時(shí)候,該方法集的實(shí)例才是該接口的實(shí)現(xiàn)實(shí)例。
可是問(wèn)題又來(lái)了,明明一個(gè)實(shí)例對(duì)象不管是指針還是非指針實(shí)例都可以執(zhí)行全部的方法,技術(shù)上完全可以實(shí)現(xiàn),為什么還要區(qū)分指針非指針方法?這是因?yàn)?quot;不符合預(yù)期",為什么,為什么"不符合預(yù)期",看下邊解釋。
1 接口數(shù)據(jù)結(jié)構(gòu)
要說(shuō)明白接口和方法集的關(guān)系不是一件容易的事,先從接口結(jié)構(gòu)說(shuō)起。
接口類型跟struct類型不同,字面上看,接口只有方法頭,沒有屬性。
接口實(shí)例跟一般的struct實(shí)例也不一樣,它是一種動(dòng)態(tài)的實(shí)例,只有接口實(shí)例被具體實(shí)例(值或指針)賦值的時(shí)候,接口實(shí)例才能確定。如下圖。
接口實(shí)例跟結(jié)構(gòu)體實(shí)例類似,也包括兩部分,值和類型。
接口中的值是動(dòng)態(tài)的,當(dāng)被具體結(jié)構(gòu)體實(shí)例賦值時(shí)候才能確定該值。該值就是結(jié)構(gòu)體實(shí)例的值的拷貝,當(dāng)實(shí)例是非指針時(shí)候會(huì)把數(shù)據(jù)都拷貝過(guò)來(lái),當(dāng)是實(shí)例是指針時(shí)候會(huì)把指針拷貝過(guò)來(lái)。golang中一切賦值都是拷貝,包括接口賦值,也是因?yàn)榭截惒艜?huì)有很多"不符合預(yù)期的"結(jié)果。
接口中的類型包括動(dòng)態(tài)類型和自身的接口類型,自身類型沒啥好說(shuō)的,看上圖就明白了,主要是動(dòng)態(tài)類型,這個(gè)是存儲(chǔ)了當(dāng)前賦值的結(jié)構(gòu)體實(shí)例的類型。
2 接口賦值
以下面的接口賦值代碼進(jìn)行說(shuō)明解釋。
package main type I interface { Get() int Set(i int) } type T struct { Num int } func (t T) Get() int { return t.Num } func (t *T) Set(num int) { t.Num = num } func main() { var a T var b *T var c = T{} var d = &T{} var ia I = a //編譯不通過(guò) 方法集不匹配 var ib I = b //編譯通過(guò) 運(yùn)行會(huì)報(bào)錯(cuò) panic: runtime error: invalid memory address or nil pointer dereference var ic I = c //編譯不通過(guò) 方法集不匹配 var id I = d }
例子代碼很簡(jiǎn)單,就是一個(gè)接口類型I,一個(gè)struct類型T,其實(shí)現(xiàn)了值Get方法,指針Set方法。
上邊代碼中a,b,c,d已經(jīng)在上部分進(jìn)行過(guò)講解了。
ia,ib,ic,id賦值過(guò)程如下圖:
值方法集
ia,ic接口對(duì)象其實(shí)在編輯階段IDE就會(huì)給出報(bào)錯(cuò)提示,實(shí)例和接口不匹配,因?yàn)閍和c實(shí)例方法集中只有一個(gè)Get函數(shù),可以通過(guò)前邊提到的"表達(dá)式方法"進(jìn)行驗(yàn)證,這里通過(guò)IDE提示也知道缺少Set函數(shù)。
那么問(wèn)題來(lái)了,在第一部分單獨(dú)a,c對(duì)象是可以調(diào)用所有方法,這里接口實(shí)現(xiàn)為啥要弄出個(gè)方法集進(jìn)行限制?因?yàn)?quot;拷貝"和"不符合預(yù)期"。
假設(shè)a,c可以成功賦值給接口ia,ic,賦值后a,c中的數(shù)據(jù)會(huì)拷貝到接口的動(dòng)態(tài)值區(qū)域,要是成功執(zhí)行了Set函數(shù),將接口動(dòng)態(tài)值區(qū)域的數(shù)據(jù)進(jìn)行了修改,那原來(lái)的a,c中的數(shù)據(jù)并未改變,這個(gè)是"不符合預(yù)期的"。所以干脆就不允許這么操作。
更常用的"不符合預(yù)期"解釋代碼是當(dāng)接口是參數(shù)值時(shí)候。如下代碼。
func DoT(a I) { a.Set(11) } func main(){ ... DoT(ic) fmt.Println(ic.Get()) }
DoT函數(shù)用I做參數(shù),內(nèi)部對(duì)I進(jìn)行了操作,用ic或者ia做參數(shù),如果可以成功,最后打印ic或者ia中的值,并未改變,這不符合預(yù)期,很令人困惑。這段原理可參考<<go核心編程>>第三章類型系統(tǒng)相關(guān)描述。
指針方法集
ib和id都是指針類型,其方法集包括所有方法,即Get和Set,其中Get是通過(guò)編譯器自動(dòng)轉(zhuǎn)化進(jìn)行間接調(diào)用,值實(shí)例不允許調(diào)用指針實(shí)例的方法集是因?yàn)?quot;不符合預(yù)期",那指針實(shí)例就允許調(diào)用值實(shí)例的方法了?是的,允許,因?yàn)?quot;符合預(yù)期"。
還用下面的代碼做解釋。
func DoT(a I) { a.Set(a.Get()++) } func main(){ ... DoT(id) fmt.Println(id.Get()) }
這里用id做參數(shù),最終執(zhí)行完,結(jié)果id確實(shí)增加了1,符合預(yù)期。
結(jié)合前邊接口賦值的圖進(jìn)行分析,接口動(dòng)態(tài)值區(qū)域拷貝了一份id的指針值,這個(gè)指針指向一個(gè)具體的實(shí)例。如下圖。
從這里可以看出對(duì)id的任何操作其實(shí)都是對(duì)具體的實(shí)例進(jìn)行的操作,所以無(wú)論讀寫都是符合預(yù)期的,所以當(dāng)使用指針調(diào)用Get方法時(shí)候就會(huì)進(jìn)行自動(dòng)轉(zhuǎn)化調(diào)用值的Get方法。
至于ib為啥編譯通過(guò),運(yùn)行時(shí)候就報(bào)錯(cuò),也是因?yàn)橹羔樖莻€(gè)nil值,無(wú)法自動(dòng)轉(zhuǎn)化找到Get方法。
總結(jié)
翻了好幾天資料,本來(lái)想把嵌入類型和反射都寫進(jìn)來(lái),但是時(shí)間有點(diǎn)倉(cāng)促,大家可以結(jié)合上邊的講解,自行對(duì)嵌入類型和反射進(jìn)行研究,基礎(chǔ)原理都一樣。
這里總結(jié)一下:
實(shí)例都包括兩部分,值和類型,編譯器正是通過(guò)實(shí)例類型所以才知道了其方法集。
單獨(dú)實(shí)例使用時(shí)候,是允許調(diào)用所有方法的,調(diào)用非自身方法集時(shí)候編譯器會(huì)自動(dòng)進(jìn)行轉(zhuǎn)換,并且都會(huì)調(diào)用成功,符合預(yù)期。
實(shí)例賦值給接口時(shí)候,是把實(shí)例信息拷貝到接口中的,其數(shù)據(jù)結(jié)構(gòu)和原來(lái)實(shí)例完全不一樣了,同時(shí)接口會(huì)嚴(yán)格檢查方法集,以防止不符合預(yù)期行為發(fā)生。
實(shí)例是指針時(shí)候,并且為空的時(shí)候,并且包含非指針方法時(shí)候,無(wú)論是該實(shí)例的接口還是該實(shí)例,都不能進(jìn)行任何方法調(diào)用,否則會(huì)有運(yùn)行時(shí)panic發(fā)生。未指向任何具體數(shù)據(jù)變量,無(wú)論讀寫肯定報(bào)錯(cuò)。
接口斷言知道為啥一定要是接口才能進(jìn)行斷言吧,因?yàn)榻涌诘膭?dòng)態(tài)值和動(dòng)態(tài)類型要進(jìn)行動(dòng)態(tài)填充,接口斷言也可以判斷一個(gè)實(shí)例的方法集,而且是安全的判斷
_,ok:=interface{}(a).(I)
判斷一個(gè)實(shí)例是否有哪個(gè)方法,方法集中的方法有哪些,目前看可以通過(guò)三種方法"方法表達(dá)式"","接口賦值","接口斷言"。
其實(shí)還有好多知識(shí)點(diǎn)比如nil類型,空接口,空指針,相互比較時(shí)候真假結(jié)果,嵌入結(jié)構(gòu)體方法集,反射操作,等等,只要把原理搞清了都很容易理解的。
以上就是goalng 結(jié)構(gòu)體 方法集 接口實(shí)例詳解的詳細(xì)內(nèi)容,更多關(guān)于goalng 結(jié)構(gòu)體 方法集 接口的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
深入探究Golang中flag標(biāo)準(zhǔn)庫(kù)的使用
在本文中,我們將深入探討 flag 標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn)原理和使用技巧,以幫助讀者更好地理解和掌握該庫(kù)的使用方法,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2023-04-04Go語(yǔ)言開發(fā)框架反射機(jī)制及常見函數(shù)示例詳解
這篇文章主要為大家介紹了Go語(yǔ)言開發(fā)框架反射機(jī)制及常見函數(shù)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09golang post請(qǐng)求常用的幾種方式小結(jié)
這篇文章主要介紹了golang post請(qǐng)求常用的幾種方式小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04使用Golang的singleflight防止緩存擊穿的方法
這篇文章主要介紹了使用Golang的singleflight防止緩存擊穿的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04golang執(zhí)行命令獲取執(zhí)行結(jié)果狀態(tài)(推薦)
這篇文章主要介紹了golang執(zhí)行命令獲取執(zhí)行結(jié)果狀態(tài)的相關(guān)知識(shí),非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2019-11-11解決Golang并發(fā)工具Singleflight的問(wèn)題
前段時(shí)間在一個(gè)項(xiàng)目里使用到了分布式鎖進(jìn)行共享資源的訪問(wèn)限制,后來(lái)了解到Golang里還能夠使用singleflight對(duì)共享資源的訪問(wèn)做限制,于是利用空余時(shí)間了解,將知識(shí)沉淀下來(lái),并做分享2022-05-05