[译] 如何在 Go 中使用接口

本文翻译自 How to use interfaces in Go 有部分删减,请以原文为准程序员

在开始使用 Go 编程以前,个人大部分工做都是用 Python 完成的。做为一名 Python 程序员,我发现学习使用 Go 中的接口是很是困难的。基础很简单,并且我知道如何在标准库中使用接口,可是我作了不少练习以后才知道如何设计本身的接口。在本文中,我将讨论 Go 的类型系统,以解释如何有效地使用接口。golang

接口介绍

接口是什么?一个接口包含两层意思:它是一个方法的集合,一样是一个类型。让咱们首先关注接口做为方法的集合这一方面。web

一般,咱们会用一些假设的例子来介绍接口。让咱们来看看这个例子: Animal 类型是一个接口,咱们将定义一个 Animal 做为任何能够说话的东西。这是 Go 类型系统的核心概念:咱们根据类型能够执行的操做而不是其所能容纳的数据类型来设计抽象。编程

type Animal interface {
    Speak() string
}
复制代码

很是简单:咱们定义 Animal 为任何具备 Speak 方法的类型。Speak 方法没有参数,返回一个字符串。全部定义了该方法的类型咱们称它实现Animal 接口。Go 中没有 implements 关键字,判断一个类型是否实现了一个接口是彻底是自动地。让咱们建立几个实现这个接口的类型:json

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!"
}
复制代码

咱们如今有四种不一样类型的动物:DogCatLlamaJavaProgrammer。在咱们的 main 函数中,咱们建立了一个 []Animal{Dog{}, Cat{}, Llama{}, JavaProgrammer{}} ,看看每只动物都说了些什么:安全

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

很好,如今你知道如何使用接口了,我不须要再讨论它们了,对吧?不是的。让咱们来看看一些不太明显的东西。bash

interface{} 类型

interface{} 类型,空接口,是致使不少混淆的根源。interface{} 类型是没有方法的接口。因为没有 implements 关键字,因此全部类型都至少实现了 0 个方法,因此 全部类型都实现了空接口。这意味着,若是您编写一个函数以 interface{} 值做为参数,那么您能够为该函数提供任何值。例如:函数

func DoSomething(v interface{}) {
   // ...
}
复制代码

这里是让人困惑的地方:在 DoSomething 函数内部,v 的类型是什么?新手们会认为 v任意类型的,但这是错误的。v 不是任意类型,它是 interface{} 类型。对的,没错!当将值传递给DoSomething 函数时,Go 运行时将执行类型转换(若是须要),并将值转换为 interface{} 类型的值。全部值在运行时只有一个类型,而 v 的一个静态类型是 interface{}post

这可能让您感到疑惑:好吧,若是发生了转换,究竟是什么东西传入了函数做为 interface{} 的值呢?(具体到上例来讲就是 []Animal 中存的是啥?)学习

一个接口值由两个字(32 位机器一个字是 32 bits,64 位机器一个字是 64 bits)组成;一个字用于指向该值底层类型的方法表,另外一个字用于指向实际数据。我不想没完没了地谈论这个。若是您理解一个接口值是两个字,而且它包含指向底层数据的指针,那么这就足以免常见的陷阱。若是您想了解更多关于接口实现的知识。这篇文章颇有用:Russ Cox’s description of interfaces 。

在咱们上面的例子中,当咱们初始化变量 animals 时,咱们不须要像这样 Animal(Dog{}) 来显示的转型,由于这是自动地。这些元素都是 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)
}
复制代码

运行这段代码你会获得以下错误:cannot use names (type []string) as type []interface {} in argument to PrintAll。若是想使其正常工做,咱们必须将 []string 转为 []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)
}
复制代码

很丑陋,可是生活就是这样,没有完美的事情。(事实上,这种状况不会常常发生,由于 []interface{} 并无像你想象的那样有用)

指针和接口

接口的另外一个微妙之处是接口定义没有规定一个实现者是否应该使用一个指针接收器或一个值接收器来实现接口。当给定一个接口值时,不能保证底层类型是否为指针。在前面的示例中,咱们将方法定义在值接收者之上。让咱们稍微改变一下,将 CatSpeak() 方法改成指针接收器:

func (c *Cat) Speak() string {
    return "Meow!"
}
复制代码

运行上述代码,会获得以下错误:

cannot use Cat literal (type Cat) as type Animal in array or slice literal:
	Cat does not implement Animal (Speak method has pointer receiver)
复制代码

该错误的意思是:你尝试将 Cat 转为 Animal ,可是只有 *Cat 类型实现了该接口。你能够经过传入一个指针 (new(Cat) 或者 &Cat{})来修复这个错误。

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

让咱们作一些相反的事情:咱们传入一个 *Dog 指针,可是不改变 DogSpeak() 方法:

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

这种方式能够正常工做,由于一个指针类型能够经过其相关的值类型来访问值类型的方法,可是反过来不行。即,一个 *Dog 类型的值可使用定义在 Dog 类型上的 Speak() 方法,而 Cat 类型的值不能访问定义在 *Cat 类型上的方法。

这可能听起来很神秘,但当你记住如下内容时就清楚了:Go 中的全部东西都是按值传递的。每次调用函数时,传入的数据都会被复制。对于具备值接收者的方法,在调用该方法时将复制该值。例以下面的方法:

func (t T)MyMethod(s string) {
    // ...
}
复制代码

func(T, string) 类型的方法。方法接收器像其余参数同样经过值传递给函数。

