接口类型是对其他类型行为的抽象和概括。接口类型不会定义接口方法中的具体实现,由实现接口的类型去提供具体的实现细节。这样,我们可以写出非常灵活的代码。
golang
的接口不同于其他面向对象的语言,不需要显示指定一个对象实现了一个接口(比如 java
、php
等就需要显示指定实现某接口),只要类型包含接口的所有方法,则隐式的实现了该接口。
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。
我们可以通过 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 类型的指针,如图:
调用该接口值的Write方法,实际调用的是(*os.File).Write
方法,这个调用输出“hello”。
第三个语句给接口值赋了一个*bytes.Buffer类型的值
w = new(bytes.Buffer)
现在动态类型是 *bytes.Buffer
,动态值是一个指向新分配的缓冲区的指针,如图:
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()
}