Go型別嵌入介紹和使用型別嵌入模擬實現“繼承”

賈維斯Echo發表於2023-11-05

Go型別嵌入介紹和使用型別嵌入模擬實現“繼承”

一、獨立的自定義型別

什麼是獨立的自定義型別呢?就是這個型別的所有方法都是自己顯式實現的。

我們舉個例子,自定義型別 T 有兩個方法 M1M2,如果 T 是一個獨立的自定義型別,那我們在宣告型別 T 的 Go 包原始碼檔案中一定可以找到其所有方法的實現程式碼,比如:

func (T) M1() {...}
func (T) M2() {...}

難道還有某種自定義型別的方法不是自己顯式實現的嗎?當涉及到 Go 語言中的自定義型別時,有一種方法可以不需要顯式地實現方法,即:讓某個自定義型別“繼承”其他型別的方法實現。

二、繼承

Go 語言從設計伊始,就決定不支援經典物件導向的程式設計正規化與語法元素,所以我們這裡只是借用了“繼承”這個詞彙而已,說是“繼承”,實則依舊是一種組合的思想

這種“繼承”是透過 Go 語言的型別嵌入(Type Embedding)來實現的。

三、型別嵌入

3.1 什麼是型別嵌入

型別嵌入指的就是在一個型別的定義中嵌入了其他型別。Go 語言支援兩種型別嵌入,分別是介面型別的型別嵌入和結構體型別的型別嵌入。

四、介面型別的型別嵌入

4.1 介面型別的型別嵌入介紹

介面型別的型別嵌入是指在一個介面型別的定義中嵌入其他介面型別,從而使介面型別包含了嵌入介面中定義的方法。這允許一個介面型別繼承另一個介面型別的方法集,以擴充套件其功能。

總結介面型別的型別嵌入的關鍵點:

  1. 嵌入介面型別:介面型別可以嵌入其他介面型別,將其方法集合併到當前介面中。
  2. 繼承方法集:透過嵌入,介面型別可以繼承嵌入介面中的方法,使得當前介面也具有這些方法。
  3. 實現多型:透過介面型別的型別嵌入,可以實現多型,使不同型別的物件可以被統一地處理,提高程式碼的靈活性。

這種機制使得Go語言的介面更加靈活和可擴充套件,允許將不同的介面組合在一起,以建立更復雜的介面,從而促進了程式碼的重用和可維護性。

4.2 一個小案例

接著,我們用一個案例,直觀地瞭解一下什麼是介面型別的型別嵌入。我們知道,介面型別宣告瞭由一個方法集合代表的介面,比如下面介面型別 E

type E interface {
    M1()
    M2()
}

這個介面型別 E 的方法集合,包含兩個方法,分別是 M1M2,它們組成了 E 這個介面型別所代表的介面。如果某個型別實現了方法 M1M2,我們就說這個型別實現了 E 所代表的介面。

此時,我們再定義另外一個介面型別 I,它的方法集合中包含了三個方法 M1M2M3,如下面程式碼:

type I interface {
    M1()
    M2()
    M3()
}

我們看到介面型別 I 方法集合中的 M1M2,與介面型別 E 的方法集合中的方法完全相同。在這種情況下,我們可以用介面型別 E 替代上面介面型別 I 定義中 M1M2如下面程式碼:

type I interface {
    E
    M3()
}

像這種在一個介面型別(I)定義中,嵌入另外一個介面型別(E)的方式,就是我們說的介面型別的型別嵌入

而且,這個帶有型別嵌入的介面型別 I 的定義與上面那個包含 M1M2M3 的介面型別 I 的定義,是等價的。因此,我們可以得到一個結論,這種介面型別嵌入的語義就是新介面型別(如介面型別 I)將嵌入的介面型別(如介面型別 E)的方法集合,併入到自己的方法集合中。

其實,使用型別嵌入方式定義介面型別也是 Go 組合設計哲學的一種體現

