12-接口
Go 中的接口 interface
是一种类型,一种抽象的类型。
interface
是一组方法的集合,是 duck-type programming
的一种体现。
接口做的事情就像是定义一个协议(规则),只要一台机器具有洗衣服和甩干的功能,我就称其为洗衣机。
接口不关心属性(数据),只关心行为(方法)。
只要一个结构体 X 实现了接口 A 中所有的方法,就称这个结构体 X 为接口 A 的实现类,称结构体 X 实现了接口 A。还可称结构体 X 是 A 类型。
Warning
为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。
定义接口
基本语法如下:
| type identifier interface {
methodName1([param])[(return)]
methodName2([param])[(return)]
methodName3([param])[(return)]
...
}
|
identifier
:接口名,命名时按照规范应该加上 er
,如 有字符串功能的 Stringer
、有读取功能的 Reader
。
methodName
:方法名
param
: 方法参数,非必填
return
: 方法返回值,非必填
Example
| type USBer interface {
start()
end()
}
|
实现接口
| // 定义一个接口
type USBer interface {
start()
end()
}
// 定义一个结构体
type Mouse struct {
name string
}
// 实现两个接口中的方法,接收者为结构体 Mouse,这样 Mouse 就是一个实现类
func (m Mouse) start() {
fmt.Println(m.name, "starts working.")
}
func (m Mouse) end() {
fmt.Println(m.name, "end working.")
}
|
结构体 Mouse
实现了 USBer
的所有方法 start()
和 end()
,所以 Mouse
就是 USBer
的一个实现类。
使用接口
实现了接口的结构体(实现类)和没实现接口的结构体,区别在于
当一个函数需要传递一个接口类型的参数时,可以传递一个接口变量进去,也可以传一个实现类对象进去。
| func test(usb USBer) { // 需要传递一个接口类型的参数
usb.start()
usb.end()
}
func main() {
m1 := Mouse{"罗技"}
test(m1) // 传递一个实现类对象
var u1 USBer = m1
test(u1) // 传递一个接口变量
}
|
Warning
注意,
接口变量使用之前,需要先赋一个实现类对象。
接口变量可以调用接口中的方法,但不可以调用实现类的属性和方法
| package main
import "fmt"
// 定义一个接口
type iUSBer interface {
start()
end()
}
// 定义一个结构体
type Mouse struct {
name string
}
// 实现两个接口中的方法,接收者为结构体 Mouse,这样 Mouse 就是一个实现类
func (m Mouse) start() {
fmt.Println(m.name, "starts working.")
}
func (m Mouse) end() {
fmt.Println(m.name, "end working.")
}
// 定义结构体 Mouse 自己的方法
func (m Mouse) selfMethod() {
fmt.Println(m.name, "selfMethod")
}
// 定义一个测试方法,需要接收一个 USB 接口变量。可以传递 USB 变量,也可以传递 USB 实现类的对象
func test(usb iUSBer) { // 需要传递一个接口类型的参数
usb.start()
usb.end()
}
func main() {
m1 := Mouse{"罗技"}
// Mouse是一个实现类,可以传递实现类对象给 test()
test(m1)
// 接口变量使用之前要先赋一个实现类对象
var u1 iUSBer = m1
// 传递一个接口变量给 test()
test(u1)
m1.start() // 罗技 starts working.
m1.end() // 罗技 end working.
m1.selfMethod() // 罗技 selfMethod
u1.start() // 罗技 starts working.
u1.end() // 罗技 end working.
// u1.selfMethod() // 错误,接口变量不可以调用实现类的方法
// u1.name // 错误,接口变量不可以调用实现类的属性
}
|
空接口
不包含任何方法的接口,称为空接口。
我们可以认为 任意类型都实现了空接口
| interface {}
type T interface {}
|
eg:
| type T interface{}
type person struct {
name string
}
var t1 T = person{"Boii"}
var t2 T = "str"
var t3 T = 100
var t4 T = []int{1, 2, 3}
fmt.Println(t1) // {Boii}
fmt.Println(t2) // str
fmt.Println(t3) // 100
fmt.Println(t4) // [1 2 3]
|
Info
空接口的作用在于 任意类型。
当我们想要接受一个任意类型的参数,或者定义一个接收任意类型的容器,都可以使用空接口来代替。
| func test (a interface{}) {
...
}
test("string")
test(123)
test([]int{1, 2, 3, 4})
|
| a := []interface{}{1, 3, "str", true}
fmt.Println(a) // {1 3 str true}
type T interface{}
b := []T{1, 3, "str", true}
fmt.Println(b) // {1 3 str true}
|
接口嵌套
接口中不仅可以定义方法签名,还可以定义其他的接口。这种方式我们称为 接口嵌套。
| // 接口A
type A interface {
method1()
}
// 接口B
type B interface {
method2()
}
// 接口C,嵌套了接口 A 和 B
type C interface {
A
B
method3()
}
// 结构体
type person struct {
name string
}
func (p person) method1() { // 使 person 成为 接口A 的实现类
...
}
func (p person) method2() { // 使 person 成为 接口B 的实现类
...
}
func (p person) method3() { // 使 person 成为 接口C 的实现类,前提是实现了前面两个
...
}
|
嵌套了接口,不仅要实现接口本身的方法,还要实现接口中的嵌套接口的方法,才能认为实现了最外侧的接口。
如上面例子,person 要成为 C 的实现类,不仅要实现 C 的 method3()
方法,还得实现 A 的 method1()
和 B 的method2()
。
| func main() {
p := person{"Boii"}
p.method1()
p.method2()
p.method3()
var a1 A = p
a1.method1()
// a1.method2() // !报错
// a1.method3() // !报错
var b1 B = p
b1.method2()
// b1.method1() // !报错
// b1.method3() // !报错
var c1 C = p
c1.method1() // 正确
c1.method2() // 正确
c1.method3() // 正确
var a2 A = c1 // 接口C变量看成是接口A类型,这是可以的
a2.method1() // 但是只能调用接口A的 method1()
// a2.method2() // !报错
// a2.method3() // !报错
var c2 C = a1 // !报错,接口A变量看成是接口C类型,这是错误的
}
|
接口断言
设 定义了一个接口A,还有两个结构体 X、Y,两个结构体都实现了接口A,当一个函数或方法想要接收 X 或 Y 两种类型时,可以将形参设置为 接口A 类型,这样传递的时候不管是 X 的对象还是 Y 的对象都可以传递进来。
Faq
【Q】:那如果在方法或函数里,我需要确定到底是 X 还是 Y 怎么办?
| type A interface {
aMethod()
}
type X struct {
name string
age int
}
type Y struct {
name string
sex string
}
// 实现接口A
func (x X) aMethod() {
fmt.Println("I am ", x)
}
func (y Y) aMethod() {
fmt.Println("I am", y)
}
// 定义一个接收 X 和 Y 对象的函数
func test(a A) {
a.aMethod()
}
|
【A】:这时候我们需要用到接口类型断言来确定传进来的到底是 x 还是 y。
-
方式1:
instance := 接口对象.(实际类型)
,这种不安全,会引发 panic()
instance, ok := 接口对象.(实际类型)
,这种就安全。接口对象是实际类型时,ok 为 true
-
方式2:switch
| switch instance := 接口对象.(type) {
case 实际类型1: ...
case 实际类型2: ...
...
}
|
于是在 test()
函数中我们可以这样写:
| func test (a A) {
if ins, ok := a.(X); ok {
fmt.Println(ins.age)
} else if ins, ok := a.(Y); ok {
fmt.Println(ins.sex)
}
}
|
或者这样写:
| // 定义一个接收 X 和 Y 对象的函数
func test(a A) {
switch ins := a.(type) {
case X:
fmt.Println(ins.age)
case Y:
fmt.Println(ins.sex)
}
}
|
值类型实现 VS 指针类型实现
我们先定义一个接口,和两个结构体
| type Aer interface {
method()
}
type X struct {}
type Y struct {}
|
然后分别使用 值类型 和 指针类型 实现接口
| // 值类型实现
func (x X) method() {}
// 指针类型实现
func (y *Y) method() {}
|
此时实现接口的是 X
类型、*X
类型 和 *Y
类型。
接着我们来使用一下:
| func main() {
x := X{}
y := Y{}
var a1 Aer = x // a1 可以接收 x 类型
var a2 Aer = &x // a2 可以接收 *X 类型
var a3 Aer = y // !报错,a3 不可以接收 y 类型
var a4 Aer = &y // a4 可以接收 *y 类型
}
|
在使用值类型实现接口之后,不管是 X 结构体类型
还是 *X 结构体指针类型
,都可以赋值给该接口变量。
因为在 Go 语言中有对指针类型变量求值的语法糖,X 指针 &x
内部会自动求值。
var a2 Aer = &x
会变成 var a2 Aer = *(&x)
。
而 Y 是用指针类型实现的,所以只能传递指针 &y
给 Aer 接口变量。
可以这么理解,结构体通过值接收者(Value receiver)实现了方法,编译器默认帮着实现了一个指针接收者(Pointer receiver)。
如果一个结构体在实现一个接口的时候有的“值实现”,有的“指针实现”,那么只有结构体指针(struct pointer)实现了这个接口。例如:
| type Ber interface {
f1()
f2()
f3()
}
type B1 struct{}
func (b *B1) f1() {}
func (b *B1) f2() {}
func (b B1) f3() {}
func inter2(b Ber) {}
func main() {
b1v := B1{}
b1p := &B1{}
inter2(b1p) // 可以通过编译
inter2(b1v) // cannot use b1v (variable of type B1) as Ber value in argument to inter2: missing method f1 (f1 has pointer receiver)
}
|
结构体嵌套接口
我们说当一个结构体 S
实现了接口 Ier
的所有方法时,则结构体 S
实现了 Ier
接口。实现了接口以后,可以将结构体变量 s1
传递给接口变量 ier1
,也可以调用接口 Ier
中的方法,当然具体执行的是 s1
实现的内容。
| type Ier interface { // 接口 Ier
method1()
method2()
}
type S struct {} // 结构体 S
func (s S) method1() { // 结构体 S 实现了 Ier 接口
fmt.Println("Hello1")
}
func (s S) method2() { // 结构体 S 实现了 Ier 接口
fmt.Println("Hello2")
}
func main() {
s1 := S{} // 结构体变量 s1
var ier1 Ier = s1 // 结构体变量 s1 可以传递给接口变量 ier1
s1.method1() // 结构体变量 s1 可以调用 Ier 中的方法,执行结果打印 Hello1
s1.method2() // 结构体变量 s1 可以调用 Ier 中的方法,执行结果打印 Hello2
}
|
那如果在结构体中嵌入一个接口变量呢?
| type Ier interface { // 接口 Ier
method1()
method2()
}
type S struct { // 结构体 S
Ier // 嵌入接口变量
name string
}
|
在结构体中匿名嵌入别的结构体相当于继承了这个结构体,是一种 is-a
的关系,上面的写法表明了:
这样写,可以将结构体变量 s1
赋值给接口变量 ier1
,但**无法调用接口中的方法**,因为结构体 S
没有实现。当然如果结构体 S
实现了方法,还是可以调用的。
| func main() {
s1 := S{} // 结构体变量 s1
var ier1 Ier = s1 // 结构体变量 s1 可以传递给接口变量 ier1
s1.method1() // x 结构体变量 s1 不能调用 Ier 中的方法,因为没有实现
s1.method2() // x 结构体变量 s1 不能调用 Ier 中的方法,因为没有实现
}
|
由此我们也可以这么理解:
当一个结构体 S
实现了接口 Ier
中的所有方法,
相当于隐式地在 S
中嵌入一个匿名字段 Ier
,
使得 S is a Ier
,
于是结构体变量 s1
可以赋值给接口变量 ier1
,可以调用接口方法。