【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]