之前介绍的Go中的类型都是具体类型(concrete type),本篇文章介绍Go中的一种抽象类型(abstract type)——接口(interface)。

1. 接口类型

Go的接口不同于其他OOP语言中的接口,Go中的接口概念上非常的简单——Go中的接口定义了一系列的方法,只要某种具体的类型拥有这些方法,我们就说这种类型满足(satisfy)这个接口或者说这个类型是接口的一个实例(instance)。原话是这样:

A type satisies an interface if it possesses all the methods the interface requires.

看下面的例子:


package io

// 定义一个Reader接口类型(有一个Read方法)
type Reader interface {
    Read(p []byte) (n int, err error)
}

// 定义一个Writer接口类型(有一个Write方法)
type Writer interface {
    Write(p []byte] (n int, err error)
}

// 定义一个Closer接口类型(有一个Close方法)
type Closer interface {
    Close() error
}

// 定义一个ReadWriter接口类型(有两个方法)
type ReadWriter interface {
    Reader
    Writer
}

// 定义一个ReadWriteCloser接口类型(有三个方法)
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

var w io.Writer
w = os.Stdout    // OK,os.Stdout是*os.File类型,该类型有Write方法
w = new(bytes.Buffer)  // OK,*bytes.Buffer有Writer方法
w = time.Second   // Compile error:time.Duration lack Write method

var roc io.ReadWriteCloser
rwc = os.Stdout    // OK,*os.File有Read、Write、Close方法
rwc = new(bytes.Buffer)    // Compile error:*bytes.Buffer没有Close方法

w = rwc    // OK: io.ReadWriteCloser有Write方法
rwc = w    // Compile error:io.Writer lacks Close方法

我们再来看一个特殊的接口:空接口(empty interface):interface{}。空接口没有任何方法,所以根据接口的定义和要求我们得出任何类型都都是满足空接口的。换句话说,我们可以把任何类型的值赋给空接口(有点像很多OOP中最顶层的类型,比如Java中的Object类)。

var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one", 1}
any = new(bytes.Buffer)

空接口的这个特性在函数传参使用的非常广泛。

2. Interface Values

接口值(Interface Values)由两部分组成:动态类型(dynamic type)和动态值(dynamic value)(以下简称类型和值)。接口的零值(nil)指类型和值都为nil。两个接口值相等的条件是都为nil或者他们的类型与值都相同。

我们看几个例子:

例子1:

var w io.Writer   // io.Writer是一个接口类型

我们声明了一个io.Writer类型的变量w,此时w的值为被默认初始化为接口的零值,即类型和值都是nil。

例子2:

w = os.Stdout

这里我们将类型为os.File类型的os.Stdout赋给了w,此时w的类型为os.File,值为os.Stdout。

例子3:

w = new(bytes.Buffer)

这里我们w的类型为*bytes.Buffer,值为[]byte

例子4:

w = nil

这个赋值语句将w的类型和值都变成了nil,即接口的零值。

Interface Values在实际使用中有一个非常容易犯错的地方:

An Interface Containing a Nil Pointers is Non-Nil.

其实就是我们要注意区分值和类型都为nil的接口和值为nil但类型不为nil的情况。看个例子

const debug = true

func main() {
    var buf *bytes.Buffer
    if debug {
        buf = new(bytes.Buffer)
    }
    f(buf)    // error...
    if debug {
        // ...use buf ...
    }
}

// If out is non-nil, output will be written to it
func f(out io.Writer) {
    // ... do something ...
    if out != nil {
        out.Write([]byte("done!\n"))
    }

}

上面这个例子是如果打开debug(debug置为true),我们就增加一些日志输出。但是有一个bug,就是当我们将debug置为false的时候,不会执行new语句,这样变量buf(指向bytes.Buffer接口的指针)的类型是bytes.Buffer,值是nil。所以buf!=nil,但因为值为nil,所以后面调用Write方法的时候会panic。

3. Type Assertions

类型断言操作只只适用于Interface Values,语法格式为:

x.(T)    

x为Interface Values或可以产生Interface Value的表达式。T为类型(称为断言类型“asserted type”),这里有两种情况:

  1. 如果T是某种具体的类型,那么类型断言就检查x的类型和T是不是一致,如果一致,则断言的结果值是x的动态值,类型是T。如果不一致,程序就panic。

    var w io.Writer
    w = os.Stdout
    f := w.(*os.File)    // success: f == os.Stdout
    c := w.(*bytes.Buffer)    // panic: interface holds *os.File, not *bytes.Buffer
  2. 如果T是接口类型,则类型断言检查x的动态类型是不是满足(satisfies)T。如果满足,则x依旧是接口类型,但是其类型变为T的类型(即拥有了更多的方法)。如果不满足,则程序panic。

    var w io.Writer
    w = os.Stdout
    rw := w.(io.ReadWriter) // success:*os.File has both Read and file
    w = new(ByteCounter)    // ByteCounter has only Write Method 
    rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read Method

很显然,如果类型不一致,程序就panic在很多场景都是不可以接受的。所以类型断言提供了“comma, ok”机制:

var w io.Writer = os.Stdout
f, ok := w.(*os.File)    // success: f=os.Stdout, ok = true
b, ok := w.(*bytes.Buffer)    // fail: f=nil, ok = false

成功的时候,第二个返回值(bool类型)为true,失败的话为false。

4. Type Switch

其实Type Switch和Type Assertions比较类似,比如我们拿到一个interface{}类型的值,如果我们想知道它具体是哪一种类型,那我们就可以写很多if-else分支,利用Type Assertions里面介绍的"comma, ok"机制去一种一种试,直到ok为true为止。但这样写起来太麻烦了,所以就有了Type Switch,它可以让我们使用switch的方式去实现上面的功能。语法格式如下:

switch x.(type) {    // type是关键字
case T1:    
    // do something
case T2:    
    // do something
...

case Tn:
    // do something

default:
    // do something
}

看个例子:

package main

import (
    "fmt"
)

func exp(x interface{}) string {
    switch x := x.(type) {
    case nil:
        return "NULL"
    case int, uint:
        return fmt.Sprintf("%d", x)
    case bool:
        if x {
            return "True"
        }
        return "False"
    case string:
        return "String"
    default:
        return fmt.Sprintf("unexpected type %T: %v", x, x)
    }
}

func main() {
    var a interface{}

    fmt.Println(exp(a))

    a = 5
    fmt.Println(exp(a))

    a = true
    fmt.Println(exp(a))

    a = "I am string"
    fmt.Println(exp(a))

    a = 3.14
    fmt.Println(exp(a))
}

输出为:

NULL
5
True
String
unexpected type float64: 3.14
[Finished in 0.3s]

关于Type Switch有几个注意点:

  • 因为一个值可能符合多个类型,所以需要注意case语句的顺序,default语句的位置无所谓。
  • 不允许使用fallthrough

Interface对于OOP编程是比较重要的,但是概念上还是比较简单的,本文主要是介绍了一些基本的概念,更多更深入的了解可以查看Golang官方的文档和博客。

PS:这篇博客10.8号写了一点后,直到今天又才接着写完,拖延症太可怕o(╯□╰)o...