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