淺析Golang中類型嵌入的簡介與使用
一、獨立的自定義類型
什么是獨立的自定義類型呢?就是這個類型的所有方法都是自己顯式實現(xiàn)的。
我們舉個例子,自定義類型 T
有兩個方法 M1
和 M2
,如果 T
是一個獨立的自定義類型,那我們在聲明類型 T
的 Go 包源碼文件中一定可以找到其所有方法的實現(xiàn)代碼,比如:
func (T) M1() {...} func (T) M2() {...}
難道還有某種自定義類型的方法不是自己顯式實現(xiàn)的嗎?當(dāng)涉及到 Go 語言中的自定義類型時,有一種方法可以不需要顯式地實現(xiàn)方法,即:讓某個自定義類型“繼承”其他類型的方法實現(xiàn)。
二、繼承
Go 語言從設(shè)計伊始,就決定不支持經(jīng)典面向?qū)ο蟮木幊谭妒脚c語法元素,所以我們這里只是借用了“繼承”這個詞匯而已,說是“繼承”,實則依舊是一種組合的思想。
這種“繼承”是通過 Go 語言的類型嵌入(Type Embedding)來實現(xiàn)的。
三、類型嵌入
3.1 什么是類型嵌入
類型嵌入指的就是在一個類型的定義中嵌入了其他類型。Go 語言支持兩種類型嵌入,分別是接口類型的類型嵌入和結(jié)構(gòu)體類型的類型嵌入。
四、接口類型的類型嵌入
4.1 接口類型的類型嵌入介紹
接口類型的類型嵌入是指在一個接口類型的定義中嵌入其他接口類型,從而使接口類型包含了嵌入接口中定義的方法。這允許一個接口類型繼承另一個接口類型的方法集,以擴展其功能。
總結(jié)接口類型的類型嵌入的關(guān)鍵點:
- 嵌入接口類型:接口類型可以嵌入其他接口類型,將其方法集合并到當(dāng)前接口中。
- 繼承方法集:通過嵌入,接口類型可以繼承嵌入接口中的方法,使得當(dāng)前接口也具有這些方法。
- 實現(xiàn)多態(tài):通過接口類型的類型嵌入,可以實現(xiàn)多態(tài),使不同類型的對象可以被統(tǒng)一地處理,提高代碼的靈活性。
這種機制使得Go語言的接口更加靈活和可擴展,允許將不同的接口組合在一起,以創(chuàng)建更復(fù)雜的接口,從而促進了代碼的重用和可維護性。
4.2 一個小案例
接著,我們用一個案例,直觀地了解一下什么是接口類型的類型嵌入。我們知道,接口類型聲明了由一個方法集合代表的接口,比如下面接口類型 E
:
type E interface { M1() M2() }
這個接口類型 E
的方法集合,包含兩個方法,分別是 M1
和 M2
,它們組成了 E
這個接口類型所代表的接口。如果某個類型實現(xiàn)了方法 M1
和 M2
,我們就說這個類型實現(xiàn)了 E
所代表的接口。
此時,我們再定義另外一個接口類型 I
,它的方法集合中包含了三個方法 M1
、M2
和 M3
,如下面代碼:
type I interface { M1() M2() M3() }
我們看到接口類型 I
方法集合中的 M1
和 M2
,與接口類型 E
的方法集合中的方法完全相同。在這種情況下,我們可以用接口類型 E
替代上面接口類型 I
定義中 M1
和 M2
,如下面代碼:
type I interface { E M3() }
像這種在一個接口類型(I
)定義中,嵌入另外一個接口類型(E
)的方式,就是我們說的接口類型的類型嵌入。
而且,這個帶有類型嵌入的接口類型 I
的定義與上面那個包含 M1
、M2
和 M3
的接口類型 I
的定義,是等價的。因此,我們可以得到一個結(jié)論,這種接口類型嵌入的語義就是新接口類型(如接口類型 I
)將嵌入的接口類型(如接口類型 E
)的方法集合,并入到自己的方法集合中。
其實,使用類型嵌入方式定義接口類型也是 Go 組合設(shè)計哲學(xué)的一種體現(xiàn)。
按 Go 語言慣例,Go 中的接口類型中只包含少量方法,并且常常只是一個方法。通過在接口類型中嵌入其他接口類型可以實現(xiàn)接口的組合,這也是 Go 語言中基于已有接口類型構(gòu)建新接口類型的慣用法。
按 Go 語言慣例,Go 中的接口類型中只包含少量方法,并且常常只是一個方法。通過在接口類型中嵌入其他接口類型可以實現(xiàn)接口的組合,這也是 Go 語言中基于已有接口類型構(gòu)建新接口類型的慣用法。
我們在 Go 標準庫中可以看到很多這種組合方式的應(yīng)用,最常見的莫過于 io
包中一系列接口的定義了。比如,io
包的 ReadWriter
、ReadWriteCloser
等接口類型就是通過嵌入 Reader
、Writer
或 Closer
三個基本的接口類型組合而成的。下面是僅包含單一方法的 io
包 Reader
、Writer
和 Closer
的定義:
// $GOROOT/src/io/io.go type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } type Closer interface { Close() error }
下面的 io
包的 ReadWriter
、ReadWriteCloser
等接口類型,通過嵌入上面基本接口類型組合而形成:
type ReadWriter interface { Reader Writer } type ReadCloser interface { Reader Closer } type WriteCloser interface { Writer Closer } type ReadWriteCloser interface { Reader Writer Closer }
不過,這種通過嵌入其他接口類型來創(chuàng)建新接口類型的方式,在 Go 1.14 版本之前是有約束的:如果新接口類型嵌入了多個接口類型,這些嵌入的接口類型的方法集合不能有交集,同時嵌入的接口類型的方法集合中的方法名字,也不能與新接口中的其他方法同名。比如我們用 Go 1.12.7 版本運行下面例子,Go 編譯器就會報錯:
type Interface1 interface { M1() } type Interface2 interface { M1() M2() } type Interface3 interface { Interface1 Interface2 // Error: duplicate method M1 } type Interface4 interface { Interface2 M2() // Error: duplicate method M2 } func main() { }
我們具體看一下例子中的兩個編譯報錯:第一個是因為 Interface3
中嵌入的兩個接口類型 Interface1
和 Interface2
的方法集合有交集,交集是方法 M1
;第二個報錯是因為 Interface4
類型中的方法 M2
與嵌入的接口類型 Interface2
的方法 M2
重名。
但自 Go 1.14 版本開始,Go 語言去除了這些約束,我們使用 Go 最新版本運行上面這個示例就不會得到編譯錯誤了。
接口類型的類型嵌入比較簡單,我們只要把握好它的語義,也就是“方法集合并入”就可以了。
五、結(jié)構(gòu)體類型的類型嵌入
5.1 結(jié)構(gòu)體類型的類型嵌入介紹
結(jié)構(gòu)體類型的類型嵌入是一種特殊的結(jié)構(gòu)體定義方式,其中結(jié)構(gòu)體的字段名可以直接使用類型名、類型的指針類型名或接口類型名,代表字段的名字和類型。以下是結(jié)構(gòu)體類型的類型嵌入的關(guān)鍵點:
- 字段名和類型合二為一:在結(jié)構(gòu)體類型的類型嵌入中,字段名和類型名合并成一個標識符,既代表了字段的名字又代表了字段的類型。這使得字段名與類型名保持一致,簡化了結(jié)構(gòu)體定義。
- 嵌入字段:這種方式被稱為嵌入字段(Embedded Field),其中嵌入字段的類型可以是自定義類型、結(jié)構(gòu)體類型的指針類型,或接口類型。
- 訪問嵌入字段:可以通過結(jié)構(gòu)體變量來訪問嵌入字段的字段和方法,無需使用字段名,因為字段名已經(jīng)隱含在類型中。
- 字段名與類型名一致:嵌入字段的字段名與類型名一致,這種一致性使得代碼更加清晰和直觀。
- 類型組合:通過嵌入字段,可以將不同類型的功能組合在一個結(jié)構(gòu)體中,形成更復(fù)雜的數(shù)據(jù)結(jié)構(gòu),提高代碼的可維護性和擴展性。
5.2 小案例
通常,結(jié)構(gòu)體都是類似下面這樣的:
type S struct { A int b string c T p *P _ [10]int8 F func() }
結(jié)構(gòu)體類型 S
中的每個字段(field)都有唯一的名字與對應(yīng)的類型,即便是使用空標識符占位的字段,它的類型也是明確的,但這還不是 Go 結(jié)構(gòu)體類型的“完全體”。Go 結(jié)構(gòu)體類型定義還有另外一種形式,那就是帶有嵌入字段(Embedded Field)的結(jié)構(gòu)體定義。我們看下面這個例子:
type T1 int type t2 struct{ n int m int } type I interface { M1() } type S1 struct { T1 *t2 I a int b string }
我們看到,結(jié)構(gòu)體 S1
定義中有三個“非常規(guī)形式”的標識符,分別是 T1
、t2
和 I
,這三個標識符究竟代表的是什么呢?是字段名還是字段的類型呢?這里我直接告訴你答案:它們既代表字段的名字,也代表字段的類型。我們分別以這三個標識符為例,說明一下它們的具體含義:
- 標識符 T1 表示字段名為 T1,它的類型為自定義類型 T1;
- 標識符 t2 表示字段名為 t2,它的類型為自定義結(jié)構(gòu)體類型 t2 的指針類型;
- 標識符 I 表示字段名為 I,它的類型為接口類型 I。
這種以某個類型名、類型的指針類型名或接口類型名,直接作為結(jié)構(gòu)體字段的方式就叫做結(jié)構(gòu)體的類型嵌入,這些字段也被叫做嵌入字段(Embedded Field)。
那么,嵌入字段怎么用呢?它跟普通結(jié)構(gòu)體字段有啥不同呢?我們結(jié)合具體的例子,簡單說一下嵌入字段的用法:
type MyInt int func (n *MyInt) Add(m int) { *n = *n + MyInt(m) } type t struct { a int b int } type S struct { *MyInt t io.Reader s string n int } func main() { m := MyInt(17) r := strings.NewReader("hello, go") s := S{ MyInt: &m, t: t{ a: 1, b: 2, }, Reader: r, s: "demo", } var sl = make([]byte, len("hello, go")) s.Reader.Read(sl) fmt.Println(string(sl)) // hello, go s.MyInt.Add(5) fmt.Println(*(s.MyInt)) // 22 }
在分析這段代碼之前,我們要先明確一點,那就是嵌入字段的可見性與嵌入字段的類型的可見性是一致的。如果嵌入類型的名字是首字母大寫的,那么也就說明這個嵌入字段是可導(dǎo)出的。
現(xiàn)在我們來看這個例子。
首先,這個例子中的結(jié)構(gòu)體類型 S
使用了類型嵌入方式進行定義,它有三個嵌入字段 MyInt
、t
和 Reader
。這里,你可能會問,為什么第三個嵌入字段的名字為 Reader
而不是 io.Reader
?這是因為,Go 語言規(guī)定如果結(jié)構(gòu)體使用從其他包導(dǎo)入的類型作為嵌入字段,比如 pkg.T
,那么這個嵌入字段的字段名就是 T
,代表的類型為 pkg.T
。
接下來,我們再來看結(jié)構(gòu)體類型 S
的變量的初始化。我們使用 field:value
方式對 S
類型的變量 s
的各個字段進行初始化。和普通的字段一樣,初始化嵌入字段時,我們可以直接用嵌入字段名作為 field
。
而且,通過變量 s
使用這些嵌入字段時,我們也可以像普通字段那樣直接用 變量s + 字段選擇符 + 嵌入字段的名字
,比如 s.Reader
。我們還可以通過這種方式調(diào)用嵌入字段的方法,比如 s.Reader.Read
和 s.MyInt.Add
。
這樣看起來,嵌入字段的用法和普通字段沒啥不同呀?也不完全是,Go 還是對嵌入字段有一些約束的。比如,和 Go 方法的 receiver
的基類型一樣,嵌入字段類型的底層類型不能為指針類型。而且,嵌入字段的名字在結(jié)構(gòu)體定義也必須是唯一的,這也意味這如果兩個類型的名字相同,它們無法同時作為嵌入字段放到同一個結(jié)構(gòu)體定義中。不過,這些約束你了解一下就可以了,一旦違反,Go 編譯器會提示你的。
六、“實現(xiàn)繼承”的原理
將上面例子代碼做一下細微改動,我這里只列了變化部分的代碼:
var sl = make([]byte, len("hello, go")) s.Read(sl) fmt.Println(string(sl)) s.Add(5) fmt.Println(*(s.MyInt))
這段代碼中,類型 S
也沒有定義 Read
方法和 Add
方法,但是這段程序不但沒有引發(fā)編譯器報錯,還可以正常運行并輸出與前面例子相同的結(jié)果!
這段代碼似乎在告訴我們:Read
方法與 Add
方法就是類型 S
方法集合中的方法。但是,這里類型 S
明明沒有顯式實現(xiàn)這兩個方法呀,它是從哪里得到這兩個方法的實現(xiàn)的呢?
其實,這兩個方法就來自結(jié)構(gòu)體類型 S
的兩個嵌入字段 Reader
和 MyInt
。結(jié)構(gòu)體類型 S
“繼承”了 Reader
字段的方法 Read
的實現(xiàn),也“繼承”了 *MyInt
的 Add
方法的實現(xiàn)。注意,我這里的“繼承”用了引號,說明這并不是真正的繼承,它只是 Go 語言的一種“障眼法”。
這種“障眼法”的工作機制是這樣的,當(dāng)我們通過結(jié)構(gòu)體類型 S
的變量 s
調(diào)用 Read
方法時,Go 發(fā)現(xiàn)結(jié)構(gòu)體類型 S
自身并沒有定義 Read
方法,于是 Go 會查看 S
的嵌入字段對應(yīng)的類型是否定義了 Read
方法。這個時候,Reader
字段就被找了出來,之后 s.Read
的調(diào)用就被轉(zhuǎn)換為 s.Reader.Read
調(diào)用。
這樣一來,嵌入字段 Reader
的 Read
方法就被提升為 S
的方法,放入了類型 S
的方法集合。同理 *MyInt
的 Add
方法也被提升為 S
的方法而放入 S
的方法集合。從外部來看,這種嵌入字段的方法的提升就給了我們一種結(jié)構(gòu)體類型 S
“繼承”了 io.Reader
類型 Read
方法的實現(xiàn),以及 *MyInt
類型 Add
方法的實現(xiàn)的錯覺。
到這里,我們就清楚了,嵌入字段的使用的確可以幫我們在 Go 中實現(xiàn)方法的“繼承”。
在文章開頭,類型嵌入這種看似“繼承”的機制,實際上是一種組合的思想。更具體點,它是一種組合中的代理(delegate)模式,如下圖所示:
我們看到,S
只是一個代理(delegate
),對外它提供了它可以代理的所有方法,如例子中的 Read
和 Add
方法。當(dāng)外界發(fā)起對 S
的 Read
方法的調(diào)用后,S
將該調(diào)用委派給它內(nèi)部的 Reader
實例來實際執(zhí)行 Read
方法。
七、類型嵌入與方法集合
在前面,接口類型的類型嵌入時我們提到接口類型的類型嵌入的本質(zhì),就是嵌入類型的方法集合并入到新接口類型的方法集合中,并且,接口類型只能嵌入接口類型。而結(jié)構(gòu)體類型對嵌入類型的要求就比較寬泛了,可以是任意自定義類型或接口類型。
下面我們就分別看看,在這兩種情況下,結(jié)構(gòu)體類型的方法集合會有怎樣的變化。我們依舊借助上一講中的 dumpMethodSet
函數(shù)來輸出各個類型的方法集合,這里,我就不在例子中重復(fù)列出 dumpMethodSet
的代碼了。
7.1 結(jié)構(gòu)體類型中嵌入接口類型
在結(jié)構(gòu)體類型中嵌入接口類型后,結(jié)構(gòu)體類型的方法集合會發(fā)生什么變化呢?我們通過下面這個例子來看一下:
type I interface { M1() M2() } type T struct { I } func (T) M3() {} func main() { var t T var p *T dumpMethodSet(t) dumpMethodSet(p) }
運行這個示例,我們會得到以下結(jié)果:
main.T's method set:
- M1
- M2
- M3
*main.T's method set:
- M1
- M2
- M3
我們可以看到,原本結(jié)構(gòu)體類型 T
只帶有一個方法 M3
,但在嵌入接口類型 I
后,結(jié)構(gòu)體類型 T
的方法集合中又并入了接口類型 I
的方法集合。并且,由于 *T
類型方法集合包括 T
類型的方法集合,因此無論是類型 T
還是類型 *T
,它們的方法集合都包含 M1
、M2
和 M3
。于是我們可以得出一個結(jié)論:結(jié)構(gòu)體類型的方法集合,包含嵌入的接口類型的方法集合。
不過有一種情況,你要注意一下,那就是當(dāng)結(jié)構(gòu)體嵌入的多個接口類型的方法集合存在交集時,你要小心編譯器可能會出現(xiàn)的錯誤提示。
雖然Go 1.14 版本解決了嵌入接口類型的方法集合有交集的情況,但那僅限于接口類型中嵌入接口類型,這里我們說的是在結(jié)構(gòu)體類型中嵌入方法集合有交集的接口類型。
根據(jù)我們前面講的,嵌入了其他類型的結(jié)構(gòu)體類型本身是一個代理,在調(diào)用其實例所代理的方法時,Go 會首先查看結(jié)構(gòu)體自身是否實現(xiàn)了該方法。
如果實現(xiàn)了,Go 就會優(yōu)先使用結(jié)構(gòu)體自己實現(xiàn)的方法。如果沒有實現(xiàn),那么 Go 就會查找結(jié)構(gòu)體中的嵌入字段的方法集合中,是否包含了這個方法。如果多個嵌入字段的方法集合中都包含這個方法,那么我們就說方法集合存在交集。這個時候,Go 編譯器就會因無法確定究竟使用哪個方法而報錯,下面的這個例子就演示了這種情況:
type E1 interface { M1() M2() M3() } type E2 interface { M1() M2() M4() } type T struct { E1 E2 } func main() { t := T{} t.M1() t.M2() }
運行這個例子,我們會得到:
main.go:22:3: ambiguous selector t.M1
main.go:23:3: ambiguous selector t.M2
我們看到,Go 編譯器給出了錯誤提示,表示在調(diào)用 t.M1
和 t.M2
時,編譯器都出現(xiàn)了分歧。在這個例子中,結(jié)構(gòu)體類型 T
嵌入的兩個接口類型 E1
和 E2
的方法集合存在交集,都包含 M1
和 M2
,而結(jié)構(gòu)體類型 T
自身呢,又沒有實現(xiàn) M1
和 M2
,所以編譯器會因無法做出選擇而報錯。
那怎么解決這個問題呢?其實有兩種解決方案。一是,我們可以消除 E1 和 E2 方法集合存在交集的情況。二是為 T 增加 M1 和 M2 方法的實現(xiàn),這樣的話,編譯器便會直接選擇 T 自己實現(xiàn)的 M1 和 M2,不會陷入兩難境地。比如,下面的例子演示的就是 T 增加了 M1 和 M2 方法實現(xiàn)的情況:
... ... type T struct { E1 E2 } func (T) M1() { println("T's M1") } func (T) M2() { println("T's M2") } func main() { t := T{} t.M1() // T's M1 t.M2() // T's M2 }
結(jié)構(gòu)體類型嵌入接口類型在日常編碼中有一個妙用,就是可以簡化單元測試的編寫。由于嵌入某接口類型的結(jié)構(gòu)體類型的方法集合包含了這個接口類型的方法集合,這就意味著,這個結(jié)構(gòu)體類型也是它嵌入的接口類型的一個實現(xiàn)。即便結(jié)構(gòu)體類型自身并沒有實現(xiàn)這個接口類型的任意一個方法,也沒有關(guān)系。我們來看一個直觀的例子:
package employee type Result struct { Count int } func (r Result) Int() int { return r.Count } type Rows []struct{} type Stmt interface { Close() error NumInput() int Exec(stmt string, args ...string) (Result, error) Query(args []string) (Rows, error) } // 返回男性員工總數(shù) func MaleCount(s Stmt) (int, error) { result, err := s.Exec("select count(*) from employee_tab where gender=?", "1") if err != nil { return 0, err } return result.Int(), nil }
在這個例子中,我們有一個 employee
包,這個包中的方法 MaleCount
,通過傳入的 Stmt
接口的實現(xiàn)從數(shù)據(jù)庫獲取男性員工的數(shù)量。
現(xiàn)在我們的任務(wù)是要對 MaleCount
方法編寫單元測試代碼。對于這種依賴外部數(shù)據(jù)庫操作的方法,我們的慣例是使用“偽對象(fake object)”來冒充真實的 Stmt
接口實現(xiàn)。
不過現(xiàn)在有一個問題,那就是 Stmt
接口類型的方法集合中有四個方法,而 MaleCount
函數(shù)只使用了 Stmt
接口的一個方法 Exec
。如果我們針對每個測試用例所用的偽對象都實現(xiàn)這四個方法,那么這個工作量有些大。
那么這個時候,我們怎樣快速建立偽對象呢?結(jié)構(gòu)體類型嵌入接口類型便可以幫助我們,下面是我們的解決方案:
package employee import "testing" type fakeStmtForMaleCount struct { Stmt } func (fakeStmtForMaleCount) Exec(stmt string, args ...string) (Result, error) { return Result{Count: 5}, nil } func TestEmployeeMaleCount(t *testing.T) { f := fakeStmtForMaleCount{} c, _ := MaleCount(f) if c != 5 { t.Errorf("want: %d, actual: %d", 5, c) return } }
我們?yōu)?nbsp;TestEmployeeMaleCount
測試用例建立了一個 fakeStmtForMaleCount
的偽對象類型,然后在這個類型中嵌入了 Stmt
接口類型。這樣 fakeStmtForMaleCount
就實現(xiàn)了 Stmt
接口,我們也實現(xiàn)了快速建立偽對象的目的。接下來我們只需要為 fakeStmtForMaleCount
實現(xiàn) MaleCount
所需的 Exec
方法,就可以滿足這個測試的要求了。
7.2 結(jié)構(gòu)體類型中嵌入結(jié)構(gòu)體類型
在前面結(jié)構(gòu)體類型中嵌入結(jié)構(gòu)體類型,為 Gopher 們提供了一種“實現(xiàn)繼承”的手段,外部的結(jié)構(gòu)體類型 T
可以“繼承”嵌入的結(jié)構(gòu)體類型的所有方法的實現(xiàn)。并且,無論是 T
類型的變量實例還是 *T
類型變量實例,都可以調(diào)用所有“繼承”的方法。但這種情況下,帶有嵌入類型的新類型究竟“繼承”了哪些方法,我們還要通過下面這個具體的示例來看一下。
type T1 struct{} func (T1) T1M1() { println("T1's M1") } func (*T1) PT1M2() { println("PT1's M2") } type T2 struct{} func (T2) T2M1() { println("T2's M1") } func (*T2) PT2M2() { println("PT2's M2") } type T struct { T1 *T2 } func main() { t := T{ T1: T1{}, T2: &T2{}, } dumpMethodSet(t) dumpMethodSet(&t) }
在這個例子中,結(jié)構(gòu)體類型 T
有兩個嵌入字段,分別是 T1
和 *T2
,根據(jù)上一講中我們對結(jié)構(gòu)體的方法集合的講解,我們知道 T1
與 *T1
、T2
與 *T2
的方法集合是不同的:
- T1 的方法集合包含:
T1M1
; *T1
的方法集合包含:T1M1
、PT1M2
;T2
的方法集合包含:T2M1
;*T2
的方法集合包含:T2M1
、PT2M2
。
它們作為嵌入字段嵌入到 T
中后,對 T
和 *T
的方法集合的影響也是不同的。我們運行一下這個示例,看一下輸出結(jié)果:
main.T's method set:
- PT2M2
- T1M1
- T2M1
*main.T's method set:
- PT1M2
- PT2M2
- T1M1
- T2M1
通過輸出結(jié)果,我們看到了 T
和 *T
類型的方法集合果然有差別的:
- 類型
T
的方法集合 =T1
的方法集合 +*T2
的方法集合 - 類型
*T
的方法集合 =*T1
的方法集合 +*T2
的方法集合
這里,我們尤其要注意 *T
類型的方法集合,它包含的可不是 T1
類型的方法集合,而是 *T1
類型的方法集合。這和結(jié)構(gòu)體指針類型的方法集合包含結(jié)構(gòu)體類型方法集合,是一個道理。
到這里,基于類型嵌入“繼承”方法實現(xiàn)的原理,我們基本都清楚了。但不知道你會不會還有一點疑惑:只有通過類型嵌入才能實現(xiàn)方法“繼承”嗎?如果我使用類型聲明語法基于一個已有類型 T
定義一個新類型 NT
,那么 NT
是不是可以直接繼承 T
的所有方法呢?
八、defined 類型與 alias 類型是否可以實現(xiàn)方法集合的“繼承”
8.1 defined 類型與 alias 類型的方法集合
Go 語言中,凡通過類型聲明語法聲明的類型都被稱為 defined
類型,下面是一些 defined
類型的聲明的例子:
type I interface { M1() M2() } type T int type NT T // 基于已存在的類型T創(chuàng)建新的defined類型NT type NI I // 基于已存在的接口類型I創(chuàng)建新defined接口類型NI
新定義的 defined
類型與原 defined
類型是不同的類型,那么它們的方法集合上又會有什么關(guān)系呢?新類型是否“繼承”原 defined
類型的方法集合呢?
這個問題,我們也要分情況來看。
對于那些基于接口類型創(chuàng)建的 defined
的接口類型,它們的方法集合與原接口類型的方法集合是一致的。但對于基于非接口類型的 defined
類型創(chuàng)建的非接口類型,我們通過下面例子來看一下:
package main type T struct{} func (T) M1() {} func (*T) M2() {} type T1 T func main() { var t T var pt *T var t1 T1 var pt1 *T1 dumpMethodSet(t) dumpMethodSet(t1) dumpMethodSet(pt) dumpMethodSet(pt1) }
在這個例子中,我們基于一個 defined
的非接口類型 T
創(chuàng)建了新 defined
類型 T1
,并且分別輸出 T1
和 *T1
的方法集合來確認它們是否“繼承”了 T
的方法集合。
運行這個示例程序,我們得到如下結(jié)果:
main.T's method set:
- M1
main.T1's method set is empty!
*main.T's method set:
- M1
- M2
*main.T1's method set is empty!
從輸出結(jié)果上看,新類型 T1
并沒有“繼承”原 defined
類型 T
的任何一個方法。從邏輯上來說,這也符合 T1
與 T
是兩個不同類型的語義。
基于自定義非接口類型的 defined
類型的方法集合為空的事實,也決定了即便原類型實現(xiàn)了某些接口,基于其創(chuàng)建的 defined
類型也沒有“繼承”這一隱式關(guān)聯(lián)。也就是說,新 defined
類型要想實現(xiàn)那些接口,仍然需要重新實現(xiàn)接口的所有方法。
那么,基于類型別名(type alias
)定義的新類型有沒有“繼承”原類型的方法集合呢?我們還是來看一個例子:
type T struct{} func (T) M1() {} func (*T) M2() {} type T1 = T func main() { var t T var pt *T var t1 T1 var pt1 *T1 dumpMethodSet(t) dumpMethodSet(t1) dumpMethodSet(pt) dumpMethodSet(pt1) }
這個例子改自之前那個例子,我只是將 T1 的定義方式由類型聲明改成了類型別名,我們看一下這個例子的輸出結(jié)果:
main.T's method set:
- M1
main.T's method set:
- M1
*main.T's method set:
- M1
- M2
*main.T's method set:
- M1
- M2
通過這個輸出結(jié)果,我們看到,我們的 dumpMethodSet
函數(shù)甚至都無法識別出“類型別名”,無論類型別名還是原類型,輸出的都是原類型的方法集合。
由此我們可以得到一個結(jié)論:無論原類型是接口類型還是非接口類型,類型別名都與原類型擁有完全相同的方法集合。
九、小結(jié)
類型嵌入分為兩種,一種是接口類型的類型嵌入,對于接口類型的類型嵌入我們只要把握好其語義“方法集合并入”就可以了。另外一種是結(jié)構(gòu)體類型的類型嵌入。通過在結(jié)構(gòu)體定義中的嵌入字段,我們可以實現(xiàn)對嵌入類型的方法集合的“繼承”。
但這種“繼承”并非經(jīng)典面向?qū)ο蠓妒街械哪莻€繼承,Go 中的“繼承”實際是一種組合,更具體點是組合思想下代理(delegate
)模式的運用,也就是新類型代理了其嵌入類型的所有方法。當(dāng)外界調(diào)用新類型的方法時,Go 編譯器會首先查找新類型是否實現(xiàn)了這個方法,如果沒有,就會將調(diào)用委派給其內(nèi)部實現(xiàn)了這個方法的嵌入類型的實例去執(zhí)行,你一定要理解這個原理。
此外,你還要牢記類型嵌入對新類型的方法集合的影響,包括:
- 結(jié)構(gòu)體類型的方法集合包含嵌入的接口類型的方法集合;
- 當(dāng)結(jié)構(gòu)體類型
T
包含嵌入字段E
時,*T
的方法集合不僅包含類型E
的方法集合,還要包含類型*E
的方法集合。
最后,基于非接口類型的 defined
類型創(chuàng)建的新 defined
類型不會繼承原類型的方法集合,而通過類型別名定義的新類型則和原類型擁有相同的方法集合。
以上就是淺析Golang中類型嵌入的簡介與使用的詳細內(nèi)容,更多關(guān)于Go類型嵌入的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
基于golang channel實現(xiàn)的輕量級異步任務(wù)分發(fā)器示例代碼
這篇文章主要給大家介紹了關(guān)于基于golang channel實現(xiàn)的輕量級異步任務(wù)分發(fā)器的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07Golang項目在github創(chuàng)建release后自動生成二進制文件的方法
這篇文章主要介紹了Golang項目在github創(chuàng)建release后如何自動生成二進制文件,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-03-03Golang設(shè)計模式之責(zé)任鏈模式講解和代碼示例
責(zé)任鏈是一種行為設(shè)計模式, 允許你將請求沿著處理者鏈進行發(fā)送, 直至其中一個處理者對其進行處理,本文就詳細給大家介紹一下Golang 責(zé)任鏈模式,文中有詳細的代碼示例,需要的朋友可以參考下2023-06-06