054-接口(Interface)

历尽千辛万苦,我们越来越接近 Golang 的精髓了。从这一章开始,你需要学习 Go 语言中另一个非常重要的知识点——接口。

1. 接口为何物

首先,我们用几句话简单描述下接口:

  • 一种类型
  • 抽象类型
  • 一种约定

如果非要加上一个类比的话,接口就像是 C++ 中的抽象类(纯虚函数),像 Java 中的 interface 接口。

接口最重要的特点就是约定。它主要用来描述一系列特定的行为,你没有必须知道这个接口是什么,但是你知道接口可以干什么。

说了这么多让人感觉很抽象的话,估计你会犯晕。我们来个具体的例子来看看接口是如何描述特定行为的。

在我们学习接口之前所遇到的所有类型,都是具体类型。比如你在上一篇文章写的 IntSet 类型,它就是一个具体类型。你知道 IntSet 类型是什么,也知道它拥有哪些方法。还记得我们之前实现过哪些方法吧?有 AddUnionWithString 这些方法。

而接口类型,比如下面这样:

type Stringer interface {
    String() string
}

上面定义的 Stringer 就是一个接口类型,它是抽象的,你并不知道这个 Stringer 到底是什么,但是你只知道 Stringer 可以做什么。再进一步的说,你可以说 IntSet 实现了 Stringer 这个接口。

如果你学习过 C++ 的纯虚函数,你就能非常轻松的理解 Go 中的 interface 是目的。但是 Go 中的 interface 特性远不止这些了,我们需要一步一步深挖它。

2. 牛刀小试

来吧,废话不多说,先来写个小例子。

package main

import "fmt"

// 发出叫声的东西
type Caller interface {
    Call() string
}

type Dog struct{}
type Cat struct{}

func (d *Dog) Call() string {
    return "Wang wang..."
}

// 我们故意让 Dog 的 Call 方法接收器为指针
// 而让 Cat 的 Call 方法接收器为普通类型
func (c Cat) Call() string {
    return "Mew mew..."
}

func AnimalCall(caller Caller) {
    fmt.Printf("This animal call: %s\n", caller.Call())
}

func main() {
    var d Dog
    var c Cat

    AnimalCall(&d) // Output: This animal call: Wang wang...
    AnimalCall(c)  // Output: This animal call: Mew mew...
}

这个例子非常简单,AnimalCall 函数的参数是一个接口类型,AnimalCall 函数根本不想关心从接口传递进来的是什么东西,它只关心这个东西有没有 Call 方法。

上面实现了两个不同的具体类型,一只 Dog,一只 Cat,不过这两只小动物的 Call 方法定义上有所区别,Dog 的接收器是指针而 Cat 的接收器是普通对象。因此最后再给 AnimalCall 传递参数的时候,需要特别注意。

正如上面看到的,d 对象并没有 Call 方法,如果调用 AnimalCall(d) 将会报错。

3. 接口定义

正如我们第 2 节所看到的那样,定义一个接口,只要定义它的行为即可,你无需为其定义具体的实现:

type name interface {
    Action1(arg-list) return-list
    Action2(arg-list) return-list
    ...
}

一个接口里,可以定义 0 个到多个行为。下面是一个空接口:

type Empty interface{}

下面是你之前经常见到的 Writer 接口,Reader 接口和 Closer 接口:

package io
type Writer interface {
    Write(p []byte) (n int, err error)
}
type Reader interface {
    Reader(p []byte) (n int, err error)
}
type Closer interface {
    Close() error
}

你应该还见过这样的接口,还记得 http.Response.Body 吗?

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

不过在 Go 里,你可以将多个接口进行组合,因此 ReadCloser 接口可以这样写,更加方便:

type ReadCloser interface {
    Reader
    Closer
}

甚至你可以写成这种混合的风格(就是各种炫技啊):

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

同样的,还有 ReadWriter 接口:

type ReadWriter interface {
    Reader
    Writer
}

4. 实现接口

第 2 小节,我们已经实现了一个简单的 Caller 接口。现在我们来具体讲讲怎样才算实现了一个接口。规则非常简单,只要类型实现了接口定义的所有方法,就算实现了接口。比较特殊的是,任意具体类型都实现了空接口 interface{}.

Golang 不需要像 C++ 和 Java 那样显式的去继承一个接口,它是隐式满足的(satisfied implicitly),只要具体的类型定义了接口所有的方法就行。

比如 *os.File 实现了 io.Reader, io.Writerio.Closer 接口,这也意味着 *os.File 也一定实现了 io.ReadWriter 接口。

需要注意的是,os.File 并没有实现这些接口,我们要严格的区分 *os.Fileos.File,到底是谁实现了接口,这是完全不同的。正如第 2 节中的那样,*Dog 实现了 Call 方法,而不是 Dog 实现了 Call 方法。

一旦某种类型实现了某个接口,那么就能将此类型的值赋值给接口对象:

var w io.Writer
w = os.Stdout // os.Stdout 是 *os.File 类型
w = new(bytes.Buffer) // *bytes.Buffer 实现了 io.Writer 接口
w = 100 // 编译错误,100 是 int 类型,未实现任何接口

实际上, w 表达式的右边不仅可以是具体类型,也可以是接口类型。比如:

var rw io.ReadWriter
w = rw // OK
rw = w // Not OK! 编译错误,因为 w 没有 Read 方法。

刚刚说过,任意具体类型都实现了空接口,因此你可以将任意类型赋值给空接口:

var e interface{}
e = 100 // OK
e = "hello world" // OK
e = os.Stdout // OK

这是一个很有用的特性,一个最常见的例子就是 fmt.Printf 函数:

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

5. 接口

  • 掌握接口定义方法
  • 掌握如何实现接口

猜你喜欢

转载自blog.csdn.net/q1007729991/article/details/80555590