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`就是不对的。但假设这个重复的名字并没有在定义之外的地方被使用到,就不会造成什么问题。这个限定为在外部进行类型嵌入修改提供了保护;如果新加入的字段和某个内部类型的字段有命名冲突,但该字段名没有被访问过,那么就不会引起任何问题。