Golang 中的 interface(一)

interface

interface 是一组方法签名,这些方法定义了 interface 的行为。

interface 基本用法

// 只定义方法,不定义实现
type Traversal interface {
    Traverse() // 注意:没有 func 关键字
}

为什么需要用到 interface

我们想实现一个功能,通过给定一个 url 爬取一段网页内容:

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

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

func main() {
    fmt.Println(Get("https://li2.xyz"))
}

上面代码中有一个缺点,那就是main 函数和 Get 函数是非常割裂的,两者关心的东西是不一样的。为了解决两者的耦合,就引入了接口(interface)的概念。

通过 interface 解决上面代码的耦合

// infra.go
type Retriever struct {}
func(Retriever) Get(url string) string {
    // 同上面的 Get
    // ...
}

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

// main.go
func main() {
    var r infra.Retriever = getRetriever()
    r.Get("https://li2.xyz")
}

上面代码中依旧存在瑕疵,其中 r 的类型一定是 infra.Retriever,而不能为其他类型。比如说我们有一个测试团队,他们想要通过 mock 来实现 Reteriever 功能,他们就自己实现了一套test.Retriever,那么就会出现问题: 即我们需要把上面的 infra.Retriever 全部手动改为 test.Retriever:

// test.go
type Retriever struct{
    Contents string
}
// returns fake content
func (r Retriever) Get(url string) string {
    return r.Contents
}
func getRetriever () test.Retriever {
    return Retriever{}
}
// main.go
func main() {
    var r test.Retriever = getRetriever()
    r.Get()
}

test 同样实现了一个 Retriever的时候,其中 r 的类型就要变成 test.Retriever 类型,这种手动修改是非常不妥的。 可以观察一下上面的代码:

var r test.Retriever = getRetriever()

r 的类型必须是固定,开发团队使用的时候 r = infra.Retriver, 测试团队使用的时候 r = test.Retriever,那么能不能把 r 的类型抽象化一下,当使用的时候才确定类型呢?比如抽象成:

var r <?> = getRetriever()

其中 <?> 这个类型就是 interface 了,即:

type retriever interface {
    Get(string) string
}

改造一下上面的功能就是:

// 省略部分代码...
type retriever interface(){
    Get(string) string
}
func getRetriever() reteriever{
    return test.Retriever{}
    // return infra.Retriever{}
}
func main() {
    var r retriever = getRetriever()
    r.Get() // 此时是 test.Retriever
    // r.Get("https://li2.xyz") 此时是 infra.Retriever
}

以上代码中我们只需要关心 getRetriever 返回的类型即可。 上面的代码还没只有足够好,我们下面接着看。

Go 语言中的 Duck Typing(鸭子类型)

先介绍一下 “鸭子类型”。
duck typing 概念:

  • "叫起来像鸭子,走路像鸭子,那么就是鸭子"
  • 描述事物的外部行为而非内部结构
  • 动态类型的一种风格

INFO

由于 golang 编译时绑定,属于结构化类型系统,严格意义上来说并非 duck typing,但类似 duck typing

先看一下 JavaScript 中的 duck typing 用法:

const googleMap = {
    show(){
        console.log('this is google map result')
    }
}
const gaodeMap = {
    show(){
        console.log('this is gaode map result')
    }
}

const renderMap = (map) => {
    map?.show()
}
renderMap(googleMap) // this is google map result
renderMap(gaodeMap) // this is gaode map result

上面这段代码中 googleMapgaodeMap都拥有 show 方法,那我么可以默认为两者都是 Map

golang 中 duck typing 的特点

  • 具有动态语言(Python、JS) duck typing 的灵活性
  • 拥有静态语言(Java)的类型检查
  • 实现多接口组装,如Writer Reader Closer ReadWriteCloser,可参考 /usr/local/go/src/io/io.go 中的源码

Go 中 interface 的定义

其中有两个角色:

  • 实现者: retriever
  • 使用者: download 上面两个角色的关系可以看作是:使用者 定义 接口

接口由使用者定义

看下使用者如何定义接口:

// main.go
// 使用者
type Retriever interface {
	Get(url string) string
}

func download(r Retriever) string {
	return r.Get("https://www.baidu.com")
}
// mock.go
// 实现者:只要实现 Get 方法即可
type Retriever struct {
    Contents string
}

func (r *Retriever) Get(url string) string {
    return r.Contents
}
// main.go
// usuage
func main() {
    r := mock.Retriever{"this is mock data"}
    fmt.Printf("%s:", download(r))
}

上面这个例子中,使用者(download)规定 retriever 接口有 Get 方法,实现者不关心是哪个接口,只关心实现接口中的方法即可。

如果一个类型实现了一个 interface 中所有方法,我们说类型实现了该 interface

上面例子中 Retriever struct 实现了 Retriever interface,那么可以说 Retriever structRetriever interface 是兼容的,看一个简单的例子:

type GetSetName interface {
	Get() string
	Set(string)
}

type Person struct {
	name string
}

func (p *Person) Get() string {
	return p.name
}
func (p *Person) Set(name string) {
	p.name = name
}

func showName() {
	shinji := Person{name: "Ikari"}
	fmt.Println(shinji.Get())
	shinji.Set("Ikari")
	fmt.Println(shinji.Get())
}
func showName2(g GetSetName) {
	fmt.Println(g.Get())
	shinji := g
	fmt.Println(shinji.Get())
	shinji.Set("Ikari")
	fmt.Println(shinji.Get())
}
func main() {
	showName()
	showName2(&Person{
		name: "Ayanami",
	})
}

上面代码中 showName2 方法需要的是一个 interface 类型的数据结构,但实际上我们传入的是一个 Person 类型的 struct

接口的实现的特点

  • 接口的实现是隐士的
  • 只要实现接口里的方法