Go Interfaces 使用教程

Go

在我使用 Go 语言编程之前,我的大部分工作都是使用 Python 完成的。作为一个 Python 开发者,我发现在 Go 中使用接口非常困难。也就是说,基础的东西很简单,并且我知道怎么在标准库中使用接口。但是在知道如何设计自己的接口之前得做一些实践。在这篇文章中,我将论述一下 Go 语言的类型系统,来说明如何有效的使用接口。

接口简介

那么,什么接口呢?一个接口包含两个东西:它是一组方法,但也是一个类型。让我们首先把关注点放在接口的方法设置方面。

通常,我们使用一些人为的例子来介绍接口。让我们看一个定义在 Animal 数据类型上的应用程序的人为接口。因为这完全是符合现实情况的。Animal 类型将会成为一个接口。我们将Animal定义成谁都可以调用的接口,这在 Go语言的类型系统中是核心概念。我们不是根据类型可以容纳的数据类型来设计抽象,而是根据类型可以执行的操作来设计抽象。

我们开始定义我们的Animal接口

type Animal interface {
    Speak() string
}

非常简单:我们定义了一个有Speak方法的Animal类型,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!"
}

现在,我们有四种不同类型的动物:狗,猫,美洲驼和Java程序员。在我们的main()函数中,我们可以创建 Animals 切片,并将每种类型中的一种放入该切片中,然后查看每只动物怎么说。现在开始操作:

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

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

太好了,现在你知道如何使用接口了,我不需要再谈论它们了,对吧?嗯,不是真的。让我们看看一些对初出茅庐的程序员来说不是很明显的事情。

 interface{} 类型

interface{} 类型,空接口是造成大量混乱的来源。interface{}类型是没有方法的接口,因为没有implements关键字,因此所有类型都至少实现零个方法,并且自动完成接口的满足,所有类型都满足空接口。这意味着如果编写的函数将interface {}值作为参数,则可以为该函数提供任何值。因此,此方法:

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

将接收任何参数

这是令人困惑的地方:在DoSomething函数内部,v的类型是什么?导致初级的人相信“ v是任何类型,但这是错误的。 v不是 任何 类型;它是接口{}类型的。等一下将值传递给DoSomething函数时,Go运行时将执行类型转换(如有必要),并将该值转换为interface {}值。所有值在运行时都只有一种类型,v的一种静态类型是interface {}

这应该让您感到疑惑:好吧,因此,如果正在进行转换,则实际上被传递到具有interface {}值的函数中(或者实际上存储在 [] Animal切片)?接口值是由两个数据字构成的。一个单词用于指向该值的基础类型的方法表,另一个单词用于指向该值所保存的实际数据。我不想为此不断抱怨。如果您了解接口值是两个字宽,并且包含指向基础数据的指针,则通常足以避免常见的陷阱。如果您想了解有关接口实现的更多信息,我想Russ Cox对接口的描述是非常,非常有帮助的。

