接口
接口的概念
现在我们要实现一个函数,用于对给定的 url 进行解析,具体的代码实现如下:
go
package main
import (
"fmt"
"io"
"net/http"
)
func retrieve(url string) string {
resp, err := http.Get(url)
if err != nil {
panic(err)
}
defer resp.Body.Close()
bytes, _ := io.ReadAll(resp.Body)
return string(bytes)
}
func main() {
url := "https://www.baidu.com"
fmt.Println(retrieve(url))
}
现在我们假设在写一个较大的工程,有一个专门负责网络架构的团队来完成网络请求、磁盘读写等需求的实现,保存在目录 infra 下,其中实现了一个 Retriever 保存在 urlretriever.go 文件下。
Retriever 的结构和方法的具体实现如下:
go
package infra
import (
"io"
"net/http"
)
type Retriever struct{}
func (Retriever) Get(url string) string {
// 接收者在不需要名字的时候可以只写类型
resp, err := http.Get(url)
if err != nil {
panic(err)
}
defer resp.Body.Close()
bytes, _ := io.ReadAll(resp.Body)
return string(bytes)
}
此时在 main 函数中,只需要新建一个 infra.Retriever 类型并使用其方法 Get 即可完成与上述代码等价的需求:
go
package main
import (
"fmt"
"learngo/infra"
)
func main() {
retriever := infra.Retriever{}
url := "https://www.baidu.com"
fmt.Println(retriever.Get(url))
}
上述代码还可以进一步地工程化,因为在大型项目当中,可能不止网络开发团队实现了 Retriever,测试团队可能同样开发了 Retriever 用于这部分代码的测试。所以显式的 infra.Retriever{ } 可以使用函数进行代替:
go
package main
import (
"fmt"
"learngo/infra"
)
func getRetriever() infra.Retriever {
return infra.Retriever{}
}
func main() {
var retriever infra.Retriever
retriever = getRetriever()
url := "https://www.baidu.com"
fmt.Println(retriever.Get(url))
}
此时我们注意到,上述代码还是不够好,因为就算将 infra.Retriever{ } 放到函数当中,main 函数当中的 retriever 的类型仍然是显式或隐式为 infra.Retriever 的。
瑕疵在于,main 函数当中的 retriever 必须是 infra.Retriever 类型的,main 函数与 infra.Retriever 的耦合较深。如果想要解耦,就需要用到 Go 的接口(interface)。
假如此时测试团队的 testing 目录下也有一个 Retriever,它同样有一个 Get 方法,但是行为与 infra 的 Retriever 完全不同,此时我们无法对 main 文件当中的 getRetriever 函数的返回类型进行修改。或者说,更换一个 Retriever,需要修改许多个地方,造成了很多工作量上的冗余。
go
package main
import (
"fmt"
"learngo/testing"
)
func getRetriever() testing.Retriever {
return testing.Retriever{}
}
func main() {
var retriever testing.Retriever
retriever = getRetriever()
url := "https://www.baidu.com"
fmt.Println(retriever.Get(url))
}
产生上述我们不满意的情况的原因是,Golang 是一个强类型的语言(或者说静态语言),而不是弱类型或动态绑定的系统,在写代码时,编译阶段我们就已经知道变量的类型,而对于 Python 等动态语言,在运行时才知道类型。
解决上述问题的方案是,让代码与逻辑相一致。在 main 函数中,变量 retriever 的类型不应该强绑定为某个类型的 retriever:
go
var retriever ?
// ? is something that can get
retriever 的类型假设我们此时是不知道的,但是我们需要这个类型具有 Get 方法,才能使后面的:
go
retriever.Get(url)
顺利运行。至于具体的类型是 infra 的 Retriever 还是 testing 的 Retriever,我们不需要关心。这个我们不知道的类型正是接口(interface)。
使用关键字 interface 来定义一个接口,它的声明语句与 struct 的声明非常类似:
go
type retriever interface {
Get(string) string
}
使用接口的具体实现如下:
go
package main
import (
"fmt"
"learngo/testing"
)
func getRetriever() testing.Retriever {
return testing.Retriever{}
}
type retriever interface {
Get(string) string
}
func main() {
var r retriever = getRetriever()
url := "https://www.baidu.com"
fmt.Println(r.Get(url))
}
函数 getRetriever() 的返回值类型也应该与 testing 解耦,直接更换为接口 retriever:
go
func getRetriever() retriever {
return testing.Retriever{} // 返回的仍然是 testing.Retriever{}
}
使用 testing 的 Retriever 测试通过后,可以将代码业务上限,此时只需要更换 getRetriever 中的 testing 为 infra 即可。
鸭子类型(duck typing)
鸭子类型可以概括为:走路像鸭子,说话像鸭子,长得像鸭子,那么它就是鸭子。它强调的是描述事物的外部行为而非内部结构。
严格说 Go 属于结构化类型系统,类似 Duck Typing。
接口的定义和实现
接口由使用者 来定义。
下面是一个示例,此例需要我们新建一个 retriever 目录,并将下述代码写在目录下的 main.go 当中:
go
package main
import "fmt"
type Retriever interface {
Get(url string) string
}
func download(r Retriever) string {
// download 是一个使用者, 使用者要 Get, 因此要定义一个 Retriever 接口
return r.Get("https://www.baidu.com")
}
func main() {
var r Retriever
fmt.Println(download(r))
}
但是上述程序还无法直接运行,因为 r 还没有一个具体的实现。
在 retriever 目录下新建 mock 目录,并在 mock 目录下定义 mockretriever.go:
go
package mock
type Retriever struct {
Contents string
}
func (r Retriever) Get(url string) string {
return r.Contents
}
编译器会发现我们定义的是可以用于接口实现的结构:
此时,修改我们的 main 函数体为:
go
func main() {
var r Retriever
r = mock.Retriever{"this is a fake www.baidu.com"}
fmt.Println(download(r))
}
👆此时,我们定义了一个接口的实现,相当于把一个可以匹配接口的结构对象赋予接口对象。
按照类似的方式,我们在 retriever 目录下新建一个 real 目录,在当中实现真实的 Retriever,其结构实现定义在 retriever.go 文件中:
Retriever 结构的定义和方法如下:
go
package real
import (
"net/http"
"net/http/httputil"
"time"
)
type Retriever struct {
UserAgent string
TimeOut time.Duration // 代表时间长度
}
func (r *Retriever) Get(url string) string {
resp, err := http.Get(url)
if err != nil {
panic(err)
}
result, err := httputil.DumpResponse(resp, true)
resp.Body.Close()
if err != nil {
panic(err)
}
return string(result)
}
在 main.go 中使用 real 的 Retriever 实现接口:
go
func main() {
r := real.Retriever{}
fmt.Println(download(r))
}
可以看到,由使用者(对应上述 main.go 当中的 download)来定义接口当中必须要有的方法,实现者不需要实现某个具体的接口,只需要实现接口当中的方法即可。
接口的值类型
现在我们想要查看接口当中究竟包含哪些成员,对 main 函数体进行如下修改:
go
func main() {
var r Retriever
r = mock.Retriever{"this is a fake www.baidu.com"}
fmt.Printf("%T %v\n", r, r)
r = real.Retriever{}
fmt.Printf("%T %v\n", r, r)
fmt.Println(download(r))
}
输出的内容如下:
go
mock.Retriever {this is a fake www.baidu.com}
real.Retriever { 0s} // UserAgent 是 空格, TimeOut 是 0s
由于 Golang 的函数调用均为传值调用,当我们为 real.Retriever 定义成员时,使用指针接收者对方法进行定义可以加快方法的速度。如果单纯地将结构的方法改为指针接收者:func (r *Retriever) Get(url string) string { ... }
则接口使用的部分将会报错:
取一个地址即可修改上述错误:
此时运行 main 函数得到的结果如下:
go
mock.Retriever {this is a fake www.baidu.com}
*real.Retriever &{Mozilla/5.0 1m0s}
因此,接口接收的可能是一个真实的值,也可能是一个指针。如果接收一个指针,那么指针指向的对象在实现接口的方法时应该使用指针接收者。
我们还想要知道接口当中结构的类型,获取接口中结构的类型有多种方法,定义一个 inspect 函数来完成,inspect 的参数是 Retriever 接口:
go
func inspect(r Retriever) {
switch v := r.(type) {
case mock.Retriever:
fmt.Printf("Contents:", v.Contents)
case *real.Retriever:
fmt.Printf("UserAgent:", v.UserAgent)
}
}
修改 main 为:
go
func main() {
var r Retriever
r = mock.Retriever{"this is a fake www.baidu.com"}
inspect(r)
r = &real.Retriever{
UserAgent: "Mozilla/5.0",
TimeOut: time.Minute,
}
inspect(r)
//fmt.Println(download(r))
}
结果如下:
go
mock.Retriever {this is a fake www.baidu.com}
Contents: this is a fake www.baidu.com
*real.Retriever &{Mozilla/5.0 1m0s}
UserAgent: Mozilla/5.0
此外,我们还可以通过 Type Assertion 的方法来获取 interface 当中的类型:
go
// Type Assertion
realRetriever := r.(*real.Retriever)
fmt.Println(realRetriever.TimeOut)
/* ... 修改 r 的类型为 mock.Retriever ... */
mockRetriever := r.(mock.Retriever) // 注意, type assertion 必须添加圆括号, 无论是值还是指针
fmt.Println(mockRetriever.Contents)
通过上面的例子,可以将接口变量当中包含的内容总结为:实现者的类型 + 实现者的值/指针:
- 接口变量自带指针;
- 接口变量同样采用值传递,几乎不需要使用接口的指针;
- 指针接收者的方法实现只能以指针方式使用,值接收者都可以。(正如上面 real.Retriever 的例子,该例将方法定义为指针接收者,在使用接口接收 real.Retriever 时,必须取 real.Retriever 的地址,否则会编译出错。而 mock.Retriever 对接口方法的实现使用值接收者,我们可以传递 mock.Retriever 的地址给接口,这样做不会产生编译错误,而直接使用 mock.Retriever 的值则更加方便。)
可以使用 interface{} 来表示任何类型
可以使用 interface{} 来表示任何类型,一个例子如下,该例尝试对我们之前实现的 queue 进行修改。先前实现的 queue 是对 []int 的别名,即 int 类型的 slice 的别名,并对 queue 定义了许多方法。如果我们不希望 queue 只能接受 int 类型的值,可以使用 interface{} 来对其进行改写:
go
package queue
type Queue []interface{} // 使用 interface{} 表示任何类型
func (q *Queue) Push(v interface{}) {
*q = append(*q, v) // q 指向的 slice 被改变了
}
func (q *Queue) Pop() interface{} {
head := (*q)[0]
*q = (*q)[1:]
return head
}
func (q *Queue) IsEmpty() bool {
return len(*q) == 0
}
此时的 Queue 类似于 Python,可以接受任何类型的变量。
此时使用 Queue:
go
package main
import (
"fmt"
"learngo/queue"
)
func main() {
q := queue.Queue{1}
q.Push(2)
q.Push(3)
fmt.Println(q.Pop())
fmt.Println(q.Pop())
fmt.Println(q.IsEmpty())
fmt.Println(q.Pop())
fmt.Println(q.IsEmpty())
q.Push("abc")
q.Push(3.1415926)
fmt.Println(q.Pop())
fmt.Println(q.Pop())
}
输出为:
go
1
2
false
3
true
abc
3.1415926
如果我们想要对 Queue 进行进一步的限定,使得它只能接受 int 类型的变量,可以进一步对 Queue 的定义进行修改:
go
type Queue []interface{} // 使用 interface{} 表示任何类型
func (q *Queue) Push(v int) { // 传入的参数限定为 int
*q = append(*q, v) // q 指向的 slice 被改变了
}
func (q *Queue) Pop() int { // 返回的参数限定为 int
head := (*q)[0]
*q = (*q)[1:]
return head.(int) // 需要将返回值强制类型转换为 int
// 👆 head 是一个 interface, 将 interface 当中的值强制转换为 int
}
func (q *Queue) IsEmpty() bool {
return len(*q) == 0
}
接口的组合
有时我们希望一个接口既可以读又可以写,此时可以用到接口的组合。
首先我们定义一个 Poster 接口,将其一并定义在 retriever 目录下的 main.go 文件当中(与 Retriever 接口定义在同一个文件当中),并定义一个接口要执行的行为(放在函数 post 当中,正如 Retriever 接口要做的事情放在了 download 当中):
go
type Poster interface {
Post(url string, form map[string]string) string
}
func post(poster Poster) {
poster.Post("https://www.baidu.com",
map[string]string{
"name": "baidu",
"item": "BaiduNetDisk",
})
}
我们希望定义一个既可以读也可以写的接口,可以将上述的 Retriever 接口和 Poster 接口定义在 RetrieverPoster 接口当中来完成接口的组合:
go
type RetrieverPoster interface {
Retriever
Poster
}
当然,RetrieverPoster 当中可以定义其它的方法,但此时还用不上,只需要 Retriever 和 Poster 两个接口。进一步将接口 RetrieverPoster 要做的事情定义在 session 当中:
go
func session(s RetrieverPoster) string {
s.Get()
s.Post()
}
在具体的实现部分,以 mock.Retriever 为例,如果想要 RetrieverPoster 接受 mock.Retriever,则它还需要再实现一个名为 Post 的接口:
go
package mock
type Retriever struct {
Contents string
}
func (r Retriever) Post(url string, form map[string]string) string { // 实现 Post 方法
r.Contents = form["contents"] // 为了将结构传入 RetrieverPoster 接口
return "ok"
}
func (r Retriever) Get(url string) string {
return r.Contents
}
此时给 session 一个具体的实现:
go
func session(s RetrieverPoster) string {
s.Post(url, map[string]string{
"contents": "another faked www.baidu.com",
})
return s.Get(url)
}
现在在 main 函数体当中,将 mock.Retriever 传递给 RetrieverPoster 接口:
go
func main() {
r := mock.Retriever{"this is a fake www.baidu.com"}
fmt.Println()
fmt.Println(session(r))
}
结果为:
go
this is a fake www.baidu.com
与我们的预期不符,原因是 session 当中修改了 Contents 的内容,而结果没有显示修改后的值。原因在于最初定义的 Post 使用的是值接受者,需要对 mock.Retriever 的两个接口方法的定义进行进一步的修改,将 Post 和 Get 方法修改为指针接收者。修改后的结果为:
go
func (r *Retriever) Post(url string, form map[string]string) string {
r.Contents = form["contents"]
return "ok"
}
func (r *Retriever) Get(url string) string {
return r.Contents
}
可以预见的是,在 main 函数体中,也需要把 RetriverPoster 的接收值修改为地址:
go
// ...
r := &mock.Retriever{"this is a fake www.baidu.com"}
// ...
此时得到的是正确的值:
go
another faked www.baidu.com