Go语言 接口

799 阅读5分钟

为啥要使用接口?

先看下 一个简单的程序, 输入一个url,我们输出对应的response

import (
   "fmt"
   "io/ioutil"
   "net/http"
)

func retrieve(url string) string{
   res, err := http.Get(url)
   if err != nil {
      panic(err)
   }
   defer res.Body.Close()

   bytes, _ := ioutil.ReadAll(res.Body)
   return string(bytes)
}


func main() {
   fmt.Println(retrieve("https://www.baidu.com"))
}

逻辑上是完全没有问题的,有没有更好的写法? 毕竟这里main函数和我们的解析url的函数 强耦合了在一起

改进一版:

image.png

package infra

import (
   "io/ioutil"
   "net/http"
)

type Retriever struct {

}

func (Retriever) Get(url string) string  {
   res, err := http.Get(url)
   if err != nil {
      panic(err)
   }
   defer res.Body.Close()

   bytes, _ := ioutil.ReadAll(res.Body)
   return string(bytes)
}
package main

import (
   "fmt"
   "gomodtest/infra"
)

func getRetriever() infra.Retriever{
   return infra.Retriever{}
}



func main() {
   retriever := getRetriever()
   fmt.Println(retriever.Get("https://www.baidu.com"))
}

到这里 看起来 比我们第一版的程序要好很多了。

假设这个时候 我们要找测试团队来测试一下 他们也要有一个retriever

image.png

既然是测试团队 那我们就暂时给个假的response 字符串

package testing

type Retriever struct {

}

func (Retriever) Get(url string) string  {
   return "fake content"
}

这个时候你就会发现 你的main函数很难写了 你得到main函数里面 修改你的getRetriever方法,而且你改了return还不行 还要修改方法的返回值。 这个时候你就会发现了 你的代码还是有耦合

那这个时候呢,如果你是从java 语言转过来的 就很容易能猜到 用接口来解决就行了。

简单看下 go语言中 接口如何使用

func getRetriever() retriever{
   return testing.Retriever{}
}

type retriever interface {
   Get(string) string
}

func main() {
   retriever := getRetriever()
   fmt.Println(retriever.Get("https://www.baidu.com"))
}

就很简单,定义一个interface 然后这个interface 也有Get的方法 即可。 这样对于上面的需求来说

我们在测试的时候只要修改我们的return 方法里的实现 即可,对于外部那些调用的地方 则无需任何修改

这就算一次基本成功的解耦了,也是接口最大的作用

duck typing

前面的章节 有java经验的同学 可能会感到疑惑,你只是定义了一个接口 但是你的实现 并没有实现对应的接口啊 这是咋回事, 这是go接口和java接口的不同。

严格来说 go 属于结构化类型系统,类似于 duck typing(具体的duck typing 可以自行百度搜索 我就不copu了)

go的interface 是描述事物的外部行为而非内部结构

go语言中的接口 是由 使用者定义的

传统的面向对象 比如java,接口一般都是由 提供者 也就是被使用者来定义的,这与go语言是截然不同的,在使用go语言的过程中 一定要注意这个思维方式的转变

所以 对于前面章节的需求,我们就按照 go语言的接口 编写思路来实现一遍:

image.png

可以看出来 download函数 要使用一个retriever ,这个retriever有啥特点?她得有一个get函数对吧

那我们就按照这个思路 定义一个:

image.png

有了这个接口 ,提供者就可以根据这个接口随意发挥了,比如有个mock服务

image.png

调用者:

func main() {
   fmt.Println(download(mock.Retriever{
       Contents: "this is mock",
   }))
}

可以看出来,go语言中 接口的实现 是只要实现方法就可以,所以go语言中接口的实现是隐式的,她不需要明确说我实现了哪个接口,而只要实现 接口里的方法即可

这是go和java 在接口实现上最大的不同

接口的值类型

紧接着上面的类型 我们可以扩展一个website的retriever,然后稍微修改一下我们的代码,看看这个接口的retriver到底是个啥

func main() {
   var r Retriever
   r = mock.Retriever{
      Contents: "this is mock",
   }
   fmt.Printf("%T %v\n",r,r)
   fmt.Println(download(r))

   r = website.Retriever{
      Domain: "baidu",
      Protol: "https",
   }
   fmt.Printf("%T %v\n",r,r)
   fmt.Println(download(r))

}

image.png

可以看出来 这里 r 是一个值类型, 那有人就会问了,值类型 不是会涉及到拷贝吗 这里能否是个指针呢? 当然也是可以的,而且修改起来很简单。

直接修改我们的接口实现:

image.png

然后再改一下我们接口的赋值

image.png

运行一下程序:

image.png

到这里就可以看出来

go语言的interface这个类型,里面既可以接收一个值类型,也可以接收一个指针类型。

和java 一样 我们也可以 直接拿到具体的类型:

这里一定要注意写法了:

func inspect(r Retriever){
   switch r.(type) {
   case mock.Retriever:
      fmt.Println("this is mock retriever")
   case *website.Retriever:
      fmt.Println("this is website retriever")
   }
}

一定要谨记 go语言中的interface 肚子里有2个东西 :实现者的类型 和 (实现者的指针 或者是 实现者的值)

**接口变量自带指针 **

interface{} 类型

这个语法 就代表任意类型

func printAny( r interface{}){
   fmt.Println(r)
}

当然也可以从任意类型中 做强制类型转换

func addAny( r interface{},q interface{}) int{
   return r.(int)+q.(int)
}

不过这种写法就没办法在编译时发现错误了,只有到运行时才能发现错误。

接口的组合

这也是go语言 与java语言的一个不同之处,在java中 接口和接口是不能组合在一起的。这在某种时候 会给我们带来一些困扰。

比如在java中 你写了一个函数 参数是一个interface Jump, 假设你想让这个函数的参数 也可以是一个interface Run 那就压根没办法做到了。

但是在go语言中 这是可以的

比如说

type Jump interface {
   JumpIn()
}

type Run interface {
   RunIn()
}

type RunAndJump interface {
   Jump
   Run
   Happy()
}

除了定义了跑和跳以外 我们还定义了 RunAndJump 她具备跑和跳的特性 另外还有一个happy函数

再定义一个函数

func printInfo(rq RunAndJump){
   rq.RunIn()
   rq.JumpIn()
   rq.Happy()
}

注意参数就变成了 这个接口类型了

接下来我们来定义个结构体,注意这个结构体 要同时实现这三个方法 才算 是RunAndJump 的类型

package test

import "fmt"

type Person struct {
   Name string
}

func (q Person) JumpIn() {
   fmt.Println(q.Name + " jump")
}

func (q Person) RunIn() {
   fmt.Println(q.Name + " run")
}

func (q Person) Happy() {
   fmt.Println(q.Name + " is happy")
}

最后看下main函数

func main() {
   printInfo(test.Person{
      Name: "wuyue",
   })
}

image.png

最后提一下,go语言的库函数中 提供了非常多的有用的接口 比如reader writer Stringer 等等

有兴趣的可以看看 这些接口 然后在自己的代码中 也实现对应的方法,就可以适配很多go语言提供的基础函数了。