按 Go 語言慣例,Go 中的介面型別中只包含少量方法,並且常常只是一個方法。透過在介面型別中嵌入其他介面型別可以實現介面的組合,這也是 Go 語言中基於已有介面型別構建新介面型別的慣用法。

按 Go 語言慣例,Go 中的介面型別中只包含少量方法,並且常常只是一個方法。透過在介面型別中嵌入其他介面型別可以實現介面的組合,這也是 Go 語言中基於已有介面型別構建新介面型別的慣用法。

我們在 Go 標準庫中可以看到很多這種組合方式的應用,最常見的莫過於 io 包中一系列介面的定義了。比如,io 包的 ReadWriterReadWriteCloser 等介面型別就是透過嵌入 ReaderWriterCloser 三個基本的介面型別組合而成的。下面是僅包含單一方法的 ioReaderWriterCloser 的定義:

// $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 包的 ReadWriterReadWriteCloser 等介面型別,透過嵌入上面基本介面型別組合而形成:

type ReadWriter interface {
    Reader
    Writer
}

type ReadCloser interface {
    Reader
    Closer
}

type WriteCloser interface {
    Writer
    Closer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

不過,這種透過嵌入其他介面型別來建立新介面型別的方式,在 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 中嵌入的兩個介面型別 Interface1Interface2 的方法集合有交集,交集是方法 M1;第二個報錯是因為 Interface4 型別中的方法 M2 與嵌入的介面型別 Interface2 的方法 M2 重名。

但自 Go 1.14 版本開始,Go 語言去除了這些約束,我們使用 Go 最新版本執行上面這個示例就不會得到編譯錯誤了。

介面型別的型別嵌入比較簡單,我們只要把握好它的語義,也就是“方法集合併入”就可以了。

五、結構體型別的型別嵌入

5.1 結構體型別的型別嵌入介紹

結構體型別的型別嵌入是一種特殊的結構體定義方式,其中結構體的欄位名可以直接使用型別名、型別的指標型別名或介面型別名,代表欄位的名字和型別。以下是結構體型別的型別嵌入的關鍵點:

  1. 欄位名和型別合二為一:在結構體型別的型別嵌入中,欄位名和型別名合併成一個識別符號,既代表了欄位的名字又代表了欄位的型別。這使得欄位名與型別名保持一致,簡化了結構體定義。
  2. 嵌入欄位:這種方式被稱為嵌入欄位(Embedded Field),其中嵌入欄位的型別可以是自定義型別、結構體型別的指標型別,或介面型別。
  3. 訪問嵌入欄位:可以透過結構體變數來訪問嵌入欄位的欄位和方法,無需使用欄位名,因為欄位名已經隱含在型別中。
  4. 欄位名與型別名一致:嵌入欄位的欄位名與型別名一致,這種一致性使得程式碼更加清晰和直觀。
  5. 型別組合:透過嵌入欄位,可以將不同型別的功能組合在一個結構體中,形成更復雜的資料結構,提高程式碼的可維護性和擴充套件性。

5.2 小案例

通常,結構體都是類似下面這樣的:

type S struct {
    A int
    b string
    c T
    p *P
    _ [10]int8
    F func()
}

結構體型別 S 中的每個欄位(field)都有唯一的名字與對應的型別,即便是使用空識別符號佔位的欄位,它的型別也是明確的,但這還不是 Go 結構體型別的“完全體”。Go 結構體型別定義還有另外一種形式,那就是帶有嵌入欄位(Embedded Field)的結構體定義。我們看下面這個例子:

type T1 int
type t2 struct{
    n int
    m int
}

type I interface {
    M1()
}

type S1 struct {
    T1
    *t2
    I            
    a int
    b string
}

我們看到,結構體 S1 定義中有三個“非常規形式”的識別符號,分別是 T1t2I,這三個識別符號究竟代表的是什麼呢?是欄位名還是欄位的型別呢?這裡我直接告訴你答案:它們既代表欄位的名字,也代表欄位的型別。我們分別以這三個識別符號為例,說明一下它們的具體含義:

  • 識別符號 T1 表示欄位名為 T1,它的型別為自定義型別 T1;
  • 識別符號 t2 表示欄位名為 t2,它的型別為自定義結構體型別 t2 的指標型別;
  • 識別符號 I 表示欄位名為 I,它的型別為介面型別 I。

這種以某個型別名、型別的指標型別名或介面型別名,直接作為結構體欄位的方式就叫做結構體的型別嵌入,這些欄位也被叫做嵌入欄位(Embedded Field)。

那麼,嵌入欄位怎麼用呢?它跟普通結構體欄位有啥不同呢?我們結合具體的例子,簡單說一下嵌入欄位的用法:

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
}

