首頁 > 軟體

goalng 結構體 方法集 介面範例詳解

2022-09-16 22:03:01

一 前序

很多時候我們以為自己懂了,但內心深處卻偶有困惑,知識是嚴謹的,偶有困惑就是不懂,很幸運通過大量程式碼的磨練,終於看清困惑,並弄懂了。

本篇包括結構體,型別, 及 介面相關知識,希望對大家有所啟發。

二 事出有因

搞golang也有三四個年頭了,大小專案不少,各種golang書籍資料也閱無數,今天突然被一個報錯搞懵了。演示程式碼如下:

type MyErr struct{}
func(this MyErr)Error()string{
    return "myerr"
}
func main(){
    var Err *MyErr
    errors.As(MyErr{},Err) //這一句
}

errors.As是標準庫裡的判斷錯誤型別的一個簡單函數,按照如上寫法他執行報錯,報錯內容如下:

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

起初我沒有太關心報錯結果,我第一感覺是指標型別實現介面有問題,於是又改實現方法,又折騰變數,有時候ide提示方法未實現,有時候執行報錯,偶有成功,為啥成功我也不知道。

突然我發現我對介面一直都停留在會用的基礎上,所有結構體方法接受者都用指標,所有結構體範例都用指標,一方面保證介面方法都能實現,另一方面減少物件拷貝,減少記憶體用量。

於是帶著這個問題開始了刨根問題。在查閱資料中又發現了新的問題。

  • 指標方法集包括結構體所有方法,值方法集不包括指標方法集,為啥一個指標或者一個值範例可以呼叫所有方法。方法集的本質是啥?
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例子。
  • 嵌入型別的結構體,面對指標和值範例,方法集規律是啥?
  • 介面到底是啥?nil又是啥?
  • 結構體體結構到底是怎麼樣的?
  • 範例結構又如何?怎麼通過範例找到相應的方法?
  • 。。。

三 結構體與範例的資料結構

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}

這四種範例定義發生了什麼?資料結構如何?

範例資料結構主要包括三部分。

  • 頭部資訊,說明範例大小,範例是指標還是非指標等
  • 值,指標時候是指向範例的地址,非指標時候是具體的屬性值
  • 型別

範例a是一個空結構體範例,其特點是a雖然沒有顯示賦值,但是會預設建立一個a範例,其中的屬性都是"型別零值"。

範例b是一個指標型別,特點是沒有被初始化,指標未任何範例。

範例c是一個顯示賦值的範例,和a區別就是Num初始化值不再是"型別零值",而是1。

範例d就有點複雜了,他會有個範例及指標兩種資料,指標指向範例。範例初始化非"型別零值"。

關於圖中地址的說明,所有資料結構最終都是記憶體中的一段連續程式碼,都有開始地址,其他需要使用該資料的地方都是通過該地址找到這段記憶體資訊的。當然要說到程式碼,記憶體,虛擬地址,堆疊,程式執行,會有很多內容,這裡只要知道通過地址能找到該資料資訊即可。

注意,上圖也僅僅只是示意圖,幫助理解。其中型別指標實現並不是一個真的指標而是一個關於型別元資訊的偏移地址。

3 方法呼叫

結合上面的圖,說一下方法呼叫問題。為啥值方法和指標方法都可以呼叫所有方法,並且都能成功,並且修改都可以成功。

	a.Get()
	a.Set(2)
	// b.Get() 編譯器通過 執行不通過
	b.Set(3)
	c.Get()
	c.Set(4)
	d.Get()
	d.Set(5)

3.1 方法表示式

範例的方法呼叫的本質是函數,類似python,編譯器呼叫該函數時候預設的第一個引數是範例值或者範例指標。

T.Get(a)

(*T).Set(b,2)

通過型別直接呼叫型別中的函數,這就是方法表示式呼叫。真實的範例呼叫,也是通過找到型別並呼叫型別的方法。關於"方法表示式"這個詞出自《go語言核心程式設計》第三章,型別系統,有興趣的可以看看。

方法表示式有個特點,就是不會被自動轉換,通過方法表示式可以清楚知道值方法集或指標方法集是否有該方法。

在沒有說到介面之前,判斷一個方法是否屬於方法集用這個方法表示式是比較方便的。

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() 你會發現編譯器會報錯 T中沒有Set方法

但*T中有方法Set,這時候編譯器會生成一個*c,指標物件,在通過該物件呼叫Set方法。雖然通過指標物件呼叫Set但確實把c物件中的Num修改成功了,因為指標指向的正是c範例。如下圖:

這就是為啥實體方法集中沒有Set方法,也可以呼叫Set方法,編譯器進行了自動轉換,而這樣設計是合理的,通過Set操作,c範例中的Num確實變成4,符合預期。

3.3 指標範例呼叫所有方法

b和d都是指標範例,看看上圖關於b和d的資料結構示意圖,這兩個圖裡最大的區別就是有沒有匿名範例,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函數 所以只能通過轉化成範例來調動Get方法

這種自動轉化及操作的結果也是符合預期的,拿到了d指標指向的範例的資料。

3.4 空指標無法呼叫值方法

在回過頭看最初的err問題,原因就出在給了一個空指標,要通過一個空指標找到一個值方法,但是執行時候無法找到,所以panic了

四 介面

正常情況下,值範例還是指標範例都可以呼叫所有方法,並且修改都可以成功,那為什麼要區分值的方法集和指標的方法集,這就不得不提介面。

方法集是給介面準備。

