《Go语言圣经》——6. 接口

26 阅读5分钟

接口的目的是什么?

在谈论go语言的接口之前,我们需要搞清楚为什么要有接口?先说结论:接口的目的还是复用代码,避免代码膨胀。
说到接口,不得不提到多态。简单来说,多态度就是:对于某个行为,我们希望不同的对象去完成时会产生不同的状态或动作,且最好能通过同一个形参来传入。
多态的实现有不同的方式,例如函数的重载我也把它理解为一种多态,传入同一个函数,编译器会根据形参帮我们选择最合适的函数去执行。

//通过函数的重载来实现多态
int add(int a, int b) {
    return a + b;
}

double add(int a, int b) {
    return a + b;
}

这种实现方式在某种程度上完成了我们的预期:对于不同的对象有不同的动作。但很明显代码量很大,且不容易维护。于是模板就诞生了:

template <typename T>
auto add(T a, T b) -> dectype(a+b) {
    return a + b;
}

编译器会在编译器根据我们传入的参数生成特定类型的代码,既实现了多态也避免了代码量过多。这种多态我们称之为编译器多态。
在C++中,通过继承实现的多态为运行期多态,因为具体的类型需要到运行的时候才能确定。多态发生的三个充分必要条件:向上转型、虚函数、指针/引用。子类可以当作传入形参为父类的函数,当通过指针或引用传递且调用的是虚函数的时候。运行期间会根据指针/引用的动态类型调用相应的虚函数,由此实现了多态。

接口约定

在go语言中,接口是一种抽象类型,它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合。以我们常用的fmt.Printf和 fmt.Sprintf函数为例子,它实际调用的是Fprintf

func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)

可以看到Fprintf函数的第一个形参是io.Writer,而fmt.Printf和fmt.Sprintf函数的第一个参数为:

func Printf(format string, args ...interface{}) (int, error) { 
    return Fprintf(os.Stdout, format, args...) 
} 
func Sprintf(format string, args ...interface{}) string { 
    var buf bytes.Buffer 
    Fprintf(&buf, format, args...) 
    return buf.String() 
}

可以看到一个是*os.File类型,一个是bytes.Buffer类型,但它们都可以作实参传递,这就是一种多态。这是因为io.Writer类型是一个接口类型,定义如下:

package io

type Writer interface {
    Wrrite(p []byte) (n int, err error)
}

因为fmt.Fprintf函数没有对具体操作的值做任何假设,而是通过接口的约定来保证行为,所以第一个参数可以安全地传入任何满足io.Writer接口的值。

接口类型

接口类型描述了一系列方法的集合,而一个实现了这些方法的具体类型是这个接口类型的实例。 io包中有很多常用的接口类型,例如Reader可以代表任意可以读取bytes的类型,Closer是任意可以关闭的值。

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

type Closer interface {
    Close() error
}

并且不同的类型可以组合(Go语言没有继承,它提倡使用组合而不是继承)。

type ReadWriter interface {
    Reader 
    Writer
}

type ReadWriterCloser interface {
    Reader
    Writer
    Closer
}

以上是简写形式,并没有把接口的包含的方法名写全,但语法上不影响。

实现接口的条件

go接口的实现是隐式的,如果一个类型拥有一个接口需要的所有方法,他么这个类型就实现了这个接口。例如*io.File类型实现了io.Reader,那它就可以作为实参传递给相应的接口类型。例如:

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)

var rwc io.ReadWriteCloser
rwc = os.Stdout

go语言中,对于指针和值类型,编译器会帮我们加上取地址符或者解引用,这算是一个语法糖。但需要注意:

type Incer interface {
    Inc()
}

type Number struct {
    num int
}

func (n *Number) Inc() {
    n.num++
}

func main() {
    var i Incer
    var num = Number{1}
    //这里必须加上取地址符号,因为方法的接受者是指针而不是值
    i = &num
    i.Inc()
    fmt.Println(i)
}

http.Handler接口

package http

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

//该方法需要传入一个Handler类型的处理器来处理发来的请求
func ListenAndServe(address string, h Handler) error

ListenAndServer会启动一个服务器,同时使用传入的Handler来处理发来的请求。假设用户可以请求该网站获取物品的价格,我们定义一个名为databasemap类型,并为该类型实现一个ServeHttp方法,这样该类型就满足了http.Handler接口。

func main() {
    db := database{"shoes": 50, "socks": 5}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

type dollars float32

func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }

type database map[string]dollars

//database实现了Handler的接口,则它可视为一个Handler类型
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

启动服务后,我们使用curl模拟get请求: image.png 可以看到发往指定端口的请求由我们自定义的函数完成了相应的响应。那么对于不同的URL,触发不同的行为呢,也就是网站的不同页面呢。

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    switch req.URL.Path {
    case "/list":
        for item, price := range db {
            fmt.Fprintf(w, "%s: %s\n", item, price)
        }
    case "/price":
        item := req.URL.Query().Get("item")
        price, ok := db[item]
        if !ok {
            w.WriteHeader(http.StatusNotFound) // 404
            fmt.Fprintf(w, "no such item: %q\n", item)
            return
        }
        fmt.Fprintf(w, "%s\n", price)
    default:
        w.WriteHeader(http.StatusNotFound) // 404
        fmt.Fprintf(w, "no such page: %s\n", req.URL)
    }
}

image.png

error接口

类型断言

类型断言就是字面意思,判断一个静态类型为接口类型的变量是否是特定的动态类型,进而做出不同的动作。例子如下:

package main

import "fmt"

type animal interface {
    speak()
}

type cat struct {
    name string
}

func (c cat) speak() {
    fmt.Println("cat speak")
}

type dog struct {
    name string
}

func (d dog) speak() {
    fmt.Println("dog speak")
}

func animalSpeck(a animal) {
    if t, ok := a.(cat); ok {
       fmt.Println("cat", t.name)
       return
    }
    if t2, ok := a.(dog); ok {
       fmt.Println("dog", t2.name)
       return
    }
}

func main() {
    var a animal
    a = cat{"mimi"}
    animalSpeck(a)  // cat mimi
    a = dog{"baga"}
    animalSpeck(a)  // dog baga
}