在分析這段程式碼之前,我們要先明確一點,那就是嵌入欄位的可見性與嵌入欄位的型別的可見性是一致的。如果嵌入型別的名字是首字母大寫的,那麼也就說明這個嵌入欄位是可匯出的。

現在我們來看這個例子。

首先,這個例子中的結構體型別 S 使用了型別嵌入方式進行定義,它有三個嵌入欄位 MyInttReader。這裡,你可能會問,為什麼第三個嵌入欄位的名字為 Reader 而不是 io.Reader?這是因為,Go 語言規定如果結構體使用從其他包匯入的型別作為嵌入欄位,比如 pkg.T,那麼這個嵌入欄位的欄位名就是 T,代表的型別為 pkg.T

接下來,我們再來看結構體型別 S 的變數的初始化。我們使用 field:value 方式對 S 型別的變數 s 的各個欄位進行初始化。和普通的欄位一樣,初始化嵌入欄位時,我們可以直接用嵌入欄位名作為 field

而且,透過變數 s 使用這些嵌入欄位時,我們也可以像普通欄位那樣直接用 變數s + 欄位選擇符 + 嵌入欄位的名字,比如 s.Reader。我們還可以透過這種方式呼叫嵌入欄位的方法,比如 s.Reader.Reads.MyInt.Add

這樣看起來,嵌入欄位的用法和普通欄位沒啥不同呀?也不完全是,Go 還是對嵌入欄位有一些約束的。比如,和 Go 方法的 receiver 的基型別一樣,嵌入欄位型別的底層型別不能為指標型別。而且,嵌入欄位的名字在結構體定義也必須是唯一的,這也意味這如果兩個型別的名字相同,它們無法同時作為嵌入欄位放到同一個結構體定義中。不過,這些約束你瞭解一下就可以了,一旦違反,Go 編譯器會提示你的。

六、“實現繼承”的原理

將上面例子程式碼做一下細微改動,我這裡只列了變化部分的程式碼:

var sl = make([]byte, len("hello, go"))
s.Read(sl) 
fmt.Println(string(sl))
s.Add(5) 
fmt.Println(*(s.MyInt))

這段程式碼中,型別 S 也沒有定義 Read 方法和 Add 方法,但是這段程式不但沒有引發編譯器報錯,還可以正常執行並輸出與前面例子相同的結果!

這段程式碼似乎在告訴我們:Read 方法與 Add 方法就是型別 S 方法集合中的方法。但是,這裡型別 S 明明沒有顯式實現這兩個方法呀,它是從哪裡得到這兩個方法的實現的呢?

其實,這兩個方法就來自結構體型別 S 的兩個嵌入欄位 ReaderMyInt。結構體型別 S “繼承”了 Reader 欄位的方法 Read 的實現,也“繼承”了 *MyIntAdd 方法的實現。注意,我這裡的“繼承”用了引號,說明這並不是真正的繼承,它只是 Go 語言的一種“障眼法”。

