go的interface

原文为http://jordanorelli.com/post/32665860244/how-to-use-interfaces-in-go
翻译一篇不错的介绍go的interface

在我开始用go编程之前,我的大部分工作都是用python。作为一个python程序员,我发现在go中使用接口是极其困难的。基础很简单,我知道如何在标准库中使用接口,但是在我知道如何设计我自己的接口之前却需要一些练习。在这篇博文中,我将讨论在go的类型系统,解释如何有效的使用接口。

介绍接口

所以,什么是接口?一个接口有两个东西:一个是方法的集合,但是又是一个类型。让我们首先聚焦在接口的一部分,方法的集合上。
通常,我们用几个假设的例子来介绍接口。我们写一些假设的应用,定义动物的数据类型,因为这真的是一个经常发生的实际案例。这个animal类型会是一个接口,我们定义animal为可以说话的任何东西。这是在go的类型系统里一个核心的概念:不是根据我们的类型可以容纳什么样的数据来设计我们的抽象,而是根据类型可以执行的操作来设计抽象。

我们开始定义我们的animal接口:

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

我们定义一个animal作为任何可能的类型,并且有一个方法叫speak.这个方法没有参数,返回一个string。定义了这个方法的任何类型都可以说是满足了animal接口。在go里没有implements关键字;一个类型是否满足接口是自动检测的。我们创建几个满足这个接口的类型:

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!"
}

现在我们有4个不同的Animals类型:dog, cat , Llama和一个java程序员。在我们的main()方法里,我们可以创建一个Animals的slice,把上面的类型都放进去,看看它们说什么。

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

很好,现在你知道如何用接口了,我不需要再讨论它们了对吧?当然不是,我们来看几个对萌新来说不是很明显的事。

interface{}类型

nterface{}类型,空的接口,是很多困惑的源头。这个接口类型没有方法。因为没有implements关键字,所有的类型至少实现了一个空方法,自动满足了一个接口的需求,所有类型满足空接口。意味着如果你写一个方法用interface{}类型值作为参数,你可以用那个方法的任何值。所以,这个方法:

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

将会接受任何参数。
这里是它令人迷惑的地方:在DoSomething方法里面,v的类型是什么?萌新们会认为“v是任何类型”,但这是错误的,v不是其他类型;它是interface{}类型。等等,什么?当传递一个值进DoSomething方法时,go运行时会做一个类型转换(如果有必要),转换这个值为interface{}值。所有的值在运行时都有一个确切地类型,而v的一个静态类型就是interface{}。
这一定会令你感到困惑:好吧,如果一个转换正在发生,实际传递给方法的一个interface{}值是什么?(或者,实际存储在[]Animal值是什么)
一个接口的值包含两个数据字:一个字是用来指向值的底层类型的方法表,另一个是用来指向值所持有的实际数据。我不想无休止的去抱怨这个。如果你能理解一个接口值有两个字宽,并且包含一个指针指向底层数据,那么就足够避免一些常见的坑了。如果你想学习更多接口的实现,Russ Cox’s description of interfaces非常有用。
在接下来的例子中,当我们构造一个Animal的slice值时,我们并没有必要像Animal(Dog{})这样繁琐的把Dog类型的值放入Animal的slice中。因为转换已经为我们自动操作了。在animals slice里面,每一个元素都是Animal类型,但是不同的值有不同的底层类型。
所以,为什么会这样呢?好吧,理解接口在内存中的表示可以让一些可能令人困惑的事情更加明确。例如,一旦你明白了接口在内存中的表示,这个问题“can I convert a []T to an []interface{}”就很容易解答了。这里有一段错误的代码,典型的对interface{}类型的误解。

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

import (
"fmt"
)

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

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

运行下,可以看到我们碰到了下面的错误:cannot use names (type []string) as type []interface {} in function argument。如果我们确实要让它正常运行,我们得把[]string转换成[]interface{}:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
}

这确实相当丑陋,但是c’est la vie(这就是生活)。并不是每件事都是完美的。(事实上,这不经常发生,因为[]interface{}被证明比你最初预想的要少用的多)

指针和接口

接口的另一个微妙之处就是接口定义并没有规定实现者必须用指针接收或者用值接收。当你获得一个接口值,它不保证底层类型是不是一个指针。在我们接下来的例子里,在值接收器里定义了所有的方法,我们把相关的值放入Animal slice里。我们可以改变这个,让Cat的Speak()方法取得一个指针接收器:

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