方法集是"符合預期"的。

可以說因為介面的需要才會有方法集概念,只有介面中的方法與方法集中的方法相匹配時候,該方法集的範例才是該介面的實現範例。

可是問題又來了,明明一個範例物件不管是指標還是非指標範例都可以執行全部的方法,技術上完全可以實現,為什麼還要區分指標非指標方法?這是因為"不符合預期",為什麼,為什麼"不符合預期",看下邊解釋。

1 介面資料結構

要說明白介面和方法集的關係不是一件容易的事,先從介面結構說起。

介面型別跟struct型別不同,字面上看,介面只有方法頭,沒有屬性。

介面範例跟一般的struct範例也不一樣,它是一種動態的範例,只有介面範例被具體範例(值或指標)賦值的時候,介面範例才能確定。如下圖。

介面範例跟結構體範例類似,也包括兩部分,值和型別。

介面中的值是動態的,當被具體結構體範例賦值時候才能確定該值。該值就是結構體範例的值的拷貝,當範例是非指標時候會把資料都拷貝過來,當是範例是指標時候會把指標拷貝過來。golang中一切賦值都是拷貝,包括介面賦值,也是因為拷貝才會有很多"不符合預期的"結果。

介面中的型別包括動態型別和自身的介面型別,自身型別沒啥好說的,看上圖就明白了,主要是動態型別,這個是儲存了當前賦值的結構體範例的型別。

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,其實現了值Get方法,指標Set方法。

上邊程式碼中a,b,c,d已經在上部分進行過講解了。

ia,ib,ic,id賦值過程如下圖:

值方法集

ia,ic介面物件其實在編輯階段IDE就會給出報錯提示,範例和介面不匹配,因為a和c實體方法集中只有一個Get函數,可以通過前邊提到的"表示式方法"進行驗證,這裡通過IDE提示也知道缺少Set函數。

那麼問題來了,在第一部分單獨a,c物件是可以呼叫所有方法,這裡介面實現為啥要弄出個方法集進行限制?因為"拷貝"和"不符合預期"。

假設a,c可以成功賦值給介面ia,ic,賦值後a,c中的資料會拷貝到介面的動態值區域,要是成功執行了Set函數,將介面動態值區域的資料進行了修改,那原來的a,c中的資料並未改變,這個是"不符合預期的"。所以乾脆就不允許這麼操作。

更常用的"不符合預期"解釋程式碼是當介面是引數值時候。如下程式碼。

func DoT(a I) {
	a.Set(11)
}
func main(){
    ...
    DoT(ic)
    fmt.Println(ic.Get())
}

DoT函數用I做引數,內部對I進行了操作,用ic或者ia做引數,如果可以成功,最後列印ic或者ia中的值,並未改變,這不符合預期,很令人困惑。這段原理可參考<<go核心程式設計>>第三章型別系統相關描述。

指標方法集

ib和id都是指標型別,其方法集包括所有方法,即Get和Set,其中Get是通過編譯器自動轉化進行間接呼叫,值範例不允許呼叫指標範例的方法集是因為"不符合預期",那指標範例就允許呼叫值範例的方法了?是的,允許,因為"符合預期"。

還用下面的程式碼做解釋。

func DoT(a I) {
    a.Set(a.Get()++)
}
func main(){
    ...
    DoT(id)
    fmt.Println(id.Get())
}

這裡用id做引數,最終執行完,結果id確實增加了1,符合預期。

結合前邊介面賦值的圖進行分析,介面動態值區域拷貝了一份id的指標值,這個指標指向一個具體的範例。如下圖。

從這裡可以看出對id的任何操作其實都是對具體的範例進行的操作,所以無論讀寫都是符合預期的,所以當使用指標呼叫Get方法時候就會進行自動轉化呼叫值的Get方法。

至於ib為啥編譯通過,執行時候就報錯,也是因為指標是個nil值,無法自動轉化找到Get方法。

總結

翻了好幾天資料,本來想把嵌入型別和反射都寫進來,但是時間有點倉促,大家可以結合上邊的講解,自行對嵌入型別和反射進行研究,基礎原理都一樣。

這裡總結一下:

範例都包括兩部分,值和型別,編譯器正是通過範例型別所以才知道了其方法集。

單獨範例使用時候,是允許呼叫所有方法的,呼叫非自身方法集時候編譯器會自動進行轉換,並且都會呼叫成功,符合預期。

範例賦值給介面時候,是把範例資訊拷貝到介面中的,其資料結構和原來範例完全不一樣了,同時介面會嚴格檢查方法集,以防止不符合預期行為發生。

範例是指標時候,並且為空的時候,並且包含非指標方法時候,無論是該範例的介面還是該範例,都不能進行任何方法呼叫,否則會有執行時panic發生。未指向任何具體資料變數,無論讀寫肯定報錯。

介面斷言知道為啥一定要是介面才能進行斷言吧,因為介面的動態值和動態型別要進行動態填充,介面斷言也可以判斷一個範例的方法集,而且是安全的判斷

_,ok:=interface{}(a).(I)

判斷一個範例是否有哪個方法,方法集中的方法有哪些,目前看可以通過三種方法"方法表示式"","介面賦值","介面斷言"。

其實還有好多知識點比如nil型別,空介面,空指標,相互比較時候真假結果,嵌入結構體方法集,反射操作,等等,只要把原理搞清了都很容易理解的。

以上就是goalng 結構體 方法集 介面範例詳解的詳細內容,更多關於goalng 結構體 方法集 介面的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com