這種“障眼法”的工作機制是這樣的,當我們透過結構體型別 S 的變數 s 呼叫 Read 方法時,Go 發現結構體型別 S 自身並沒有定義 Read 方法,於是 Go 會檢視 S 的嵌入欄位對應的型別是否定義了 Read 方法。這個時候,Reader 欄位就被找了出來,之後 s.Read 的呼叫就被轉換為 s.Reader.Read 呼叫。

這樣一來,嵌入欄位 ReaderRead 方法就被提升為 S 的方法,放入了型別 S 的方法集合。同理 *MyIntAdd 方法也被提升為 S 的方法而放入 S 的方法集合。從外部來看,這種嵌入欄位的方法的提升就給了我們一種結構體型別 S“繼承”了 io.Reader 型別 Read 方法的實現,以及 *MyInt 型別 Add 方法的實現的錯覺。

到這裡,我們就清楚了,嵌入欄位的使用的確可以幫我們在 Go 中實現方法的“繼承”。

在文章開頭,型別嵌入這種看似“繼承”的機制,實際上是一種組合的思想。更具體點,它是一種組合中的代理(delegate)模式,如下圖所示:

WechatIMG247

我們看到,S 只是一個代理(delegate),對外它提供了它可以代理的所有方法,如例子中的 ReadAdd 方法。當外界發起對 SRead 方法的呼叫後,S 將該呼叫委派給它內部的 Reader 例項來實際執行 Read 方法。

七、型別嵌入與方法集合

在前面,介面型別的型別嵌入時我們提到介面型別的型別嵌入的本質,就是嵌入型別的方法集合併入到新介面型別的方法集合中,並且,介面型別只能嵌入介面型別。而結構體型別對嵌入型別的要求就比較寬泛了,可以是任意自定義型別或介面型別。

下面我們就分別看看,在這兩種情況下,結構體型別的方法集合會有怎樣的變化。我們依舊藉助上一講中的 dumpMethodSet 函式來輸出各個型別的方法集合,這裡,我就不在例子中重複列出 dumpMethodSet 的程式碼了。

7.1 結構體型別中嵌入介面型別

在結構體型別中嵌入介面型別後,結構體型別的方法集合會發生什麼變化呢?我們透過下面這個例子來看一下:

type I interface {
    M1()
    M2()
}

type T struct {
    I
}

func (T) M3() {}

func main() {
    var t T
    var p *T
    dumpMethodSet(t)
    dumpMethodSet(p)
}

執行這個示例,我們會得到以下結果:

main.T's method set:
- M1
- M2
- M3

*main.T's method set:
- M1
- M2
- M3

我們可以看到,原本結構體型別 T 只帶有一個方法 M3,但在嵌入介面型別 I 後,結構體型別 T 的方法集合中又併入了介面型別 I 的方法集合。並且,由於 *T 型別方法集合包括 T 型別的方法集合,因此無論是型別 T 還是型別 *T,它們的方法集合都包含 M1M2M3。於是我們可以得出一個結論:結構體型別的方法集合,包含嵌入的介面型別的方法集合。

不過有一種情況,你要注意一下,那就是當結構體嵌入的多個介面型別的方法集合存在交集時,你要小心編譯器可能會出現的錯誤提示。

雖然Go 1.14 版本解決了嵌入介面型別的方法集合有交集的情況,但那僅限於介面型別中嵌入介面型別,這裡我們說的是在結構體型別中嵌入方法集合有交集的介面型別。

根據我們前面講的,嵌入了其他型別的結構體型別本身是一個代理,在呼叫其例項所代理的方法時,Go 會首先檢視結構體自身是否實現了該方法。

如果實現了,Go 就會優先使用結構體自己實現的方法。如果沒有實現,那麼 Go 就會查詢結構體中的嵌入欄位的方法集合中,是否包含了這個方法。如果多個嵌入欄位的方法集合中都包含這個方法,那麼我們就說方法集合存在交集。這個時候,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 編譯器給出了錯誤提示,表示在呼叫 t.M1t.M2 時,編譯器都出現了分歧。在這個例子中,結構體型別 T 嵌入的兩個介面型別 E1E2 的方法集合存在交集,都包含 M1M2,而結構體型別 T 自身呢,又沒有實現 M1M2,所以編譯器會因無法做出選擇而報錯。

