前言

本文翻译自:How to use interfaces in Go,很经典的文章,推荐查看原文。


在我开始Go语言编程前,我的大部分工作都是使用Python完成的。作为一名Python程序员,我发现学习使用Go语言的接口是非常困难的。也就是说,基础很简单,我知道怎么使用标准库里接口,但是如何设计我自己的接口需要做一些练习。在这篇文章中,我会讨论Go的类型系统,并解释如何有效地使用接口。

接口简介

什么接口?接口是两件事:它是一组方法,也是一种类型。让我们先关注接口的方法集方面。

通常,我们会通过一些人为的例子来介绍接口。 让我们来看一个人为的例子,编写一些定义Animal数据类型的应用程序,因为这是一个完全现实的情况,并且一直在发生。Animal类型将会是一个接口,我们将Animal定义为任何可说话的东西,这是Go语言类型系统中的核心概念; 我们不是根据我们的类型可以保存什么样的数据来设计我们的抽象,而是根据我们的类型可以执行的操作来设计我们的抽象。

我们从定义Animal接口开始:

type Animal interface {
    Speak() string
}

非常简单:我们将Animal定义拥有一个Speak方法的任意类型。Speak方法没有参数且返回值是一个字符串。任意定义此方法的类型都被称为满足了Animal接口。Go中没有implements关键字;类型是否满足接口是自动确定的。让我们创建几个满足这个接口的类型。

type Dog struct {
}

func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct {
}

func (c Cat) Speak() string {
    return "Meow!"
}

type Llama struct {
}

func (l Llama) Speak() string {
    return "?????"
}

type JavaProgrammer struct {
}

func (j JavaProgrammer) Speak() string {
    return "Design patterns!"
}

我们现在有四个不同类型的Animal:dog,cat,llama,Java programmer。在我们的main()方法中,,我们可以创建一个Animals切片,并将每种类型的一个放到切片内,然后查看每种animal说了什么。让我们现在这样做:

func main() {
    animals := []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}}
    for _, animal := range animals {
        fmt.Println(animal.Speak())
    }
}

你可以在此查看并且运行此示例:http://play.golang.org/p/yGTd4MtgD5

太好了,现在你知道怎么使用接口了,我就不用多说了,对吧?嗯,不,不是真的。让我们看几个对于Go萌新不是非常明显的事情。

interface{} 类型

interface{}类型,即空接口。是很多混乱的根源。interface{}类型是没有方法的接口。由于没有implements关键字,所有的类型实现都至少0个方法,并且满足接口是自动完成的。所有的类型都满足空接口。这意味着如果你写一个函数参数为interface{},你可以向这个函数传入任意值。所以,这个函数:

func DoSomething(v interface{}) {
   // ...
}

将会接受任意参数。

令人困惑的地方在于:在DoSomething函数内部,v的类型是什么?Go萌新会被引导相信“v是任意类型”,但那是错误的。v不是任意类型;它是interface{}类型。等等,什么?当我们向DoSomething函数传入一个值时,Go 运行时会将类型转换(如果有必要),并且将值转为interface{}值。所有的值在运行时都只有一种类型。而v的静态类型就是interface{}.

这应该让你想知道:好的,所以如果正在发生转换,实际传递给接受 interface{} 值的函数的内容(或者,实际存储在 []Animal 切片中的内容)? 一个接口值由两个数据字构成; 一个词用于指向该值的基础类型的方法表,另一个词用于指向该值所保存的实际数据。 我不想无休止地喋喋不休。 如果您了解接口值是两个字宽并且它包含指向底层数据的指针,那么通常足以避免常见的陷阱。 如果你有兴趣了解更多关于接口的实现,我认为 Russ Cox 对接口的描述非常非常有帮助。

在我们之前的示例中,当我们构造一个Animal值的切片时,我们不必说像 Animal(Dog{}) 这样繁琐的东西来将Dog类型的值放入Animal值的切片中,因为转换是自动处理的。 在Animal切片中,每个元素都是Animal类型,但我们不同的值具有不同的底层类型。

那么……这有什么关系呢? 好吧,了解接口在内存中的表示方式会使一些可能令人困惑的事情变得非常明显。 例如,一旦你了解了接口在内存中的表示方式,“我可以将 []T 转换为 []interface{} 吗”这个问题很容易回答。 下面是一些损坏的代码示例,代表了对 interface{} 类型的常见误解

package main

import (
    "fmt"
)

func PrintAll(vals []interface{}) {
    for _, val := range vals {
        fmt.Println(val)
    }
}

func main() {
    names := []string{"stanley", "david", "oscar"}
    PrintAll(names)
}

在这里运行:http://play.golang.org/p/4DuBoi2hJU

通过运行,你可以看到我们遇到了如下错误: cannot use names (type []string) as type []interface {} in function argument. If we want to actually make that work, we would have to convert the []string to an []interface{}:

package main

import (
    "fmt"
)

func PrintAll(vals []interface{}) {
    for _, val := range vals {
        fmt.Println(val)
    }
}

