高效的 Go 编程 Effective Go - 内嵌
免费 - Sunrise - - 浏览量: 0 - 文章来源
Go并不提供典型的,类型驱动的子类化概念,但通过将类型内嵌到结构体或接口中, 它就能“借鉴”部分实现。
接口内嵌非常简单。我们之前提到过 io.Reader
和 io.Writer
接口,这里是它们的定义。
type Reader interface { //定义读取的接口类型
Read(p []byte) (n int, err error) //定义函数 传入[]byte类型 返回一个整型和err
}
type Writer interface { //定义写入的接口类型
Write(p []byte) (n int, err error) //定义函数 传入[]byte类型 返回一个整型和err
}
io
包也导出了一些其它接口,以此来阐明对象所需实现的方法。 例如 io.ReadWriter
就是个包含 Read
和 Write
的接口。我们可以通过显示地列出这两个方法来指明 io.ReadWriter
, 但通过将这两个接口内嵌到新的接口中显然更容易且更具启发性,就像这样:
// ReadWriter 接口结合了 Reader 接口 和 Writer 接口
type ReadWriter interface {
Reader
Writer
}
正如它看起来那样:ReadWriter
能够做任何 Reader
和 Writer
可以做到的事情,它是内嵌接口的联合体 (它们必须是不相交的方法集)。只有接口能被嵌入到接口中。
同样的基本想法可以应用在结构体中,但其意义更加深远。bufio
包中有 bufio.Reader
和 bufio.Writer
这两个结构体类型, 它们每一个都实现了与 io
包中相同意义的接口。此外,bufio
还通过结合 reader/writer
并将其内嵌到结构体中,实现了带缓冲的 reader/writer
:它列出了结构体中的类型,但并未给予它们字段名。
// ReadWriter 存储了指向 Reader 和 Writer 的指针。
// 它实现了 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
方法被调用时, 它与之前写的转发方法具有同样的效果;接收者是 ReadWriter
的 reader
字段,而非 ReadWriter
本身。
内嵌同样可以提供便利。这个例子展示了一个内嵌字段和一个常规的命名字段。
type Job struct {
Command string
*log.Logger
}
Job
类型现在有了 Log
、Logf
和 *log.Logger
的其它方法。我们当然可以为 Logger
提供一个字段名,但完全不必这么做。现在,一旦初始化后,我们就能记录 Job
了:
job.Println("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
类型的变量 job
的 *log.Logger
, 可以直接写作 job.Logger
。若我们想精炼 Logger
的方法时, 这会非常有用。
func (job *Job) Printf(format string, args ...interface{}) {
job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
内嵌类型会引入命名冲突的问题,但解决规则却很简单。首先,字段或方法 X
会隐藏该类型中更深层嵌套的其它项 X
。若 log.Logger
包含一个名为 Command
的字段或方法,Job
的 Command
字段会覆盖它。
其次,若相同的嵌套层级上出现同名冲突,通常会产生一个错误。若 Job
结构体中包含名为 Logger
的字段或方法,再将 log.Logger
内嵌到其中的话就会产生错误。然而,若重名永远不会在该类型定义之外的程序中使用,那就不会出错。 这种限定能够在外部嵌套类型发生修改时提供某种保护。 因此,就算添加的字段与另一个子类型中的字段相冲突,只要这两个相同的字段永远不会被使用就没问题。