那怎麼解決這個問題呢?其實有兩種解決方案。一是,我們可以消除 E1 和 E2 方法集合存在交集的情況。二是為 T 增加 M1 和 M2 方法的實現,這樣的話,編譯器便會直接選擇 T 自己實現的 M1 和 M2,不會陷入兩難境地。比如,下面的例子演示的就是 T 增加了 M1 和 M2 方法實現的情況:

... ...
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
}

結構體型別嵌入介面型別在日常編碼中有一個妙用,就是可以簡化單元測試的編寫。由於嵌入某介面型別的結構體型別的方法集合包含了這個介面型別的方法集合,這就意味著,這個結構體型別也是它嵌入的介面型別的一個實現。即便結構體型別自身並沒有實現這個介面型別的任意一個方法,也沒有關係。我們來看一個直觀的例子:

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)
}

// 返回男性員工總數
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 介面的實現從資料庫獲取男性員工的數量。

現在我們的任務是要對 MaleCount 方法編寫單元測試程式碼。對於這種依賴外部資料庫操作的方法,我們的慣例是使用“偽物件(fake object)”來冒充真實的 Stmt 介面實現。

不過現在有一個問題,那就是 Stmt 介面型別的方法集合中有四個方法,而 MaleCount 函式只使用了 Stmt 介面的一個方法 Exec。如果我們針對每個測試用例所用的偽物件都實現這四個方法,那麼這個工作量有些大。

那麼這個時候,我們怎樣快速建立偽物件呢?結構體型別嵌入介面型別便可以幫助我們,下面是我們的解決方案:

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
    }
}

我們為 TestEmployeeMaleCount 測試用例建立了一個 fakeStmtForMaleCount 的偽物件型別,然後在這個型別中嵌入了 Stmt 介面型別。這樣 fakeStmtForMaleCount 就實現了 Stmt 介面,我們也實現了快速建立偽物件的目的。接下來我們只需要為 fakeStmtForMaleCount 實現 MaleCount 所需的 Exec 方法,就可以滿足這個測試的要求了。

7.2 結構體型別中嵌入結構體型別

在前面結構體型別中嵌入結構體型別,為 Gopher 們提供了一種“實現繼承”的手段,外部的結構體型別 T 可以“繼承”嵌入的結構體型別的所有方法的實現。並且,無論是 T 型別的變數例項還是 *T 型別變數例項,都可以呼叫所有“繼承”的方法。但這種情況下,帶有嵌入型別的新型別究竟“繼承”了哪些方法,我們還要透過下面這個具體的示例來看一下。

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)
}

在這個例子中,結構體型別 T 有兩個嵌入欄位,分別是 T1*T2,根據上一講中我們對結構體的方法集合的講解,我們知道 T1*T1T2*T2 的方法集合是不同的:

  • T1 的方法集合包含:T1M1
  • *T1 的方法集合包含:T1M1PT1M2
  • T2 的方法集合包含:T2M1
  • *T2 的方法集合包含:T2M1PT2M2

它們作為嵌入欄位嵌入到 T 中後,對 T*T 的方法集合的影響也是不同的。我們執行一下這個示例,看一下輸出結果:

main.T's method set:
- PT2M2
- T1M1
- T2M1

*main.T's method set:
- PT1M2
- PT2M2
- T1M1
- T2M1

透過輸出結果,我們看到了 T*T 型別的方法集合果然有差別的:

  • 型別 T 的方法集合 = T1 的方法集合 + *T2 的方法集合
  • 型別 *T 的方法集合 = *T1 的方法集合 + *T2 的方法集合

