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
。 你可以将 Square
或 Circle
对象发送到此函数,尽管输出会有所不同,但仍可使用。 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 应用程序帖子。