如果你改变了这个签名,你再运行它,会看见如下错误:
prog.go:40: cannot use Cat literal (type Cat) as type Animal in array element:
Cat does not implement Animal (Speak mniethod requires pointer receiver)
说实话,刚开始这个错误会有点令人迷惑。它说的并不是接口Animal要求你要给你的方法一个指针接收器,而是你试图转换一个Cat结构体为一个Animal接口值,但是只有Cat满足这个接口。你可以修改这个bug,通过传递一个Cat指针给Animal slice而不是一个Cat值,用new(Cat)而不是Cat{}(你也可以用&Cat{},我比较喜欢用new(Cat)):

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

现在我们的程序又能执行了。
我们走相反的方向:传递一个*Dog指针而不是一个Dog值,但是这次我们不会改变Dog类型的speak方法定义:

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

这也有效,但是有一个细微的不同:我们不需要改变Speak方法接受者的类型。这个有效是因为一个指针类型可以访问其关联值类型的方法,但反过来却不行。Dog值可以使用Dog上定义的Speak方法,但是正如我们之前看到的,Cat值不能访问Cat上定义的Speak方法。

这可能听起来很难解,但是当你记住下面这句话就说的通了:在Go中所有东西都是按值传递的。每一次你调用一个方法,你就会传递它的一份复制的数据。对于带有值接收器的方法,当调用它时会复制该值。当你理解下面的签名方法时会略微明显一点:

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

这是函数func(T, string),方法接收器通过值传递到函数中,就像其他参数一样。

在值类型上定义的方法内对接收器进行的任何更改都不会被调用者看到,因为调用者的作用域是完全独立的Dog值。因为所有的都是按值传递,这应该很明显为什么Cat方法不能用于Cat值;任何一个Cat值都可能有多个Cat指针指向它。如果我们试图通过用一个Cat值调用Cat方法,我们将不会有一个Cat指针。相反,如果我们有一个Dog类型的方法,并且有一个Dog指针,当调用这个方法我们可以确切地知道用哪一个Dog值,因为这个Dog指针指向确切地一个Dog值;Go运行时将会在必要时取消引用指向其关联Dog值的指针。给一个*Dog值d和一个Dog类型的方法Speak,我们可以使用d.Speak();我们不需要像其他语言一样使用d->Speak()。

真实的世界:不用Twitter的API得到合适的时间戳

Twitter的API使用如下的一个字符串格式表示时间戳:
“Thu May 31 00:00:01 +0000 2012”
当然,在json文档中时间戳可以用很多种方式表示,因为时间戳并不是json规范的一部分。为了简单,我不会把整个Twitter的json描述放进来,但是我们可以来看看create_at字段将会被encoding/json如何使用:

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))
}
}

执行这段程序,看到如下输出:
map[created_at:Thu May 31 00:00:01 +0000 2012]
created_at string

我们可以看到我们取到了合适的key,但是像这样的字符串格式时间戳不是很有用。如果我们想比较时间戳看哪一个更早,或者给定值和当前时间看消耗了多少时间,用简单字符串就没多大用处了。

我们天真的尝试把这个解包成time.Time值,这是time提供的标准库描述。然后看看会得到什么错误。修改如下:

val map[string]time.Time
1
2
3
4

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

执行,我们会收到下面的错误:
parsing time “”Thu May 31 00:00:01 +0000 2012”” as “”2006-01-02T15:04:05Z07:00””:
cannot parse “Thu May 31 00:00:01 +0000 2012”” as “2006”

这是go处理time.Time值和字符串之间转换方式产生的令人迷惑的错误信息。简而言之,意思是我们提供的字符串描述,不符合标准的时间格式(因为Twiiter的api最初使用ruby写的,ruby默认的格式和go的不一样)我们需要定义我们自己的类型来正确的解码这个值。用encoding/json包看起来如果传值给json.Unmarshal,能满足json.Unmarshaler接口,如下:

1
2
3
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}

文档在这:http://golang.org/pkg/encoding/json/#Unmarshaler

所以我们需要的就是一个time.Time值和一个UnmarshalJSON([]byte) error方法:

1
2
3
4
5
type Timestamp time.Time

func (t *Timestamp) UnmarshalJSON(b []byte) error {
// ...
}