這裡,我們尤其要注意 *T 型別的方法集合,它包含的可不是 T1 型別的方法集合,而是 *T1 型別的方法集合。這和結構體指標型別的方法集合包含結構體型別方法集合,是一個道理。

到這裡,基於型別嵌入“繼承”方法實現的原理,我們基本都清楚了。但不知道你會不會還有一點疑惑:只有透過型別嵌入才能實現方法“繼承”嗎?如果我使用型別宣告語法基於一個已有型別 T 定義一個新型別 NT,那麼 NT 是不是可以直接繼承 T 的所有方法呢?

八、defined 型別與 alias 型別是否可以實現方法集合的“繼承”?

8.1 defined 型別與 alias 型別的方法集合

Go 語言中,凡透過型別宣告語法宣告的型別都被稱為 defined 型別,下面是一些 defined 型別的宣告的例子:

type I interface {
    M1()
    M2()
}
type T int
type NT T // 基於已存在的型別T建立新的defined型別NT
type NI I // 基於已存在的介面型別I建立新defined介面型別NI

新定義的 defined 型別與原 defined 型別是不同的型別,那麼它們的方法集合上又會有什麼關係呢?新型別是否“繼承”原 defined 型別的方法集合呢?

這個問題,我們也要分情況來看。

對於那些基於介面型別建立的 defined 的介面型別,它們的方法集合與原介面型別的方法集合是一致的。但對於基於非介面型別的 defined 型別建立的非介面型別,我們透過下面例子來看一下:

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 建立了新 defined 型別 T1,並且分別輸出 T1*T1 的方法集合來確認它們是否“繼承”了 T 的方法集合。

執行這個示例程式,我們得到如下結果:

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!

從輸出結果上看,新型別 T1 並沒有“繼承”原 defined 型別 T 的任何一個方法。從邏輯上來說,這也符合 T1T 是兩個不同型別的語義。

基於自定義非介面型別的 defined 型別的方法集合為空的事實,也決定了即便原型別實現了某些介面,基於其建立的 defined 型別也沒有“繼承”這一隱式關聯。也就是說,新 defined 型別要想實現那些介面,仍然需要重新實現介面的所有方法。

那麼,基於型別別名(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 的定義方式由型別宣告改成了型別別名,我們看一下這個例子的輸出結果:

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

透過這個輸出結果,我們看到,我們的 dumpMethodSet 函式甚至都無法識別出“型別別名”,無論型別別名還是原型別,輸出的都是原型別的方法集合。

由此我們可以得到一個結論:無論原型別是介面型別還是非介面型別,型別別名都與原型別擁有完全相同的方法集合。

九、小結

型別嵌入分為兩種,一種是介面型別的型別嵌入,對於介面型別的型別嵌入我們只要把握好其語義“方法集合併入”就可以了。另外一種是結構體型別的型別嵌入。透過在結構體定義中的嵌入欄位,我們可以實現對嵌入型別的方法集合的“繼承”。

但這種“繼承”並非經典物件導向正規化中的那個繼承,Go 中的“繼承”實際是一種組合,更具體點是組合思想下代理(delegate)模式的運用,也就是新型別代理了其嵌入型別的所有方法。當外界呼叫新型別的方法時,Go 編譯器會首先查詢新型別是否實現了這個方法,如果沒有,就會將呼叫委派給其內部實現了這個方法的嵌入型別的例項去執行,你一定要理解這個原理。

此外,你還要牢記型別嵌入對新型別的方法集合的影響,包括:

  • 結構體型別的方法集合包含嵌入的介面型別的方法集合;

  • 當結構體型別 T 包含嵌入欄位 E 時,*T 的方法集合不僅包含型別 E 的方法集合,還要包含型別 *E 的方法集合。

最後,基於非介面型別的 defined 型別建立的新 defined 型別不會繼承原型別的方法集合,而透過型別別名定義的新型別則和原型別擁有相同的方法集合。

相關文章