在前面的示例中,当我们构造一个Animal值的切片时,我们不必说很费力的诸如Animal(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

通过运行此命令,您可以看到我们遇到以下错误:不能在函数参数中使用名称([[string]类型)作为[] interface {}类型。如果要真正实现该功能,我们必须将[] string转换为[]接口{}

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

这很丑陋,但是这就是生活。并非所有事物都是完美的。 (实际上,这种情况很少出现,因为[] interface {}的用处比最初预期的要少)

指针和接口

接口的另一个微妙之处在于,接口定义没有规定实现者应该使用指针接收器还是值接收器。当您获得接口值时,将无法保证基础类型是否是指针。在前面的示例中,我们在值接收器上定义了所有方法,并将关联的值放入Animal 切片中。接下来对其进行更改,使 CatSpeak() 方法采用指针接收器:

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 切片来修复此错误,方法是使用 new(Cat) 而不是 Cat{} (也可以使用&Cat{},我只是更喜欢 new(Cat)):

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

再次运行 :http://play.golang.org/p/x5VwyExxBM

换个思维:让我们传递一个*Dog 指针而不是 Dog 值,但是这次我们不会更改DogSpeak 方法:

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 指针开头。相反,如果我们有一个 Dog类型的方法,并且有一个 *Dog 指针,我们知道调用此方法时要使用哪个 Dog 值,因为*Dog 指针正好指向一个Dog值; Go 将在必要时随时将指针取消引用与其关联的指针。也就是说,给定 *Dogd并在Dog 类型上使用方法Speak,我们可以 d.Speak() 而不需要像其他语言 d->Speak()

实践:从Twitter API获得正确的时间戳

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

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

当然,时间戳记可以以多种方式在 JSON 中表示,因为时间戳记不是 JSON 规范的一部分。为了简洁起见,我不会放入一条 tweet 整个 JSON 数据,但是让我们看一下如何使用 encoding/json 处理 created_at字段:

package main

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

// 从字符串形式的 json 开始
var input = `
{
    "created_at": "Thu May 31 00:00:01 +0000 2012"
}
`

func main() {
    // 我们的目标将为 map[string] interface{} 类型,它是
    // 非常普通的类型,它将为我们提供一个哈希表,其键
    // 是字符串,其值的类型为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://play.golang.org/p/VJAyqO3hTF

运行该应用程序,我们看到以下输出:

map[created_at:Thu May 31 00:00:01 +0000 2012]
created_at string

我们可以看到我们已经正确解析了键值,但是像这样的字符串格式的时间戳不是很有用:如果我们想比较时间戳以查看哪个更早?或者查看自给定值和当前时间起经过了多少时间?

让我们天真地尝试将其解组为 time.Time 值,这是时间的标准库表示形式,并查看我们得到的错误类型。进行以下更改:

   var val map[string]time.Time

    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 值与字符串之间的转换。简而言之,这意味着我们提供的字符串形式与标准时间格式不匹配(因为Twitter的API最初是用Ruby编写的,而 Ruby 的默认格式与 Go 的默认格式不同)。我们需要定义我们自己的类型,以便正确解析此值。 encoding/json 包用于查看将值传递给 json.Unmarshal 的值是否满足 json.Unmarshaler 接口的要求,如下所示:

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

这在这里的文档指出 :http://golang.org/pkg/encoding/json/#Unmar...

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

type Timestamp time.Time

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

通过实现该方法,我们满足了 json.Unmarshaler 接口,当解析时间导致 json.Unmarshal 时调用我们的自定义的解析代码。对于这种情况,我们使用指针方法,因为我们希望调用方看到对接收方所做的更改。为了设置指针指向的值,我们使用运算符手动取消对指针的引用。在 UnmarshalJSON 方法内部,t 表示指向时间戳值的指针。通过声明 *t,我们取消了指针 t 的引用,因此能够访问t所指向的值。记住:在 Go 里一切皆按值传递。这意味着在 UnmarshalJSON 方法内部,指针 t 与其调用上下文中的指针不同;它是一个副本。如果要直接将t赋值给另一个值,则只需重新分配函数本地指针;调用方看不到更改。但是,方法内的指针与其调用范围内指针的数据相同;通过取消对指针的引用,我们可以使调用方获取到的更改。

我们可以利用 time.Parse 的方法签名 func(layout, value string) (Time, error)。也就是说,它需要两个字符串:第一个字符串是描述如何格式化时间戳的布局字符串,第二个字符串是要解析的值。它返回一个time.time值,以及一个错误(以防由于某种原因未能解析时间戳)。您可以在时间包文档中阅读有关布局字符串语义的更多信息。但是在这个例子中,我们不需要手工计算布局字符串,因为这个布局字符串已经存在于标准库中,值为 time.RubyDate 。因此,实际上,我们可以通过调用函数  time.Parse(time.RubyDate, "Thu May 31 00:00:01 +0000 2012") 将字符串Thu May 31 00:00:01 +0000 2012 解析为一个time.time值。我们将收到的值类型为 time.time。在我们的例子中,我们对时间戳类型的值感兴趣。我们可以将 time.Time 值转换为时间戳值,其中 v 是我们的time.Time值。最终,我们的 UnmarshalJSON 函数的结果是这样的:

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://play.golang.org/p/QpiFsJi-nZ

现实世界的接口:从 http 请求中获取一个对象

最后,让我们看看如何设计接口来解决常见的 Web 开发问题:我们希望将 HTTP 请求的主体解析为一些对象数据。首先,这不是一个很明显的定义接口。假如我们将从 HTTP 请求中获取资源,如下所示:

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

因为interface{}可以有任何底层类型,所以我们可以解析请求并返回想要的任何内容。这是一个非常糟糕的策略,原因是我们在 GetEntity 函数中粘贴了太多的逻辑,现在需要为每种新类型修改 GetEntity 函数,我们需要使用类型断言对返回的 interface{} 值执行任何有用的操作。实际上,返回 interface{}值的函数往往非常烦人,根据经,将interface{}值作为参数通常比返回interface{} 值要好。(波斯特尔定律 适用于接口)

我们也可能会尝试编写一些特定类型的函数:

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

这也是相当不灵活的,因为现在我们对每种类型都有不同的功能,但是没有一种明智的方法来概括它们。相反,我们真正想做的是这样的事情:

type Entity interface {
    UnmarshalHTTP(*http.Request) error
}
func GetEntity(r *http.Request, v Entity) error {
    return v.UnmarshalHTTP(r)
}

其中,GetEntity 函数接受的接口值需保证具有 UnmarshalHTTP 方法。为了确保这一点,我们将在我们的User对象上定义一些方法,允许User 描述它如何从 HTTP 请求中脱离出来:

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 方法未能利用某些字段,这些字段也将包含有效的零数据,而不是垃圾数据。

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

总结一下

我希望您在阅读本文之后,会觉得在 Go 中使用接口更舒服。记住以下几点:

  • 通过考虑数据类型之间通用的功能来创建抽象,而不是数据类型之间通用的字段
  • 一个interface{}值不是任何类型;它是interface{}类型
  • 接口有两个字宽;从原理上看,它们类似于(type,value)
    -接受 interface{} 值比返回 interface{}值要好
  • 指针类型可以调用其关联值类型的方法,但反之亦然
  • 一切都是按值传递的,即使是方法的接收者,接口值也不是严格意义上的指针,它只是一个接口,如果需要完全覆盖方法内部的值,请使用 * 运算符手动取消对指针的引用

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

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://jordanorelli.com/post/3266586024...

译文地址:https://learnku.com/go/t/38843

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 2

我们有四种不同类型的动物:狗,猫,美洲驼和 Java 程序员

3年前 评论

翻译的很好,但是有点难以理解,像是早起的laravel翻译 :joy:

1年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!