Go 学习笔记8 - 接口 |Go主题月

206 阅读9分钟

接口类型是对其他类型行为的抽象和概括。接口类型不会定义接口方法中的具体实现,由实现接口的类型去提供具体的实现细节。这样,我们可以写出非常灵活的代码。

golang 的接口不同于其他面向对象的语言,不需要显示指定一个对象实现了一个接口(比如 javaphp等就需要显示指定实现某接口),只要类型包含接口的所有方法,则隐式的实现了该接口。

1.接口的定义与基本操作

package main

import (
	"fmt"
)

// 定义接口
type USB interface {
	Name() string // 接口中的方法
	Connection()
}

type PhoneConnecter struct{
	name string
}

func (pc PhoneConnecter) Name() string{
	return pc.name
}

func (pc PhoneConnecter) Connection() {
	fmt.Println("connection", pc.name)
}

func main() {
	var a USB
	a = PhoneConnecter{name:"PhoneConnecter"}
	a.Connection()
}

2.接口约定

在下面的例子中,AnimalEat() 会打印某动物吃某食物,这里使用了接口,使得 AnimalEat不用关心到底是什么动物、不必关心他们吃的到底是什么食物。新增一种动物时,客户端调用方法也不会更改,仅仅是修改传入的参数。

type Animal interface {
	Animal() string
	Food() string
}

type Cat struct{}

func (Cat) Food() string   { return "猫粮" }
func (Cat) Animal() string { return "猫" }

type Dog struct{}

func (Dog) Food() string   { return "狗粮" }
func (Dog) Animal() string { return "狗" }

func AnimalEat(a Animal) {
	food := Prepare(a)
	fmt.Println(fmt.Sprintf("%s开始吃%s", a.Animal(), food))
}

func Prepare(a Animal) string {
	food := a.Food()
	// 加入蛋黄
	food = food + "、蛋黄"
	// 加入羊奶
	food = food + "、羊奶"
	return food
}

Prepare() 参数是是一个接口,Prepare 没有对具体操作的值做任何假设,而是仅仅通过 Animal 接口的约定来保证行为,只要是满足 Animal 接口的任意类型都能被传入。一个类型可以自由地被另一个满足相同接口的类型替换,被称作可替换性(LSP里氏替换)。

由于不管哪种动物,我们业务都要加入蛋黄、羊奶,这部分重复代码我们封装到 Prepare 函数中,而具体的食物通过接口传入的具体类型来决定。

3.接口类型

接口类型描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。

接口类型可以通过组合来定义新的接口,io 包中定义的接口:

type ReadWriter interface {
    Reader
    Writer
}
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

也可以这样:

// 可以这样
type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

// 还可以这样
type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Writer
}

上面3种定义方式都是一样的效果。方法顺序的变化也没有影响,唯一重要的就是这个集合里面的方法。

4.实现接口的条件

一个类型如果实现了一个接口的所有的方法,那么这个类型就实现了这个接口。

var w io.Writer
w = os.Stdout           // OK: *os.File has Write method
w = new(bytes.Buffer)   // OK: *bytes.Buffer has Write method
w = time.Second         // compile error: time.Duration lacks Write method

var rwc io.ReadWriteCloser
rwc = os.Stdout         // OK: *os.File has Read, Write, Close methods
rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method

os.Stdout 的类型是 *os.File,它实现了 io.Writer,所以可以赋值成功。*bytes.Buffer同理。而 time.Second 没有实现 io.Writer,所以编译报错。

在上一节中我们介绍了方法,对于一个具体类型T,不管是它的方法是指针接收者还是值接收者,我们都可以通过指针或非指针变量调用这个方法。对于非指针变量调用指针接收者方法,编译器隐式的帮我们获取了它的地址,但这并不代表 T 类型的值不拥有所有 *T 指针的方法。

IntSet类型的String方法的接收者是一个指针类型,所以我们不能在一个不能寻址的IntSet值上调用这个方法:

