目录

interfaces


interfaces

Go 中的接口是一种用于表示其他类型的行为的数据类型。 接口类似于对象应满足的蓝图或协定。 在你使用接口时,你的基本代码将变得更加灵活、适应性更强,因为你编写的代码未绑定到特定的实现。 因此,你可以快速扩展程序的功能。 在本模块,你将了解相关原因。

与其他编程语言中的接口不同,Go 中的接口是满足隐式实现的。 Go 不提供用于实现接口的关键字。 因此,如果你之前使用的是其他编程语言中的接口,但不熟悉 Go,那么此概念可能会造成混淆。

在此模块中,我们将使用多个示例来探讨 Go 中的接口,并演示如何充分利用这些接口。

1 声明接口

Go 中的接口类似于蓝图。 一种抽象类型,只包括具体类型必须拥有或实现的方法。

假设你希望在几何包中创建一个接口来指示形状必须实现的方法。 你可以按如下所示定义接口:

type Shape interface {
    Perimeter() float64
    Area() float64
}

Shape 接口表示你想要考虑 Shape 的任何类型都需要同时具有 Perimeter()Area() 方法。 例如,在创建 Square 结构时,它必须实现两种方法,而不是仅实现一种。 另外,请注意接口不包含这些方法的实现细节(例如,用于计算某个形状的周长和面积)。 接口仅表示一种协定。 三角形、圆圈和正方形等形状有不同的计算面积和周长方式。

2 实现接口

正如上文所讨论的内容,你没有用于实现接口的关键字。 当 Go 中的接口具有接口所需的所有方法时,则满足按类型的隐式实现。

让我们创建一个 Square 结构,此结构具有 Shape 接口中的两个方法,具体如下方的示例代码所示:

type Square struct {
    size float64
}

func (s Square) Area() float64 {
    return s.size * s.size
}

func (s Square) Perimeter() float64 {
    return s.size * 4
}

请注意 Square 结构的方法签名与 Shape 接口的签名的匹配方式。 但是,另一个接口可能具有不同的名称,但方法相同。 Go 如何或何时知道某个具体类型正在实现哪个接口? 在运行时,Go 会知道你何时使用了接口。

如要演示如何使用接口,你可以编写以下代码:

func main() {
    var s Shape = Square{3}
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
}

运行前面的代码时,你将看到以下输出:

main.Square
Area:  9
Perimeter: 12

此时,无论你是否使用接口,都没有任何区别。 接下来,让我们创建另一种类型,如 Circle,然后探讨接口有用的原因。 以下是 Circle 结构的代码:

type Circle struct {
    radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.radius
}

现在,让我们重构 main() 函数,并创建一个函数来打印其收到的对象类型,以及其面积和周长,具体如下所示:

func printInformation(s Shape) {
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
    fmt.Println()
}

请注意 printInformation 函数具有参数 Shape。 你可以将 SquareCircle 对象发送到此函数,尽管输出会有所不同,但仍可使用。 main() 函数此时将如下所示:

func main() {
    var s Shape = Square{3}
    printInformation(s)

    c := Circle{6}
    printInformation(c)
}

请注意,对于 c 对象,我们不能将其指定为 Shape 对象。 但是,printInformation 函数需要一个对象来实现 Shape 接口中定义的方法。

在运行程序时,你将会看到以下输出:

main.Square
Area:  9
Perimeter: 12

main.Circle
Area:  113.09733552923255
Perimeter: 37.69911184307752

请注意,此时你不会得到错误,输出会根据其收到的对象类型而变化。 你还可以看到输出中的对象类型不涉及 Shape 接口的任何内容。

使用接口的优点在于,对于 Shape的每个新类型或实现,printInformation 函数都不需要更改。 正如我们之前所述,当你使用接口时,代码会变得更灵活、更容易扩展。

3 实现字符串接口

扩展现有功能的一个简单示例是使用 Stringer,它是具有 String() 方法的接口,具体如下所示:

type Stringer interface {
    String() string
}

fmt.Printf 函数使用此接口来输出值,这意味着你可以编写自定义 String() 方法来打印自定义字符串,具体如下所示:

package main

import "fmt"

type Person struct {
    Name, Country string
}

func (p Person) String() string {
    return fmt.Sprintf("%v is from %v", p.Name, p.Country)
}
func main() {
    rs := Person{"John Doe", "USA"}
    ab := Person{"Mark Collins", "United Kingdom"}
    fmt.Printf("%s\n%s\n", rs, ab)
}

运行前面的代码时,你将看到以下输出:

John Doe is from USA
Mark Collins is from United Kingdom

如你所见,你已使用自定义类型(结构)来写入 String() 方法的自定义版本。 此方法是在 Go 中实现接口的一种常用方法,正如我们之前的讲解,你还会在许多程序中找到此方法的示例。

4 扩展现有实现

假设你具有以下代码,并且希望通过编写负责处理某些数据的 Writer 方法的自定义实现来扩展其功能。

通过使用以下代码,你可以创建一个程序,此程序使用 GitHub API 从 Microsoft 获取三个存储库:

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    io.Copy(os.Stdout, resp.Body)
}

运行前面的代码时,你会收到类似于以下输出的内容(已缩短以便改善可读性):

