Go沒有提供經典的類型驅動式的派生類概念,但卻可以通過*內嵌*其他類型或接口代碼的方式來實現類似的功能。
接口的“內嵌”比較簡單。我們之前曾提到過`io.Reader`和`io.Writer`這兩個接口,以下是它們的實現:
~~~
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
~~~
在`io`包中,還提供了許多其它的接口,它們定義一類可以同時實現幾個不同接口的類型。例如`io.ReadWriter`接口,它同時包含了`Read`和`Write`兩個接口。盡管可以通過列出`Read`和`Write`兩個方法的詳細聲明的方式來定義`io.ReadWriter`接口,但是以內嵌兩個已有接口進行定義的方式會使代碼顯得更加簡潔、直觀:
~~~
// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
Reader
Writer
}
~~~
這段代碼的意義很容易理解:一個`ReadWriter`類型可以同時完成`Reader`*和*`Writer`的功能,它是這些內嵌接口的聯合(這些內嵌接口必須是一組不相干的方法)。接口只能“內嵌”接口類型。
類似的想法也可以應用于結構體的定義,其實現稍稍復雜一些。在`bufio`包中,有兩個結構體類型:`bufio.Reader`和?`bufio.Writer`,它們分別實現了`io`包中的類似接口。`bufio`包還實現了一個帶緩沖的reader/writer類型,實現的方法是將reader和writer組合起來內嵌到一個結構體中:在結構體中,只列出了兩種類型,但沒有給出對應的字段名。
~~~
// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
~~~
內嵌的元素是指向結構體的指針,因此在使用前,必須將其初始化并指向有效的結構體數據。結構體`ReadWriter`可以被寫作如下形式:
~~~
type ReadWriter struct {
reader *Reader
writer *Writer
}
~~~
為了使各字段對應的方法能滿足`io`的接口規范,我們還需要提供如下的方法:
~~~
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}
~~~
通過對結構體直接進行“內嵌”,我們避免了一些復雜的記錄。所有內嵌類型的方法可以不受約束的使用,換句話說,`bufio.ReadWriter`類型不僅具有`bufio.Reader`和`bufio.Writer`兩個方法,同時也滿足`io.Reader`,`io.Writer`和`io.ReadWriter`這三個接口。
在“內嵌”和“子類型”兩種方法間存在一個重要的區別。當我們內嵌一個類型時,該類型的所有方法會變成外部類型的方法,但是當這些方法被調用時,其接收的參數仍然是內部類型,而非外部類型。在本例中,一個`bufio.ReadWriter`類型的`Read`方法被調用時,其效果和調用我們剛剛實現的那個`Read`方法是一樣的,只不過前者接收的參數是`ReadWriter`的`reader`字段,而不是`ReadWriter`本身。
“內嵌”還可以用一種更簡單的方式表達。下面的例子展示了如何將內嵌字段和一個普通的命名字段同時放在一個結構體定義中。
~~~
type Job struct {
Command string
*log.Logger
}
~~~
現在,`Job`類型擁有了`Log`,`Logf`以及`*log.Logger`的其他所有方法。當然,我們可以給`Logger`提供一個命名字段,但完全沒有必要這樣做。現在,當初始化結束后,就可以在`Job`類型上調用日志記錄功能了。
~~~
job.Log("starting now...")
~~~
`Logger`是結構體`Job`的一個常規字段,因此我們可以在`Job`的構造方法中按通用方式對其進行初始化:
~~~
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
~~~
或者寫成下面的形式:
~~~
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
~~~
如果我們需要直接引用一個內嵌的字段,那么將該字段的類型名稱省略了包名后,就可以作為字段名使用,正如之前在`ReaderWriter`結構體的`Read`方法中實現的那樣。可以用`job.Logger`訪問`Job`類型變量`job`的`*log.Logger`字段。當需要重新定義`Logger`的方法時,這種引用方式就變得非常有用了。
~~~
func (job *Job) Logf(format string, args ...interface{}) {
job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
~~~
內嵌類型會引入命字沖突,但是解決沖突的方法也很簡單。首先,一個名為`X`的字段或方法可以將其它同名的類型隱藏在更深層的嵌套之中。假設`log.Logger`中也包含一個名為`Command`字段或方法,那么可以用`Job`的`Command`字段對其訪問進行封裝。
其次,同名沖突出現在同一嵌套層里通常是錯誤的;如果結構體`Job`本來已經包含了一個名為`log.Logger`的字段或方法,再繼續內嵌`log.Logger`就是不對的。但假設這個重復的名字并沒有在定義之外的地方被使用到,就不會造成什么問題。這個限定為在外部進行類型嵌入修改提供了保護;如果新加入的字段和某個內部類型的字段有命名沖突,但該字段名沒有被訪問過,那么就不會引起任何問題。