首页 [译]如何在Go中使用接口/How to use interfaces in Go
文章
取消

[译]如何在Go中使用接口/How to use interfaces in Go

refer: How to use interfaces in Go

在我开始用Go编程前,我完成我大部分工作都是用Python。作为一个Python工程师,我发现学习使用Go的接口十分困难。就是基础比较简单,并且我知道标准库如何使用,但是需要一些练习在我知道如何设计我自己的接口。在这篇文章里,我将会讨论Go的类型系统中努力解释如何高效使用接口。

接口介绍

那么什么是接口?接口是两个东西:它是一系列的方法,而且同时也是一个类型。让我们关注接口作为方法集的这个方面。 通常,我们介绍接口用一些专门例子。让我们用专门的例子写一些定义有 Animal 数据类型的程序,因为它总是随时发生的实际情况。这个 Animal 类型将会有一个接口,我们会定义一个 Animal 作为所有可以讲解的例子。这个是一个核心组件在Go的类型系统里;而不是根据我们的类型可以保存什么样的数据来设计我们的抽象,我们根据类型可以执行的操作来设计我们的抽象。 我们从定义我们 Animal 接口开始:

1
2
3
type Animail interface {
    Speak() string
}

相当简单:我们定义一个 Animal 作为所有类型有的方法称为 Speak。这个 Speak 方法没有任何参数并且返回一个字符串。所有类型可以定义这个方法的都符合 Animal 接口。这个没有 implements 关键字在Go里;是否一个类型满足一个接口是自动决定的?让我们创建一系列类型来满足这个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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!"
}

我们现在有四种不同类型的动物:狗,猫,骆驼和一个Java程序员。在我们的 main() 函数里,我们可以创建一个动物切片,并且把动物类型放进去,我们看看每个动物说什么,让我们行动起来:

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

你可以查看并且运行这个例子:https://play.golang.org/p/yGTd4MtgD5 很好,现在你知道如何使用接口了,我也不需要讲更多的东西了,对吧?好吧,不,不全是。让我们看看一些对于新手来说不是显而易见的东西。

类型 interface{}

interface{} 类型,一个空接口,是很多困惑的源头。这个 interface{} 类型是一个没有方法的接口。自从它不需要 implements 关键字,所有类型实现至少零个方法,并且满足一个接口是自动完成的,所有类型都满足这个空接口。也就意味着如果你写一个方法用到了 interface{} 值作为参数,你可以使用所有没有值的方法。所以,函数类型这样:

1
2
3
func DoSomething(v interface{}) {
    // ...
}

我们是否可以接受任何参数? 这个让我们感到疑惑:在 DoSomething 方法里,这个是什么类型?初学者觉得 v 是所有类型。但是这个是错误的。 v 不是任何类型;它是 interface{} 类型。等下,什么?当我们传递一个值到 DoSomething 函数里,这个Go运行时将会进行一个类型转换(如果必要的话)。然后把值转换成一个 interface{} 值。所有的值在运行时有一个确定的值, 并且 v 的一个静态类型就是 interface{}。 这些让你想知道:好,如果转换发生,什么被实际传入函数当使用一个 interface{} 。(或者,什么被实际存储在 []Animal 类型)?一个接口值是被两个字的数据构造;一个用于指向该值的基础类型的方法表,另一个用于该值所持有的实际数据,这些足够用于避免通常普通的陷阱。如果你对怎么实现接口感到好奇,我认为Russ Cox’s description of interfaces是非常有用的。 在我们之前的例子,我们构造了一个 Animail 的切片,我们说一些类似于 Animal(Dog{}) 繁重的东西放类型 DogAnimal 切片中,因为这个转化对我们来说是自动的。在 Animal 切片中,每个元素都是 Animal 类型,但是不同的值有不同的基础类型。 所以。。。问题是什么呢?明白接口如何在内存中表示造成了一些潜在的困惑是很明显的。一旦你明白了接口在内存中如何表示,例如问题(can I convert a []T to an []interface{})[https://go.dev/doc/faq#convert_slice_of_interface]是非常容易回答。以下是一段代码,代表了对 interface{} 的误解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import (
    "fmt"
)
func PrintAll(vals []interfaces{}) {
    for _, val := range vals {
        fmt.Println(val)
    }
}

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

通过运行以上代码,你可以看到遇到以下错误: cannot use names (type []string) as type []interface {} in function argument. 如果我们想要让他运行。我们必须要转换 []string[]interfaces{}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
    "fmt"
)
func PrintAll(vals []interfaces{}) {
    for _, val := range vals {
        fmt.Println(val)
    }
}
func main() {
    names := []string {"stanly", "david", "oscar"}
    vals := make([]interface{}, len(names))
    for _, val := range names {
        vals[i] = v
    }
    PrintAll(vals)
}

这个代码十分丑陋,但这就是现实。不是所有的东西都是完美的。(实际上,这种情况并不经常出现,因为 []interface{} 的用处没有您最初预期的那么大)。

指针和接口

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

1
2
3
func (c *Cat) Speak() {
    return "Meow!"
}

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

1
2
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) ):

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

现在我们可以正常运行了:http://play.golang.org/p/x5VwyExxBM 让我们转换一个方向:让我们传递一个 *Dog 指针替换值 Dog,但是我们不改变 Dog 类型的 Speak 方法。