[{"id":276496384,"node_id":"MDEwOlJlcG9zaXRvcnkyNzY0OTYzODQ=","name":"-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","full_name":"microsoft/-Users-deepakdahiya-Desktop-juhibubash-test21zzzzzzzzzzz","private":false,"owner":{"login":"microsoft","id":6154722,"node_id":"MDEyOk9yZ2FuaXphdGlvbjYxNTQ3MjI=","avatar_url":"https://avatars2.githubusercontent.com/u/6154722?v=4","gravatar_id":"","url":"https://api.github.com/users/microsoft","html_url":"https://github.com/micro
....

请注意,io.Copy(os.Stdout, resp.Body) 调用是指将通过对 GitHub API 的调用获取的内容打印到终端。 假设你想要写入自己的实现以缩短你在终端中看到的内容。 在查看 io.Copy 函数的源 时,你将看到:

func Copy(dst Writer, src Reader) (written int64, err error)

如果你深入查看第一个参数 dst Writer 的详细信息,你会注意到 Writer接口

type Writer interface {
    Write(p []byte) (n int, err error)
}

你可以继续浏览 io 包的源代码,直到找到 Copy 调用 Write 方法的位置,但我们将暂时不进行此浏览。

由于 Writer 是接口,并且是 Copy 函数需要的对象,你可以编写 Write 方法的自定义实现。 因此,你可以自定义打印到终端的内容。

实现接口所需的第一项操作是创建自定义类型。 在这种情况下,你可以创建一个空结构,因为你只需按如下所示编写自定义 Write 方法即可:

type customWriter struct{}

现在,你已准备就绪,可开始编写自定义 Write 函数。 此时,你还需要编写一个结构,以便将 JSON 格式的 API 响应解析为 Golang 对象。 你可以使用“JSON 转 Go”站点从 JSON 有效负载创建结构。 因此,Write 方法可能如下所示:

type GitHubResponse []struct {
    FullName string `json:"full_name"`
}

func (w customWriter) Write(p []byte) (n int, err error) {
    var resp GitHubResponse
    json.Unmarshal(p, &resp)
    for _, r := range resp {
        fmt.Println(r.FullName)
    }
    return len(p), nil
}

最后,你必须修改 main() 函数以使用你的自定义对象,具体如下所示:

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    writer := customWriter{}
    io.Copy(writer, resp.Body)
}

在运行程序时,你将会看到以下输出:

microsoft/aed-blockchain-learn-content
microsoft/aed-content-nasa-su20
microsoft/aed-external-learn-template
microsoft/aed-go-learn-content
microsoft/aed-learn-template

由于你写入的自定义 Write 方法,输出效果现在更好。 以下是程序的最终版本:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

type GitHubResponse []struct {
    FullName string `json:"full_name"`
}

type customWriter struct{}

func (w customWriter) Write(p []byte) (n int, err error) {
    var resp GitHubResponse
    json.Unmarshal(p, &resp)
    for _, r := range resp {
        fmt.Println(r.FullName)
    }
    return len(p), nil
}

func main() {
    resp, err := http.Get("https://api.github.com/users/microsoft/repos?page=15&per_page=5")
    if err != nil {
        fmt.Println("Error:", err)
        os.Exit(1)
    }

    writer := customWriter{}
    io.Copy(writer, resp.Body)
}

5 编写自定义服务器 API

最后,我们一起来探讨接口的另一种用例,如果你要创建服务器 API,你可能会发现此用例非常实用。 编写 Web 服务器的常用方式是使用 net/http 程序包中的 http.Handler 接口,具体如下所示(无需写入此代码):

package http

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error

请注意 ListenAndServe 函数需要服务器地址(如 http://localhost:8000)以及将响应从调用调度至服务器地址的 Handler 的实例。

接下来,创建并浏览以下程序:

package main

import (
    "fmt"
    "log"
    "net/http"
)

type dollars float32

func (d dollars) String() string {
    return fmt.Sprintf("$%.2f", d)
}

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

func main() {
    db := database{"Go T-Shirt": 25, "Go Jacket": 55}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

在浏览前面的代码之前,让我们按如下所示运行程序:

go run main.go

如果没有得到任何输出,说明情况不错。 此时,在新浏览器窗口中打开 http://localhost:8000,或在终端中运行以下命令:

curl http://localhost:8000

现在你应会看到以下输出:

Go T-Shirt: $25.00
Go Jacket: $55.00

让我们一起慢慢回顾之前的代码,了解其用途并观察 Go 接口的功能。 首先,创建 float32 类型的自定义类型,然后编写 String() 方法的自定义实现,以便稍后使用。

type dollars float32

func (d dollars) String() string {
    return fmt.Sprintf("$%.2f", d)
}

然后,写入 http.Handler 可使用的 ServeHTTP 方法的实现。 请注意,我们重新创建了自定义类型,但这次它是映射,而不是结构。 接下来,我们通过使用 database 类型作为接收方来写入 ServeHTTP 方法。 此方法的实现使用来自接收方的数据,然后对其进行循环访问,再输出每一项。

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

最后,在 main() 函数中,我们将 database 类型实例化,并使用一些值对其进行初始化。 我们使用 http.ListenAndServe 函数启动了 HTTP 服务器,在其中定义了服务器地址,包括要使用的端口和实现 ServeHTTP 方法自定义版本的 db 对象。 在你运行程序时,Go 将使用此方法的实现,这也正是你在服务器 API 中使用和实现接口的方式。

func main() {
    db := database{"Go T-Shirt": 25, "Go Jacket": 55}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

使用 http.Handle 函数时,可以在服务器 API 中找到接口的其他用例。 有关详细信息,请参阅 Go 网站上编写 Web 应用程序帖子。