由于全部的参数都是经过值传递的,这就能够解释为何 *Cat 的方法不能被 Cat 类型的值调用了。任何一个 Cat 类型的值可能会有不少 *Cat 类型的指针指向它,若是咱们尝试经过 Cat 类型的值来调用 *Cat 的方法,根本就不知道对应的是哪一个指针。相反,若是 Dog 类型上有一个方法,经过 *Dog 来调用这个方法能够确切的找到该指针对应的 Gog 类型的值,从而调用上面的方法。运行时,Go 会自动帮咱们作这些,因此咱们不须要像 C语言中那样使用相似以下的语句 d->Speak()

例1:经过 Twitter API 获取正确的时间戳

Twitter API 使用下面的格式来展现时间戳:

"Thu May 31 00:00:01 +0000 2012"
复制代码

Twitter API 返回的是一个 json 字符串,这里咱们只考虑解析 created_at 字段:

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 Thu May 31 00:00:01 +0000 2012 string
复制代码

咱们获得了解析后的结果,可是解析出来的时间是字符串类型的,做用有限,所以咱们想把它解析成 time.Time 类型的,对代码作出以下修改:

var val map[string]interface{} -> var val map[string]time.Time
复制代码

结果出错了:

panic: 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 中的时间格式不匹配(由于 Twitter's API 是用 Ruby 写的,其格式跟 Go 不一样)。咱们必须定义咱们本身的类型来解析时间。encoding/json 在解析时会判断传入 json.Unmarshal 的值是否实现了 json.Unmarshaler 接口:

type Unmarshaler interface {
    UnmarshalJSON([]byte) error
}
复制代码

若是实现了,就会调用 UnmarshalJSON 方法来解析(参考),因此咱们须要的是一个实现了 UnmarshalJSON([]byte) error 方法的类型:

type Timestamp time.Time

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

值得注意的是,咱们使用一个指针做为方法接受者,由于咱们但愿在方法内对接受者进行更改。UnmarshalJSON 中,t 表明指向 Timestamp 类型值的指针,经过 *t 咱们能够访问到这个值,这样就能够修改它了。

咱们可使用 time.Parse(layout, value string) (Time, error) 来解析时间,该函数的第一个参数是表示时间格式的字符串(更多字符串格式),第二个是咱们要解析的字符串。返回 time.Time 类型的值以及 error(若是解析出错)。解析获得 time.Time 类型的值后,转换成 Timestamp 类型而后赋值给 *t

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
}
复制代码

注意,传入函数的 []byte 是原始的 JSON 数据,其中包含有引号,因此这里须要切片去掉引号。

例2:从 HTTP 请求中获得对象

然咱们设计一个接口来解决 web 开发中常见的一个问题:咱们想解析 HTTP 请求体获得咱们须要的对象数据。例如,咱们这样定义咱们的接口:

GetEntity(*http.Request) (interface{}, error)
复制代码

由于 interface{} 能够有任意的底层类型,因此咱们能够解析获得任何咱们须要的东西。可是这是一个很差的设计,咱们将过多的逻辑引入到 GetEntity 函数中,GetEntity 函数如今须要针对每一种新类型进行修改,咱们须要使用类型断言来处理返回的值。在实践中,返回 interface{} 的函数每每很烦人,做为一个经验法则,您只须要记住,将 interface{} 做为参数而不是返interface{} 值一般更好(Postel’s Law)。

咱们也可能会尝试编写一些返回类型明确的函数,像这样:

GetUser(*http.Request) (User, error)
复制代码

可是这样又显得不够灵活,由于须要对不一样的类型写不一样的函数。咱们真正须要的是像这样的一个设计:

type Entity interface {
	UnmarshalerHTTP(*http.Request) error
}

func GetEntity(r *http.Request, v Entity) error {
	return v.UnmarshalerHTTP(r)
}
复制代码

GetEntiry 方法须要传入一个参数,该参数为 Entity 接口类型,确保实现了 UnmarshalHTTP 方法。为了使用该方法,咱们须要定义 User 类型并实现 UnmarshalHTTP 方法,并在方法中解析 HTTP 请求:

type User struct {
   ...
}

func (u *User) UnmarshalHTTP(r *http.Request) error {
   // ...
}
复制代码

而后,定义一个 User 类型的变量,并将其指针传递给 GetEntity 方法:

var u User
if err := GetEntity(req, &u); err != nil {
    // ...
}
复制代码

这同解析 JSON 数据相似。这种方式能够始终如一地安全地工做,由于 var u User 将自动地将 User 结构体初始化为零值。Go 不像其余语言同样声明和初始化是分开进行的。经过声明一个值而不初始化它,运行时将为该值分配适当的内存空间。即便咱们的 UnmarshalHTTP 方法不能使用某些字段,这些字段也将包含有效的零数据,而不是垃圾数据。

结语

我但愿读完此文后你能够更加驾轻就熟地使用 Go 中的接口,记住下面这些结论:

  • 经过考虑数据类型之间的相同功能来建立抽象,而不是相同字段
  • interface{} 的值不是任意类型,而是 interface{} 类型
  • 接口包含两个字的大小,相似于 (type, value)
  • 函数能够接受 interface{} 做为参数,但最好不要返回 interface{}
  • 指针类型能够调用其所指向的值的方法,反过来不能够
  • 函数中的参数甚至接受者都是经过值传递
  • 一个接口的值就是就是接口而已,跟指针没什么关系
  • 若是你想在方法中修改指针所指向的值,使用 * 操做符
相关文章
相关标签/搜索