func main() {
    names := []string{"stanley", "david", "oscar"}
    vals := make([]interface{}, len(names))
    for i, v := range names {
        vals[i] = v
    }
    PrintAll(vals)
}

在这里运行:http://play.golang.org/p/Dhg1YS6BJS

这很丑陋,但是c’est la vie(这就是生活)。并非一切都是完美的。(实际上,这种情况并不经常出现,因为[]interface{}的用处并没有你预期的那么大)

指针和接口

接口另一个微妙之处是接口定义没有规定实现者应该使用指针接收器还是值接收器实现接口。当你得到一个接口值,不能保证底层类型是不是指针。在前面示例中, 我们在值接收器上定义了所有方法,并将关联的值放入 Animal 切片中。 让我们改变一下,让 Cat 的 Speak() 方法接受一个指针接收器:

func (c *Cat) Speak() string {
    return "Meow!"
}

如果你改变了这个签名,并尝试完全按原样运行相同的程序 (http://play.golang.org/p/TvR758rfre),你将看到如下错误:

prog.go:40: cannot use Cat literal (type Cat) as type Animal in array element: Cat does not implement Animal (Speak method requires pointer receiver)

老实说,这个错误消息起初有点令人困惑。 它的意思不是 Animal 接口要求你将方法定义为指针接收器,而是你尝试将 Cat 结构转换为 Animal 接口值,但只有 *Cat 满足该接口。 你可以通过将 *Cat 指针传递给 Animal 切片而不是 Cat 值来修复此错误,方法是使用 new(Cat) 而不是 Cat{}(你也可以说 &Cat{},我只是更喜欢 new(Cat)):

animals := []Animal{Dog{}, new(Cat), Llama{}, JavaProgrammer{}}

现在我们的程序再次运行:http://play.golang.org/p/x5VwyExxBM

让我们朝相反的方向前进:让我们传入 *Dog 指针而不是 Dog 值,但这一次我们不会更改 Dog 类型的 Speak 方法的定义:

animals := []Animal{new(Dog), new(Cat), Llama{}, JavaProgrammer{}}

这也有效(http://play.golang.org/p/UZ618qbPkj),但要认识到一个细微的区别:我们不需要更改 Speak 方法的接收器的类型。 这是有效的,因为指针类型可以访问其关联值类型的方法,但反之则不行。 也就是说,*Dog 值可以使用 Dog 上定义的 Speak 方法,但正如我们之前看到的,Cat 值不能访问 *Cat 上定义的 Speak 方法。

这可能听起来很神秘,但当你记住以下内容时,它就很有意义了:Go 中的所有内容都是按值传递的。 每次调用函数时,都会复制传递给它的数据。 对于具有值接收器的方法,在调用该方法时会复制该值。 当你理解以下签名的方法时,这一点会更加明显:

func (t T)MyMethod(s string) {
    // ...
}

是 func(T, string) 类型的函数; 方法接收器像任何其他参数一样按值传递给函数。

在值类型(例如,func (d Dog) Speak() { … })上定义的方法内部对接收者所做的任何更改都不会被调用者看到,因为调用者正在确定一个完全独立的 Dog 值。 既然一切都是按值传递的,那么为什么 *Cat 方法不能被 Cat 值使用应该是显而易见的; 任何一个 Cat 值都可以有任意数量的 *Cat 指针指向它。 如果我们尝试通过使用 Cat 值来调用 *Cat 方法,我们就不会以 *Cat 指针开头。 相反,如果我们有一个 Dog 类型的方法,并且我们有一个 *Dog 指针,那么我们在调用这个方法时就确切地知道要使用哪个 Dog 值,因为 *Dog 指针正好指向一个 Dog 值; Go 运行时将在必要时取消引用指向其关联 Dog 值的指针。 也就是说,给定一个 *Dog 值 d 和 Dog 类型的 Speak 方法,我们可以说 d.Speak(); 我们不需要像在其他语言中那样说 d->Speak() 之类的东西。

总结

我希望,在阅读完这篇文章后,你会觉得在 Go 中使用接口会更舒服。 请记住以下内容:

  • 通过考虑数据类型之间共有的功能而不是数据类型之间共有的字段来创建抽象
  • interface{} 值不是任何类型; 它是 interface{} 类型
  • 接口是两个字节; 它们看起来像(类型,值)
  • 接受 interface{} 值比返回 interface{} 值更好
  • 指针类型可以调用其关联值类型的方法,反之则不行
  • 一切都是按值传递的,即使是方法的接收者
  • 接口值不是严格意义上的指针或不是指针,它只是一个接口
  • 如果您需要完全覆盖方法内的值,请使用 * 运算符手动取消引用指针

好的,我认为这总结了我个人认为令人困惑的接口的所有内容。Happy coding :)

Update:

type eface struct { // 16 字节
	_type *_type
	data  unsafe.Pointer
}

interface{} 底层数据结构是两个指针,存放原数据类型和数据。