type IntSet struct { /* ... */ }
func (*IntSet) String() string
var _ = IntSet{}.String() // compile error: String requires *IntSet receiver

但是我们可以在一个IntSet值上调用这个方法:

var s IntSet
var _ = s.String() // OK: s is a variable and &s has a String method

然而,由于只有*IntSet类型有String方法,所以也只有*IntSet类型实现了fmt.Stringer接口:

var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s  // compile error: IntSet lacks String method

空接口 interface{}非常特殊,它没有任何方法,所以任何类型都实现了空接口,任何类型都可以赋值给空接口。

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

强制检测一个类型是否实现一个接口的方法:

// _ 下划线代表不定义变量
// 编译器会检测 *bytes.Buffer 是否实现了 io.Writer,未实现编译期就会报错
var _ io.Writer = (*bytes.Buffer)(nil)

5.接口值

接口值,由两个部分组成,一个具体的类型和那个类型的值。它们被称为接口的动态类型和动态值。

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

现在来分析下每一个语句后,w 变量的值和动态行为。

第一个语句定义了变量w:

var w io.Writer

此时 w 是 io.Writer 接口,它是一个零值,它的类型和值得部分都是 nil。

1.png

我们可以通过 w==nil 来判断接口值是否为空。调用一个空接口值上的任意方法都会产生panic:

w.Write([]byte("hello")) // panic: nil pointer dereference

第二个语句将一个*os.File类型的值赋给变量w:

w = os.Stdout

这里 os.Stdout 类型被隐式转换为了一个接口类型,这和显式的使用io.Writer(os.Stdout)是等价的。这时接口的动态类型被设置为 *os.File,它的动态值被设置为 os.Stdout的副本,即一个指向进程的标准输出的 os.File 类型的指针,如图:

image.png

调用该接口值的Write方法,实际调用的是(*os.File).Write方法,这个调用输出“hello”。

第三个语句给接口值赋了一个*bytes.Buffer类型的值

w = new(bytes.Buffer)

现在动态类型是 *bytes.Buffer,动态值是一个指向新分配的缓冲区的指针,如图:

image.png

Write方法的调用也也跟第二个语句一致:

w.Write([]byte("hello")) // writes "hello" to the bytes.Buffers

这次类型描述符是 bytes.Buffer,所以调用了 (*bytes.Buffer).Write 方法,方法的接收者是该缓冲区的地址。这个调用把字符串 “hello” 追加到缓冲区中。

最后,第四个语句将nil赋给了接口值:

w = nil

这个语句把动态类型和动态值都设置为 nil ,把变量 w 恢复到它刚声明时的状态。

我们可以使用 fmt包的 %T 来获取接口值得动态类型:

var w io.Writer
fmt.Printf("%T\n", w) // "<nil>"
w = os.Stdout
fmt.Printf("%T\n", w) // "*os.File"
w = new(bytes.Buffer)
fmt.Printf("%T\n", w) // "*bytes.Buffer"

在 fmt 包内部,使用反射来获取接口动态类型的名称。

注意:一个包含nil指针的接口是一个非空接口

一个空接口值(动态类型和动态值为nil)和一个接口值(动态类型不为nil而动态值为nil)是不一样的。

思考下面的程序。当debug变量设置为true时,main函数会将f函数的输出收集到一个bytes.Buffer类型中。

const debug = true

func main() {
    var buf *bytes.Buffer
    if debug {
        buf = new(bytes.Buffer) // 启用输出收集
    }
    f(buf) // 注意: 这里容易产生错误
    if debug {
        // ...使用 buf...
    }
}

// 如果 out 不是 nil, 那么会向其写入输出的数据
func f(out io.Writer) {
    // ...其他代码...
    if out != nil {
        out.Write([]byte("done!\n"))
    }
}

当设置 debug 为 false 时,我们可能会以为 f 函数里面的 out != nil 执行为 false。这里实际上会引发 panic。

if out != nil {
	out.Write([]byte("done!\n")) // panic:对空指针取引用值
}