通过执行这个方法,我们满足了json.Unmarshaler接口,使得当遇到一个Timestamp值时,json.Unmarshal会调用我们通用的解包代码。在这里,我们用了一个指针类型方法,因为我们想要调用者看到对接收者的改变。为了设置指针指向的值,我们用获取指针。在UnmarshalJSON方法里面,t是一个指向Timestamp值的指针。通过调用t,我们获取指针t,并且可以访问到t指向的值。记住:在go里面所有都是值传递。那意味着,在UnmarshalJSON方法里,指针t和在调用时上下文的指针不是同一个,这是一个复制;如果你直接赋值t为另一个值,你将只是覆盖赋值一个函数范围的指针;这个改变不会被调用者看到。但是,方法里的指针和在调用范围的指针指向的是同一个数据;通过指针,我们可以让调用者看到我们对它的改变。

我们可以利用time.Parse方法,它的字面量是func(layout, value string)(Time, error)。它接收两个字符串参数:第一个是一个布局字符串,用来描述我们如何格式化时间戳,第二个是一个我们需要解析的值。它返回一个time.Time的值,还有一个错误。(返回我们因为某些原因错误解析了时间戳)你可以阅读更多关于布局字符串的含义

但是在这个例子中,我们不需要手动指出这个布局字符串,因为这个已经在标准库中作为值time.RubyDate存在了。所以最后,我们可以解析字符串”Thu May 31 00:00:01 +0000 2012”成一个time.Time值,通过调用函数time.Parse(time.RubyDate, “Thu May 31 00:00:01 +0000 2012”)。我们接收的值将是time.Time类型。在我们的例子中,我们对Timestamp类型比较感兴趣。通过调用Timestamp(v),v是我们的time.Time值,我们可以把time.Time值转换为一个Timestamp值。最终我们的UnmarshalJSON函数展开如下:

1
2
3
4
5
6
7
8
func (t *Timestamp) UnmarshalJSON(b []byte) error {
v, err := time.Parse(time.RubyDate, string(b[1:len(b)-1]))
if err != nil {
return err
}
*t = Timestamp(v)
return nil
}

我们从传入的字节切片中得到一个子切片,因为传入的字节切片是原始的json数据,并包含字符串周围的引号;我们想在传给time.Parse前砍掉这些引号。

现实的接口:从http请求中获取对象

让我们看看如何设计一个接口来解决常见的web开发问题:我们希望解析一个http请求的body为一些对象数据。首先,这不是一个非常明显能定义的接口。我们可以尝试解释,我们将从一个http请求中得到一个资源如下:

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

因为一个interface{}可以有任何底层类型,所以我们可以只是解析我们的请求并且返回任何我们想返回的。这被证明是一个非常糟糕的策略,原因是最终我们往GetEntity函数中添加了太多逻辑,现在GetEntity函数需要为每一个新类型进行修改,并且我们需要使用类型断言来对返回的interface{}做任何有用的操作。实际上,函数返回interface{}值往往很烦人,根据经验你只要记住,通常把一个interface{}值作为参数比返回一个interface{}值更好。
我们可能也需要写一些特殊类型的函数,比如:

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类型的变量,并且把这个函数的指针传给GetEntity:

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

这和你如何解包json数据类似。这种类型的东西一贯且安全,因为语句var u User将会自动赋予User数据结构每个字段零值。go不像其他语言声明和初始化单独进行,没有初始化的声明一个值,你可能会创造一个微妙的陷阱,访问一段垃圾数据;当声明一个值,go的运行时将会初始化合适的内存空间来存放这个值。即使我们的UnmarshalHTTP方法无法利用一些字段,这些字段也将会包含零数据而不是垃圾数据。

这应该会令你感到奇怪,如果你是一个python程序员,因为这基本上是在python中做的。这个表单变得如此简单的原因是,现在我们可以定义任意数量的类型,每个类型都从一个http请求中自己解包。现在由实体定义决定如何表示它们。然后,我们可以构建实体类型来创建通用http处理程序。

结尾

我希望你读完这些,在go中用接口会更顺手。记住以下要点:

  • 通过考虑数据类型之间通用的功能,而不是数据类型之间通用的字段来创建抽象;
  • 一个interface{}值不是任何类型,它就是interface{}类型;
  • 接口有两个字长;就像(type, value);
  • 最好是访问一个interface{}值,而不是返回一个interface{}值;
  • 一个指针类型可以调用和它关联的值类型方法,反之亦然;
  • 任何都是值传递,即使是方法的接受者;
  • 一个接口值不是一个严格的指针或不是指针,它只是一个接口;
  • 如果你需要完全覆盖一个方法里的值,用*运算符来手动得到一个指针;