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