Go语言接口(Golang - Interface)

Duck Typing

说Go的接口就必须先了解duck typing的概念。很多其他的语言都支持这个概念。

首先上图的这个东西是鸭子吗?

鸭子的定义:

鸭子 英文名称:Duck。脊索动物门,脊椎动物亚门,鸟纲雁形目鸭科鸭属动物,是由野生绿头鸭斑嘴鸭驯化而来。是一种常见家禽。鸭是雁形目鸭科鸭亚科水禽的统称。是一种水、陆两栖动物。但不能在水中待太久,是卵生动物。中文学名鸭界动物界门脊索动物门亚 门脊椎动物亚门纲鸟纲亚 纲今鸟亚纲目雁形目

按照传统的定义,上面这个图肯定不是鸭子。但是按照duck typing的定义来说,上图这个东西是一只鸭子。因为他长得像鸭子。Duck Typing关注的是描述事物的外部行为而非内部结构。

但是严格来说Duck Typing的定义中要求动态绑定,但是Go是编译就绑定了,所以Go只能说是类似Duck Typing。

Python和C++中的Duck Typing

def download(retriever);
    return retriever.get("www.google.com")

只有在运行的时候才知道传入的retriever有没有get,如果没有get方法,download的就会报错,但是如果download和retriever是两组不同的人开发。传入download的retriever必须有get方法这个信息通常会写在注释里

同样C++也有类似的写法,然后C++可以做到编译的时候知道传入的retriever有没有get。同时这个信息还是需要写在注释里。

JAVA中的类似的东西

<R extends Retriever>
String download(R r){
    return r.get("www.google.com")
}

JAVA的这种做法确实很安全,传入的参数必须实现Retriever,也不用写注释了,也不会编译错误了。但是这已经不是Duck Typing,因为你必须实现Retriever接口。导致download不能实现多个接口。(比如read和write)

Go的Duck Typing

兼备Python的灵活性和JAVA的类型检查

接口的定义

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

使用者

User
type Retriever interface {
	Get(url string) string
}
func download(r Retriever) string {
	return r.Get(url)
}

实现者

Implementor
type Retriever struct {
	Contents string
}
func (r *Retriever) Get(url string) string {
	return r.Contents
}

实际使用的时候

User
func main() {
    r := mock.Retriever{"test"}
    fmt.print(download(r))
}

接口的值类型

接口内部

接口内部可以有实现者的类型和实现者

type Retriever struct {
	Contents string
}

func (r Retriever) String() string {
	return fmt.Sprintf(
		"Retriever: {Contents=%s}", r.Contents)
}

也可以用实现者的指针

func (r *Retriever) String() string {
	return fmt.Sprintf(
		"Retriever: {Contents=%s}", r.Contents)
}

检测类型

由于Go语言的所有类型都是值类型,所以在实际使用实现了接口的方法的时候r := mock.Retriever{"test"} 中的r除了保存了值,也保存了实现者的类型。

查看interace的类型有2中方法:

  • Type assertion

if mockRetriever, ok := r.(*mock.Retriever); ok {
		fmt.Println(mockRetriever.Contents)
	} else {
		fmt.Println("r is not a mock retriever")
	}

注意这里由于实现者里的方法是传的指针,所以r.(*mock.Retriever)也需要时指针。不然会报错。

  • Switch

switch v := r.(type) {
case *mock.Retriever:
    fmt.Println("Contents:", v.Contents)
case *real.Retriever:
    fmt.Println("UserAgent:", v.UserAgent)
}
  • 打印类型

fmt.Printf(" > Type:%T Value:%v\n", r, r)

空接口

interface{}代表空接口,可以代表任何类型。

假设有一个队列

type Queue []int

如果定义为int的话当然只能往里面加int,但是如果您定义成空接口,就可以传任何值。

type Queue [] interface{}

但是在往里面你传值的时候,你可以通过方法限定值得类型

func (q *Queue) Push(v int) {
	*q = append(*q, v)
}

这样的话如果q.push("abc")的话在编译的时候就能检测的错误。

还有一种方法是在append的时候做转换。

func (q *Queue) Push(v interface{}) {
	*q = append(*q, v.(int))
}

但是这样做的话就只能在执行的时候才能检查到错误了所以不推荐。

接口的组合

接口内部可以加入其他接口来进行组合。比如很多写入操作的参数都是reader或者writer

type Reader interface {
    Read(p []byte) (n int, err error)
}
type Writer interface {
    Write(p []byte) (n int, err error)
}
type ReadWriter interface {
    Reader
    Writer
}

原生接口的例子

举个原生包的例子,比如写入文件。

使用者fmt.Fprintf接受一个io.writer,io.writer是一个接口,里面定义了一个write方法。

再看作为实现者的os.file构造体里面也实现了一个write方法,按照duck typing的规则,file实现了write方法,所以file也是一个writer,所以file也可以传给fmt.Fprintf

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    file, err := os.Create("result.txt")
    if err != nil {
        log.Fatal("Cannot create file", err)
    }
    defer file.Close()

    fmt.Fprintf(file, "Hello Readers of golangcode.com")
}

Go语言接口的好处

这样好处是显而易见的,我们拥有非常高的灵活性,同时保证在编译前就能检测到错误。尤其是在团队开发的时候,在其他编程语言当中,都是谁提供服务,谁提供接口。你需要调用我的服务,就必须声明你实现了我的接口。

而这在逻辑上实际是说不通的,服务实现者怎么会确切的知道服务使用者的具体需求呢?当需求发生变化的时候,服务实现者就需要考虑使用者的需求,从而设计接口。而从理论上来说,每一个服务的开发人员都应该专注于自己的服务。

而go语言不同。go语言的接口是非侵入式接口,只要调用者本身实现了该接口的全部方法,就默认实现了该接口(事实上也确实是实现了这个接口),而不需要显示的声明实现某个接口。这极大的方便了接口的调用,开发人员不必再需要苦想接口的粒度,只需要专注功能函数的实现就可以了。

Last updated