【Golang】Go语言编程头脑(一):接口

打印 上一主题 下一主题

主题 1896|帖子 1896|积分 5688

接口

接口的概念

现在我们要实现一个函数,用于对给定的 url 举行剖析,具体的代码实现如下:
  1. package main
  2. import (
  3.         "fmt"
  4.         "io"
  5.         "net/http"
  6. )
  7. func retrieve(url string) string {
  8.         resp, err := http.Get(url)
  9.         if err != nil {
  10.                 panic(err)
  11.         }
  12.         defer resp.Body.Close()
  13.         bytes, _ := io.ReadAll(resp.Body)
  14.         return string(bytes)
  15. }
  16. func main() {
  17.         url := "https://www.baidu.com"
  18.         fmt.Println(retrieve(url))
  19. }
复制代码
现在我们假设在写一个较大的工程,有一个专门负责网络架构的团队来完成网络哀求、磁盘读写等需求的实现,保存在目次 infra 下,其中实现了一个 Retriever 保存在 urlretriever.go 文件下。
Retriever 的结构和方法的具体实现如下:
  1. package infra
  2. import (
  3.         "io"
  4.         "net/http"
  5. )
  6. type Retriever struct{}
  7. func (Retriever) Get(url string) string {
  8.         // 接收者在不需要名字的时候可以只写类型
  9.         resp, err := http.Get(url)
  10.         if err != nil {
  11.                 panic(err)
  12.         }
  13.         defer resp.Body.Close()
  14.         bytes, _ := io.ReadAll(resp.Body)
  15.         return string(bytes)
  16. }
复制代码
此时在 main 函数中,只需要新建一个 infra.Retriever 类型并使用其方法 Get 即可完成与上述代码等价的需求:
  1. package main
  2. import (
  3.         "fmt"
  4.         "learngo/infra"
  5. )
  6. func main() {
  7.         retriever := infra.Retriever{}
  8.         url := "https://www.baidu.com"
  9.         fmt.Println(retriever.Get(url)
  10. )
  11. }
复制代码
上述代码还可以进一步地工程化,因为在大型项目当中,可能不止网络开发团队实现了 Retriever,测试团队可能同样开发了 Retriever 用于这部分代码的测试。以是显式的 infra.Retriever{ } 可以使用函数举行代替:
  1. package main
  2. import (
  3.         "fmt"
  4.         "learngo/infra"
  5. )
  6. func getRetriever() infra.Retriever {
  7.         return infra.Retriever{}
  8. }
  9. func main() {
  10.         var retriever infra.Retriever
  11.         retriever = getRetriever()
  12.         url := "https://www.baidu.com"
  13.         fmt.Println(retriever.Get(url)
  14. )
  15. }
复制代码
此时我们留意到,上述代码还是不够好,因为就算将 infra.Retriever{ } 放到函数当中,main 函数当中的 retriever 的类型仍旧是显式或隐式为 infra.Retriever 的。
瑕疵在于,main 函数当中的 retriever 必须是 infra.Retriever 类型的,main 函数与 infra.Retriever 的耦合较深。如果想要解耦,就需要用到 Go 的接口(interface)。
假云云时测试团队的 testing 目次下也有一个 Retriever,它同样有一个 Get 方法,但是举动与 infra 的 Retriever 完全不同,此时我们无法对 main 文件当中的 getRetriever 函数的返回类型举行修改。或者说,更换一个 Retriever,需要修改很多个地方,造成了很多工作量上的冗余。
  1. package main
  2. import (
  3.         "fmt"
  4.         "learngo/testing"
  5. )
  6. func getRetriever() testing.Retriever {
  7.         return testing.Retriever{}
  8. }
  9. func main() {
  10.         var retriever testing.Retriever
  11.         retriever = getRetriever()
  12.         url := "https://www.baidu.com"
  13.         fmt.Println(retriever.Get(url)
  14. )
  15. }
复制代码
产生上述我们不满足的情况的缘故起因是,Golang 是一个强类型的语言(或者说静态语言),而不是弱类型或动态绑定的系统,在写代码时,编译阶段我们就已经知道变量的类型,而对于 Python 等动态语言,在运行时才知道类型。
解决上述问题的方案是,让代码与逻辑相一致。在 main 函数中,变量 retriever 的类型不应该强绑定为某个类型的 retriever:
  1. var retriever ?
  2. // ? is something that can get
复制代码
retriever 的类型假设我们此时是不知道的,但是我们需要这个类型具有 Get 方法,才华使后面的:
  1. retriever.Get(url)
复制代码
顺利运行。至于具体的类型是 infra 的 Retriever 还是 testing 的 Retriever,我们不需要关心。这个我们不知道的类型正是接口(interface)。
使用关键字 interface 来界说一个接口,它的声明语句与 struct 的声明非常类似:
  1. type retriever interface {
  2.         Get(string) string
  3. }
复制代码

使用接口的具体实现如下:
  1. package mainimport (        "fmt"        "learngo/testing")func getRetriever() testing.Retriever {        return testing.Retriever{}}type retriever interface {
  2.         Get(string) string
  3. }
  4. func main() {        var r retriever = getRetriever()        url := "https://www.baidu.com"        fmt.Println(r.Get(url))}
复制代码
函数 getRetriever() 的返回值类型也应该与 testing 解耦,直接更换为接口 retriever:
  1. func getRetriever() retriever {
  2.         return testing.Retriever{}        // 返回的仍然是 testing.Retriever{}
  3. }
复制代码
使用 testing 的 Retriever 测试通事后,可以将代码业务上限,此时只需要更换 getRetriever 中的 testing 为 infra 即可。
鸭子类型(duck typing)

鸭子类型可以概括为:走路像鸭子,说话像鸭子,长得像鸭子,那么它就是鸭子。它强调的是形貌事物的外部举动而非内部结构。
严酷说 Go 属于结构化类型系统,类似 Duck Typing。
接口的界说和实现

接口由使用者来界说。

下面是一个示例,此例需要我们新建一个 retriever 目次,并将下述代码写在目次下的 main.go 当中:
  1. package main
  2. import "fmt"
  3. type Retriever interface {
  4.         Get(url string) string
  5. }
  6. func download(r Retriever) string {
  7.         // download 是一个使用者, 使用者要 Get, 因此要定义一个 Retriever 接口
  8.         return r.Get("https://www.baidu.com")
  9. }
  10. func main() {
  11.         var r Retriever
  12.         fmt.Println(download(r))
  13. }
复制代码
但是上述程序还无法直接运行,因为 r 还没有一个具体的实现。
在 retriever 目次下新建 mock 目次,并在 mock 目次下界说 mockretriever.go:
  1. package mock
  2. type Retriever struct {
  3.         Contents string
  4. }
  5. func (r Retriever) Get(url string) string {
  6.         return r.Contents
  7. }
复制代码
编译器会发现我们界说的是可以用于接口实现的结构:

此时,修改我们的 main 函数体为:
  1. func main() {
  2.         var r Retriever
  3.         r = mock.Retriever{"this is a fake www.baidu.com
  4. "}
  5.         fmt.Println(download(r))
  6. }
复制代码

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

花瓣小跑

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表