花瓣小跑 发表于 2024-12-11 11:36:25

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

接口

接口的概念

现在我们要实现一个函数,用于对给定的 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 方法,才华使后面的:
retriever.Get(url)
顺利运行。至于具体的类型是 infra 的 Retriever 还是 testing 的 Retriever,我们不需要关心。这个我们不知道的类型正是接口(interface)。
使用关键字 interface 来界说一个接口,它的声明语句与 struct 的声明非常类似:
type retriever interface {
        Get(string) string
}
https://i-blog.csdnimg.cn/direct/b286d63279724beca3c65b658e5d7465.png#pic_center
使用接口的具体实现如下:
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。
接口的界说和实现

接口由使用者来界说。
https://i-blog.csdnimg.cn/direct/e70a5fc25e084dbe94295cba9d44b8e9.png#pic_center
下面是一个示例,此例需要我们新建一个 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
}

编译器会发现我们界说的是可以用于接口实现的结构:
https://i-blog.csdnimg.cn/direct/8792a29cfb6241619720cf851b298194.png#pic_center
此时,修改我们的 main 函数体为:
func main() {
        var r Retriever
        r = mock.Retriever{"this is a fake www.baidu.com
"}
        fmt.Println(download(r))
}
页: [1]
查看完整版本: 【Golang】Go语言编程头脑(一):接口