《Go學習筆記 雨痕》方法

2022-09-15 05:18:12 字數 4630 閱讀 5505

方法 是與物件例項繫結的特殊函式。

方法 是物件導向程式設計的基本概念,用於維護和展示物件的自身狀態。物件是內斂的,每個例項都有各自不同的獨立特徵,以 屬性 和 方法 來暴露對外通訊介面。普通函式則專注於演算法流程,通過接收引數來完成特定邏輯運算,並返回最終結果。換句話說,方法是有關聯狀態的,而函式通常沒有。

方法 和 函式 定義語法區別的在於前者有前置例項接收引數(receiver),編譯器以此確定方法所屬型別。在某些語言裡,儘管沒有顯示定義,但會在呼叫時隱式傳遞 this 例項引數。

可以為 當前包,以及除 介面 和 指標 以外的任何型別定義方法。

type n int

func (n n) tostring() string

func main()

輸出:

0x19
方法同樣不支援過載(overload)。receiver 引數名沒有限制,按慣例會選用簡短有意義的名稱(不推薦使用 this、self)。如果 方法內部並不引用例項,可省略引數名,僅保留型別。

type n int

func (n) test()

方法 可看作特殊的函式,那麼 receiver 的型別自然可以是 基礎型別 或 指標型別。這會關係到呼叫時物件例項是否被複製。

type n int

func (n n) value()

func (n *n) pointer()

func main()

輸出:

v: 0xc42000a290, 26  // receiver 被複製

p: 0xc42000a268, 26

a: 0xc42000a268, 26

可使用 例項值 或 指標 呼叫方法,編譯器會根據方法 receiver 型別自動在 基礎型別 和 指標型別 間轉換。

func main()
輸出:

v: 0xc42000a290, 26

p: 0xc42000a268, 26

v: 0xc42000a2c0, 27

p: 0xc42000a268, 27

不能用多級指標呼叫方法。

func main()
指標型別的 receiver 必須是合法指標(包括 nil),或能獲取例項位址。

type x struct {}

func (x *x) test()

func main() .test() // 錯誤:cannot take the address of x literal

}

將方法看作普通函式,就很容易理解 receiver 的傳參方式。

如何選擇方法的 receiver 型別?

可以像訪問匿名字段成員那樣呼叫方法,由編譯器負責查詢。

type data struct 

func main()

d.lock() // 編譯器會處理為 sync.(*mutex).lock() 呼叫

defer d.unlock()

}

方法也會有同名遮蔽問題。但利用這種特性,可實現類似覆蓋(override)操作。

type user struct {}

type manager struct

func (user) tostring() string

func (m manager) tostring() string

func main()

輸出:

user; manager

user

儘管能直接訪問匿名欄位的 成員 和 方法,但它們依然不屬於繼承關係。

型別有乙個與之相關的方法集(method set),這決定了它是否實現某個介面。

可利用反射(reflect)測試這些規則。

type s struct {}

type t struct

func (s) sval() {}

func (*s) sptr() {}

func (t) tval() {}

func (*t) tptr() {}

// 顯示方法集裡所有方法名字

func methodset(a inte***ce{})

}func main()

sval func(main.t)

tval func(main.t)

----------

sptr func(*main.t)

sval func(*main.t)

tptr func(*main.t)

tval func(*main.t)

輸出結果符合預期,但我們也注意到某些方法的 receiver 型別發生了改變。真實情況是,這些都是由編譯器按方法集所需自動生成的額外包裝方法。

$ nm test | grep "main\."

...

方法集 僅影響 介面實現 和 方法表示式轉換,與通過 例項 或 例項指標 呼叫方法無關。例項並不使用方法集,而是直接呼叫(或通過隱式欄位名)。

很顯然,匿名字段就是為方法集準備的。否則,完全沒必要為少寫個欄位名而大費周章。

物件導向的三大特徵「封裝」、「繼承」和「多型」,go 僅實現了部分特徵,它更傾向於「組合優先於繼承」這種思想。將模組分解成相互獨立的更小單元,分別處理不同方面的需求,最後以匿名嵌入方式組合到一起,共同實現對外介面。而且其簡短一致的呼叫方式,更是隱藏了內部實現細節。

組合沒有父子依賴,不會破壞封裝。且整體和布局松耦合,可任意增加來實現擴充套件。各單元持有單一職責,互無關聯,實現和維護更加簡單。

儘管介面也是多型的一種實現形式,但我認為應該和基於繼承體系的多型分離開來。

方法 和 函式 一樣,除直接呼叫外,還可賦值給變數,或作為引數傳遞。依照具體引用方式的不同,可分為 expression

和 value

兩種狀態。

通過型別引用的 method expression 會被還原為 普通函式樣式,receiver 是第一引數,呼叫時須顯式傳參。至於型別,可以是 t 或 *t,只要目標方法存在於該型別方法集

中即可。

type n int

func (n n) test()

func main()

輸出:

main.n: 0xc42008c030, 25

test.n: 0xc42008c048, 25

test.n: 0xc42008c058, 25

儘管 *n 方法集包裝的 test() 方法 receiver 型別不同,但編譯器會保證按原定義型別拷貝傳值。

當然,也可直接以表示式方式呼叫。

基於 例項 或 指標引用 的 method value,引數簽名不會改變,依舊按正常方式呼叫。但當 method value 被賦值給變數或作為引數傳遞時,會立即計算並複製該方法執行所需的 receiver 物件,與其繫結,以便在稍後執行時,能隱式傳入 receiver 引數。

type n int

func (n n) test()

func main()

輸出:

main.n: 0xc42000a268, 103

test.n: 0xc42000a2a0, 101

test.n: 0xc42000a2b0, 102

編譯器會為 method value 生成乙個包裝函式,實現間接呼叫。至於 receiver 複製,和閉包的實現方法基本相同,打包成 funcval,經由 dx 暫存器傳遞。

當 method value 作為引數時,會複製含 receiver 在內的整個 method value。

type n int

func (n n) test()

func call(m func())

func main()

輸出:

main.n: 0xc420072188, 100

test.n: 0xc4200721c0, 101

test.n: 0xc4200721d0, 102

當然,如果目標方法的 receiver 是指標型別,那麼被複製的僅是指標(注:指標值,及指標指向的內容沒有變!)。

type n int

func (n *n) test()

func main()

輸出:

main.n: 0xc420072188, 103

test.n: 0xc420072188, 103

test.n: 0xc420072188, 103

只要 receiver 引數型別正確,使用 nil 同樣可以執行。

type n int

func (n) value() {}

func (*n) pointer() {}

func main()

go 方法 摘自go語言學習筆記

如何選擇方法的receiver型別 要修改例項狀態,用 t 無須修改狀態的小物件或固定值,建議用t 大物件建議用 t,以減少複製成本 引用型別 字串 函式等指標包裝物件,直接用t 若包含mutex等同步字段,用 t,避免因複製造成鎖操作無效 其他無法確定的情況,都用 t 方法集 型別t方法集包含所有...

Go學習筆記

使用關鍵字var定義變數,自動初始化為零值。如果提供初始化值,可省略變數型別。在函式內部,可用更簡略的 方式定義變數。空白符號 package main import fmt func test 2,0 函式內部 定義變數陣列data 0 data 1 data 2 並賦值0,1,2,且i 0 i,...

go學習筆記

那些打不倒你的,終將讓你變的更強 package main import fmt func main 輸出結果 num1的型別是 int,數值是 30 num2的型別是 int,數值是 40 name的型別是 string,數值是 zhangshang sum的型別是 int,數值是 30 1 2 ...