1
animal := []Animals { new(Dog), new(Cat), Llama{}, JavaProgrammer{} }

这个是也是可工作的(http://play.golang.org/p/UZ618qbPkj),但是意识到了些许的不同:我们不需要去改变类型在 Speak 这个接收方法中。这个可以正常运行是因为执行可以访问其关联值的方法,但是并不是反之依然,也就是说,*Dog 值可以使用 Dog 上定义的 Speak 方法,但正如我们之前看到的,Cat 值不能访问 *Cat 上定义的 Speak 方法。 这听起来可能很神秘,但当您记住以下内容时,它就很有意义:Go 中的所有内容都是按值传递的。 每次调用函数时,都会复制传递给它的数据。 对于具有值接收器的方法,在调用该方法时会复制该值。 当您了解以下签名的方法时,这一点会更加明显:

1
2
3
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 值的指针。也就是说,给定一个 *DogdDog 类型的 Speak 方法,我们可以说 d.Speak();我们不需要像在其他语言中那样说 d->Speak() 之类的东西。

真实世界:从 Twitter API 中获取正确的时间戳

Twitter API 使用以下格式的字符串表示时间戳:

1
"Thu May 31 00:00:01 +0000 2012"

当然,时间戳可以在 JSON 文档中以多种方式表示,因为时间戳不是 JSON 规范的一部分。 为简洁起见,我不会将一条推文的整个 JSON 表示形式放入其中,但让我们看一下如何通过 encoding/json 处理 created_at 字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

// start with a string representation of our JSON data
var input = `
{
    "created_at": "Thu May 31 00:00:01 +0000 2012"
}
`

func main() {
    // our target will be of type map[string]interface{}, which is a
    // pretty generic type that will give us a hashtable whose keys
    // are strings, and whose values are of type interface{}
    var val map[string]interface{}

    if err := json.Unmarshal([]byte(input), &val); err != nil {
        panic(err)
    }

    fmt.Println(val)
    for k, v := range val {
        fmt.Println(k, reflect.TypeOf(v))
    }
}

真实世界接口:从HTTP请求获取一个对象

最后,让我们看看如何设计一个接口来解决一个常见的 Web 开发问题:我们希望将 HTTP 请求的主体解析为一些对象数据。 起初,这不是一个很明显的接口来定义。 我们可能会尝试说我们将从这样的 HTTP 请求中获取资源:

1
GetEntity(*http.Request) (interface{}, error)

因为 interface{} 可以有任何底层类型,所以我们可以解析我们的请求并返回我们想要的任何内容。 事实证明这是一个非常糟糕的策略,原因是我们最终在 GetEntity 函数中添加了太多逻辑,现在需要为每个新类型修改 GetEntity 函数,并且我们需要使用类型断言来 对返回的 interface{} 值做任何有用的事情。 在实践中,返回 interface{} 值的函数往往很烦人,根据经验,您只需记住通常将 interface{} 值作为参数比返回 interface{} 更好 价值。 (Postel 定律,适用于接口)

我们也可能想编写一些特定于类型的函数,如下所示:

1
GetUser(*http.Request) (User, error)

这也被证明是非常不灵活的,因为现在我们对每种类型都有不同的函数,但没有理智的方法来概括它们。 相反,我们真正想做的是更像这样的事情:

1
2
3
4
5
6
type Entity interface {
    UnmarshalHTTP(*http.Request) error
}
func GetEntity(r *http.Request, v Entity) error {
    return v.UnmarshalHTTP(r)
}

GetEntity 函数采用保证具有 UnmarshalHTTP 方法的接口值。 为了利用这一点,我们将在 User 对象上定义一些方法,允许 User 描述它如何从 HTTP 请求中获取自身:

1
2
3
func (u *User) UnmarshalHTTP(r *http.Request) error {
   // ...
}

在您的应用程序代码中,您将声明一个 User 类型的 var,然后将指向该函数的指针传递给 GetEntity

1
2
3
4
var u User
if err := GetEntity(req, &u); err != nil {
    // ...
}

这与解压 JSON 数据的方式非常相似。这种类型的事情始终如一且安全地工作,因为语句 var u User 将自动将 User 结构归零。 Go 不像其他一些语言那样声明和初始化是分开发生的,通过声明一个值而不初始化它,你可以创建一个微妙的陷阱,你可能会访问一段垃圾数据;声明值时,运行时会将适当的内存空间归零以保存该值。即使我们的 UnmarshalHTTP 方法未能利用某些字段,这些字段也将包含有效的零数据而不是垃圾。

如果你是一名 Python 程序员,这对你来说应该很奇怪,因为它基本上是我们通常在 Python 中所做的事情。这种形式变得如此方便的原因是现在我们可以定义任意数量的类型,每个类型都负责从 http 请求中解包。现在由实体定义来决定如何表示它们。然后,我们可以围绕 Entity 类型进行构建,以创建诸如通用 HTTP 处理程序之类的东西。

结束

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

通过考虑数据类型之间共有的功能而不是数据类型之间共有的字段来创建抽象

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

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

本文由作者按照 CC BY-NC-SA 4.0 进行授权

[译]Go Slices:使用以及原理/Go Slices: usage and internals

vim插件(1)