Interfaces
在Go语言中,一个接口也是一个自定义的类型,它指定了一个或者多个方法签名。接口是完全抽像的,所以不可能会实例化一个接口。但是,可以创建一个变量,指定它的类型为一个某一个接口——这样就可以赋值任何满足于这个接口的类型给这个变量。
interface{}类型是一个接口类型,它表示空的方法集。每一个值都满足interface{}接口,而不管这个值是否有方法——毕竟,如果一个值有方法,它的方法集就包括空的方法集以及它实际的方法。这也就是为什么interface{}类型可以用于任何值。我们不能直接在以interface{}方式传递的值上调用方法(即使它有这些方法),因为空接口是没有方法的。所以,一般都是传递值的实际类型或者包含方法的接口。当然如果我们使用interface{}类型的值,我们也可以通过断言和type switch, 或者使用内省。
以下是一个简单的接口。
type Exchanger interface {
Exchange()
}
Exchanger接口指定了单个的方法Exchange(), 它不接受任何参数,也没有返回值。我们按照Go对接口命名的约定,接口的名字以er结尾。只有一个方法的接口非常普遍——比如,标准库中的io.Reader和io.Writer接口都是指定了一个方法。请注意,一个接口实际上就是指定的API(应用程序接口), 即零个或者多个方法,而没有这些方法的实现。
一个非空的接口本身是没有什么作用的,为了使它有用,我们必须创建自定义类型,这些自定义类型的方法满足于接口的要求。以下是两个这样的类型
type StringPair struct{ first, second string }
func (pair *StringPair) Exchange() {
pair.first, pair.second = pair.second, pair.first
}
type Point [2]int
func (point *Point) Exchange() { point[0], point[1] = point[1], point[0] }
自定义的StringPair和Point类型是完全不同的两个类型,但它们都提供了Exchange()方法,都满足于Exchanger接口。这意味着我们可以创建StringPair和Point的值,然后将它们传递给接收Exchanger作为参数的函数。
请注意,虽然StringPair和Point类型都实现了Exchanger接口,但是你确没有看到"implements" 和 "inherits"语句。仅仅是StringPair和Point类型提供了这个接口指定的方法。这就可以让Go知道,它们满足这个接口。
这个方法的接收者都是指针,所以我们可以在方法调用时改变指针指向的值。
虽然Go可以足够聪明的打印自定义类型,通常我们还想要控制自定义类型的字符串表示。这可以很容易的做到,只需要这个自定义类型添加一个String() string方法,让它实现fmt.Stringer接口。
func (pair StringPair) String() string {
return fmt.Sprintf("%q+%q", pair.first, pair.second)
}
这个方法返回一个由双引号包的字符串。当这个方法被定义了,Go fmt package中的打印方法,将使用这个方法打印StringPair的值和*StringPairs.
以下的代码,创建了多个Exchanger的值,以及调用Exchange()方法,并且调用了一个自定义的函数exchangeThese(),它接受一个Exchanger的值。
jekyll := StringPair{"Henry", "Jekyll"}
hyde := StringPair{"Edward", "Hyde"}
point := Point{5, -3}
fmt.Println("Before: ", jekyll, hyde, point)
jekyll.Exchange() // Treated as: (&jekyll).Exchange()
hyde.Exchange() // Treated as: (&hyde).Exchange()
point.Exchange() // Treated as: (&point).Exchange()
fmt.Println("After #1:", jekyll, hyde, point)
exchangeThese(&jekyll, &hyde, &point)
fmt.Println("After #2:", jekyll, hyde, point)
/*
Before: "Henry"+"Jekyll" "Edward"+"Hyde" [5 -3]
After #1: "Jekyll"+"Henry" "Hyde"+"Edward" [-3 5]
After #2: "Henry"+"Jekyll" "Edward"+"Hyde" [5 -3]
*/
所有的变量都是以值的方式创建,而Exchange()方法的接收者是一个指针类型。这不是什么问题,如我们之前提到过的,当我们调用的方法在一个指针上时,Go会足够聪明的传递这个值的地址——前提是这个值必须是可寻址的。所以在这代码里,jekyll.Exchange()会自动的作为(&jekyll).Exchange()来处理。
在调用exchangeThese()函数时,我们必须明确的传递这个值的地址。假如我们传递StringPair类型的值hyde给这个函数,Go编译器会注意到StringPairs不符合Exchange接口——因为这里没有接受者为StringPairs的Exchange()方法(是*StringPairs类型)——停止编译并且报告问题。可是,如果我们传递一个*StringPair(e.g., &hyde),编译器就可以编译成功。这是因为在这有一个Exchange()方法,它接收一个*StringPair接收器,这就意味者*StringPair实现了Exchange接口。
以下是exchangeThese()函数
func exchangeThese(exchangers ...Exchanger) {
for _, exchanger := range exchangers {
exchanger.Exchange()
}
}
这个函数不知道也不关心我们传递它的*StringPairs和*Point; 它只要求传递的是Exchanger接口——这个要求会由编译器强制执行,因此这里使用鸭子类型是类型安全的(type-safe).
除了满足于我们自己自定义的接口,我们也可以满足标准库中的接口或者其它第三方的,我们可以看到,在定义StringPair.String()方法满足fmt.Stringer接口。另一个例子是io.Reader接口指明了单个方法,它的签名是Read([]byte) (int, error). 当这个方法被调用时,它将调用这个方法的值的数据写入到[]byte。这种写是破坏性的,即,每一个被写入到[]byte后的字节都会从调用的值中删除。
func (pair *StringPair) Read(data []byte) (n int, err error) {
if pair.first == "" && pair.second == "" {
return 0, io.EOF
}
if pair.first != "" {
n = copy(data, pair.first)
pair.first = pair.first[n:]
}
if n < len(data) && pair.second != "" {
m := copy(data[n:], pair.second)
pair.second = pair.second[m:]
n += m
}
return n, nil
}
通过实现这个方法,我们使得StringPair类型实现了io.Reader接口,所以StringPairs(或者严格来说,是*StringPairs,因为这些方法要求是的指针接收者)是Exchanger, fmt.Stringer 和 io.Readers——而不需要说*StringPair "implements"Exchanger或者任何其它的接口。当然,如果我们要实现其它接口,只需要添加更多的方法。
这个方法使用了内置的copy()函数。 copy()函数可以用于将相同类型的slice中的元素复制到另一个slice中——但在这里我们使用的是它的另一种形式,将一个字符串中的字节复制到[]byte中。copy()函数不会复制超过目标[]byte空间的字节,同时这个函数返回复制的字节数量。这个自定义的StringPair.Read()方法将第一个字符串的字节(任何被写入字节会被删除)写入到目标[]byte中。如果两个字符串都为空,则返回0和io.EOF.
在这里,我们必须使用指针接收器,因为这个Read()方法会修改调用它的值。通常我们更愿意使用指针接收器,而不是值接收器,除非是对于小类型的值。
以下是Read()方法的使用。
const size = 16
robert := &StringPair{"Robert L.", "Stevenson"}
david := StringPair{"David", "Balfour"}
for _, reader := range []io.Reader{robert, &david} {
raw, err := ToBytes(reader, size)
if err != nil {
fmt.Println(err)
}
fmt.Printf("%q\n", raw)
}
/*
"Robert L.Stevens"
"DavidBalfour"
*/
这段代码创建了两个io.Readers. 由于我们实现的是接收器为指针的StringPair.Read()方法,所以只有*StringPairs满足于io.Reader()接口,而不是StringPair的值。对于第一个StringPair,我们创建了一个StringPair的值,并且设置变量robert指向到这个值。我们设置变量david为StringPair的值——所以必须在[]io.Reader slice中使用它的地址。
一旦变量设置完成,我们可以遍历它们,对于slice中的每一个元素,我们使用ToByte()函数,将它们的数据复制到一个[]byte中,然后以双引号的格式打印出原始字节。
ToBytes()函数接受一个io.Reader(i.e.,可以是任何值,只要提供Read([]byte) (int, error), 比如*os.File),和大小限制,并且返回一个[]byte(我含读取到的数据)和error.
func ToBytes(reader io.Reader, size int) ([]byte, error) {
data := make([]byte, size)
n, err := reader.Read(data)
if err != nil {
return data, err
}
return data[:n], nil // Slice off any unused bytes
}
这跟exchangeThese()函数一样,它个方法不知道或者关心传入值的类型——只要它是io.Reader.
如果读取成功,data 会减小为它实际读取到字节的大小。如果我们不这样做并且size大于读取到的字节,那么在读取到的字节后面填充0x00. 比如,david为""DavidBalfour\x00\x00\x00\x00".
需要注意的是,在接口和任意符合这个接口的类型之间,没有明确的联系——我们没有对自定义类型使用"inherits"或"extends"或"implements"实现一个接口。只是让这个类型提供了接口要求的方法。这让Go语言变得非常的灵活——我们可以随时添加一个新的接口,类型,和方法,而不会破坏原有的继承树。
Interface Embedding
Go的接口以及struct都支持嵌入。接口可以嵌入其它的接口,这样被嵌入的接口的方法签名也在新的接口中有效。如下所示。
type LowerCaser interface {
LowerCase()
}
type UpperCaser interface {
UpperCase()
}
type LowerUpperCaser interface {
LowerCaser // As if we had written LowerCase()
UpperCaser // As if we had written UpperCase()
}
LowerCaser接口指定了单个方法LowerCase(), 这个方法没有任何参数和返回值。UpperCaser接口相类似。LowerUpperCaser接口嵌入了这两个接口。这意味着要实现LowerUpperCaser接口,必须提供LowerCase()和UpperCase()方法。
这个小的例子看不出嵌入的优势。可是,如果我们给前两个接口添加额外的方法(比如,LowerCaseSpecial()和UpperCaseSpecial()),LowerUpperCase接口会自动的包含这两个方法.
type FixCaser interface {
FixCase()
}
type ChangeCaser interface {
LowerUpperCaser // As if we had written LowerCase(); UpperCase()
FixCaser // As if we had written FixCase()
}
我们增加了两个接口,所以现在我们有了一种嵌入接口的层级结构。如表6.2所示。
单独使用接口是没有用的,我们还需要实现接口的具体类型。
func (part *Part) FixCase() { part.Name = fixCase(part.Name) }
我们在之前讲到过Part类型(原书260页). 在这我们给它增加了一个方法FixCase(), 这个方法作用于Part的Name字段,这跟之前的LowerCase()和UpperCase()方法是一样的。所有的大小写变化的方法都的接收者都是一个指针,因为它们会改变调用这些方法的值。LowerCase()和Uppercase()方法都是通过标准库中的函数实现,FixCase()方法依赖于自定义的fixCase()函数——依赖于函数做具体工作的方法,这种模式在 Go语言中很常见。
Part.String()方法满足标准库中的fmt.Stringer接口。所以打印Part(or *Part)都将使用这个函数返回的字符串。
func fixCase(s string) string {
var chars []rune
upper := true
for _, char := range s {
if upper {
char = unicode.ToUpper(char)
} else {
char = unicode.ToLower(char)
}
chars = append(chars, char)
upper = unicode.IsSpace(char) || unicode.Is(unicode.Hyphen, char)
}
return string(chars)
}
这个函数对给定的字符串,除了第一个字符,空格或连字符之后的第一个字符大写外,其它都小写,并返回这个新的字符串。比如,给定的字符串为"lobelia sackville-baggins", 这个函数将返回"Lobelia Sackville-Baggins".
自然的,我们可以使得任意的自定义类型满足部分或者全部的大小接口。
func (pair *StringPair) UpperCase() {
pair.first = strings.ToUpper(pair.first)
pair.second = strings.ToUpper(pair.second)
}
func (pair *StringPair) FixCase() {
pair.first = fixCase(pair.first)
pair.second = fixCase(pair.second)
}
这里,我们给上一节的StringPair类型添加了两个方法,使它满足了LowerCaser, UpperCaser和FixCaser接口,顺便说一下,我们没有展示StirngPair.LowerCase()方法的代码,因为它的结构跟UpperCase()方法一样。
*Part和*StringPair类型满足所有的caser接口,包括ChangeCaser接口。它们同样满足于标准库中的fmt.Stringer接口。同时,*StringPair类型还满足了Exchanger接口和标准库中的io.Reader接口。
我们没有义务去实现每一个接口——比如,如果我们不实现StringPair.FixCase()方法,*StringPair类型仅满足LowerCaser, UpperCaser, LowerUpperCaser, Exchanger, fmt.Stringer和io.Reader接口。
以下的代码是如何使用这些值和方法
toastRack := Part{8427, "TOAST RACK"}
toastRack.LowerCase()
lobelia := StringPair{"LOBELIA", "SACKVILLE-BAGGINS"}
lobelia.FixCase()
fmt.Println(toastRack, lobelia)
//«8427 "toast rack"» "Lobelia"+"Sackville-Baggins"
这些方法的调用和起到的作用跟我们预期的是一样的,但如果我们一群这样的值和想要调用它们的一个方法,会发生什么?以下是一个不好的方法。
for _, x := range []interface{}{&toastRack, &lobelia} { // UNSAFE!
x.(LowerUpperCaser).UpperCase() // Unchecked type assertion
}
在这里我们必须使用指针,因为所以的caser方法都会修改原调用它们的值。
在这段代码中使用的方法有两个缺陷. 小一点的缺陷是我们将空接口转换为LowerUpperCase接口(更糟的情况是转换为ChangeCaser接口,因为它更一般化),对于这个小的缺陷,我们应该使用刚好满足使用的专用接口——这个情况下,应该使用UpperCaser接口。这种方式主要的缺陷是我们根本就不能使用unchecked type assertion,因为unchecked会导致panic.
for _, x := range []interface{}{&toastRack, &lobelia} {
if x, ok := x.(LowerCaser); ok { // shadow variable
x.LowerCase()
}
这段代码使用了一个安全的方法和做这项工具最具体的接口。问题在于我们使用了一般的interface{}, 而不是特定的类型或者满足的特定的接口。
for _, x := range []FixCaser{&toastRack, &lobelia} { // Ideal ✓
x.FixCase()
}
这段代码是最好的方法:我们指定slice的类型为FixCasers而不是interface{}, 这样就不需要使用到断言——这个最具体的接口类型符合我们的需求——并且将所有的类型检查交给编译器。
接口灵活性的另一个方面是,我们可以在实际之后创建。比如,假如我们创建了一些自定义类型,其中有的自定义类型有IsValid() bool方法。如果之后,我们发现我们有一个函数接受我们自定义的类型,并且我们当空上自定义类型有IsValid()方法时,调用IsValid()。可以如下所示:
type IsValider interface {
IsValid() bool
}
首先我们创建一个接口,用于指定我们用于检查的方法。
if thing, ok := x.(IsValider); ok {
if !thing.IsValid() {
reportInvalid(thing)
} else {
// ... process valid thing ...
}
}
通过这个接口,我们现在可以检查任何定义的值,它们是否有IsValid() bool方法,如果有,我们就调用这个方法。
接口提供了一个非常强大的抽像机制,允许我们指定方法集,我们就可以使用接口作为函数的参数或者方法的参数。这样我们只需要关心一个值能做什么,而不是这个值的类型是什么。