当 main 函数调用 f 函数时,传递给 f 的参数是 *bytes.Buffer 的空指针,这时 out 的动态类型是 *bytes.Buffer,而动态值是 nil,out 是一个包含空指针值的非空接口,所以 out != nil 这里执行依然是 true。

动态分发机制决定(*bytes.Buffer).Write的方法会被调用,但是这次的接收者的值是nil。对于一些如*os.File 的类型,nil是一个有效的接收者,但是 *bytes.Buffer 类型则不行。尽管方法会被调用,但是当它尝试访问缓冲区时会发生panic。

尽管一个nil *bytes.Buffer 指针拥有实现这个接口的方法,但它不满足该接口所需的一些行为。它违背了 (*bytes.Buffer).Write的接收者不能为空这个隐含前置条件,所以将 nil 指针赋值给这个接口就是一个错误。解决方案就是将main函数中的变量buf的类型改为io.Writer,因此可以避免一开始就将一个不完整的值赋值给这个接口:

var buf io.Writer // 修改这里的类型
if debug {
    buf = new(bytes.Buffer) // enable collection of output
}

6.类型断言

类型断言是一个使用在接口值上的操作。语法上它看起来像 x.(T) 被称为断言类型,这里 x 表示一个接口类型的表达式,T表示一个类型(断言类型)。一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。

这里有两种可能。第一种,如果断言的类型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

可以通过设置类型断言结果为两个接收参数来检查,避免 panic:

package main

import (
	"fmt"
)

type USB interface {
	Name() string
	Connecter
}
type Connecter interface {
	Connection()
}
type PhoneConnecter struct {
	name string
}

func (pc PhoneConnecter) Name() string {
	return pc.name
}

func (pc PhoneConnecter) Connection() {
	fmt.Println("connection", pc.name)
}

func DisConnect(u USB) {
	if pc, ok := u.(PhoneConnecter); ok {      // 类型断言,使用 ok 来判断
		fmt.Println("DisConnect", pc.name)
		return
	}
	fmt.Println("unknown device")
}

func main() {
	a := PhoneConnecter{name: "PhoneConnecter"}
	a.Connection()
	DisConnect(a)
}

7.空接口与 type switch

package main

import (
	"fmt"
)

type empty interface {
	// 空接口里面没有任何方法,所以任何类型都实现了空接口
}
type USB interface {
	Name() string
	Connecter
}
type Connecter interface {
	Connection()
}
type PhoneConnecter struct {
	name string
}

func (pc PhoneConnecter) Name() string {
	return pc.name
}

func (pc PhoneConnecter) Connection() {
	fmt.Println("connection", pc.name)
}

func DisConnect(u interface{}) { // 允许传入空接口,即允许传入任何值
	switch v := u.(type) {  // type switch用法,用在参数为空接口时
	case PhoneConnecter:
		fmt.Println("DisConnect", v.name)
	default:
		fmt.Println("unknown device")
	}

}

func main() {
	a := PhoneConnecter{name: "PhoneConnecter"}
	a.Connection()
	DisConnect(a)
}

8.接口转换

package main

import (
	"fmt"
)

type empty interface {
	// 空接口里面没有任何方法,所以任何类型都实现了空接口
}
type USB interface {
	Name() string
	Connecter
}
type Connecter interface {
	Connection()
}
type PhoneConnecter struct {
	name string
}

func (pc PhoneConnecter) Name() string {
	return pc.name
}

func (pc PhoneConnecter) Connection() {
	fmt.Println("connection", pc.name)
}

func DisConnect(u interface{}) { // 允许传入空接口,即允许传入任何值
	switch v := u.(type) {
	case PhoneConnecter:
		fmt.Println("DisConnect", v.name)
	default:
		fmt.Println("unknown device")
	}
}

func main() {
    // 接口的转换,只能从父级接口转为子集接口,不能反过来转
	a := PhoneConnecter{name: "PhoneConnecter"}
	var b Connecter
	b = Connecter(a)
	b.Connection()
}