接口
接口的概念
现在我们要实现一个函数,用于对给定的 url 举行剖析,具体的代码实现如下:
- 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 的结构和方法的具体实现如下:
- 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 即可完成与上述代码等价的需求:
- 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{ } 可以使用函数举行代替:
- 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,需要修改很多个地方,造成了很多工作量上的冗余。
- 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:
- var retriever ?
- // ? is something that can get
复制代码 retriever 的类型假设我们此时是不知道的,但是我们需要这个类型具有 Get 方法,才华使后面的:
顺利运行。至于具体的类型是 infra 的 Retriever 还是 testing 的 Retriever,我们不需要关心。这个我们不知道的类型正是接口(interface)。
使用关键字 interface 来界说一个接口,它的声明语句与 struct 的声明非常类似:
- type retriever interface {
- Get(string) string
- }
复制代码
使用接口的具体实现如下:
- package mainimport ( "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:
- func getRetriever() retriever {
- return testing.Retriever{} // 返回的仍然是 testing.Retriever{}
- }
复制代码 使用 testing 的 Retriever 测试通事后,可以将代码业务上限,此时只需要更换 getRetriever 中的 testing 为 infra 即可。
鸭子类型(duck typing)
鸭子类型可以概括为:走路像鸭子,说话像鸭子,长得像鸭子,那么它就是鸭子。它强调的是形貌事物的外部举动而非内部结构。
严酷说 Go 属于结构化类型系统,类似 Duck Typing。
接口的界说和实现
接口由使用者来界说。
下面是一个示例,此例需要我们新建一个 retriever 目次,并将下述代码写在目次下的 main.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:
- package mock
- type Retriever struct {
- Contents string
- }
- func (r Retriever) Get(url string) string {
- return r.Contents
- }
复制代码 编译器会发现我们界说的是可以用于接口实现的结构:
此时,修改我们的 main 函数体为:
- func main() {
- var r Retriever
- r = mock.Retriever{"this is a fake www.baidu.com
- "}
- fmt.Println(download(r))
- }
复制代码 |