本文内容基于《Go语言核心编程》,李文塔著。
1 接口
1.1 基本概念
1.1.1 接口声明
Go语言的接口分为接口字面量类型和接口命名类型,接口的声明使用interface关键字,接口命名类型使用type关键字声明:
// 接口字面量类型
interface {
MethodSignature1
MethodSignature2
}
// 接口命名类型
type INterfaceName interface {
MethodSignature1
MethodSignature2
}
接口定义大括号内可以是方法声明的集合,也可以嵌入另一个接口类型匿名字段,还可以是二者的混合:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// 以下声明等价
type ReadWriter interface {
Read
Writer
}
type ReadWriter interface {
Read
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
区别方法声明和函数声明:
// 方法声明 = 方法名 + 方法签名
MethodName(InputTypeList) OutputTypeList
// 函数声明 = 函数名 + 函数签名
func FunctionName(InputTypeList) OutputTypeList
接口的特点:
- 接口的命名一般以“er”结尾;
- 接口定义的内部方法声明不需要func引导;
- 在接口定义中,只有方法声明没有方法实现。
1.1.2 接口初始化
接口起到抽象和适配的作用,接口绑定具体类型的实例的过程称为接口初始化,支持两种直接初始化方法:
- 实例赋值接口:如果具体类型实例的方法集是某个接口的方法集的超集,则称该具体实例实现了接口,可以将该具体类型的实例直接赋值给接口类型的变量,此时编译器会进行静态的类型检查,接口被初始化后,调用接口的方法就相当于调用接口绑定的具体类型的方法,这就是接口调用的语意;
- 接口变量赋值接口变量:已经初始化的接口类型变量a直接赋值给另一种接口变量b,要求b的方法集是a的方法集的子集,此时Go编译器会在编译时进行方法集静态检查,这个过程也是接口初始化的一种方式,此时接口变量b绑定的具体实例是接口变量a绑定的具体实例的副本。
type Aer interface {
Method1()
Method2()
}
type Ber interface {
Method1()
}
func main() {
var a Aer
var b Ber
b = a
//a = b // Cannot use 'b' (type Ber) as type Aer Type does not implement 'Aer' as some methods are missing: Method2()
}
1.1.3 接口方法调用
接口方法的调用的最终地址是在运行期决定的,将具体类型变量赋值给接口后,会使用具体类型的方法指针初始化接口变量,当调用接口变量的方法时,实际上是间接地调用实例的方法,接口方法调用不是一种直接的调用,有一定的运行时开销(直接调用未初始化的接口变量的方法会产生panic):
type Printer interface {
Print()
}
type S struct {}
func (s S) Print() {
fmt.Println("something")
}
func main() {
var i Printer
//i.Print() // Method call 'i.Print()' may lead to nil pointer dereference
i = S{}
i.Print()
}
1.1.4 接口的动态类型和静态类型
- 动态类型:
接口绑定的具体实例的类型成为接口的动态类型,接口可以绑定不同类型的实例,所以接口的动态类型是随着绑定的不同类型实例而发生变化的。
- 静态类型:
接口被定义时,其类型就已经被确定,这个类型叫接口的静态类型,接口的静态类型在定义时就被确定,静态类型的本质特征就是接口的方法签名集合,两个接口如果方法签名集合相同(顺序可以不同),则这两个接口在语义上完全等价,它们之间不需要强制类型转换就可以相互赋值。
1.2 接口运算
1.2.1 类型断言
接口类型断言的语法形式如下,有两层语义:
- 如果TypeName是一个具体类型名,则类型断言用于判断接口变量i绑定的实例类型是否就是具体类型TypeName;
- 如果TypeName是一个接口类型名,则类型断言用于判断接口变量i绑定的实例类型是否同时实现了TypeName接口。
i.(TypeName)
接口类型断言的两种语法表现:
- 直接赋值模式:
type Aer interface {
Method1()
Method2()
}
type S1 struct {}
func (s S1) Method1() {}
func (s S1) Method2() {}
type S2 struct {}
func main() {
s1 := S1{}
var i interface{} = s1
o1 := i.(S1)
o1.Method1()
o1.Method2()
o2 := i.(S2) // panic: interface conversion: interface {} is main.S1, not main.S2
fmt.Println(o2)
}
- common, ok表达模式:
type Aer interface {
Method1()
Method2()
}
type S1 struct {}
func (s S1) Method1() {}
func (s S1) Method2() {}
type S2 struct {}
func main() {
s1 := S1{}
var i interface{} = s1
o1, ok := i.(S1)
if ok {
o1.Method1()
o1.Method2()
}
o2, ok := i.(S2)
if ok {
fmt.Println(o2)
}
}
1.2.2 类型查询
接口类型查询的语法格式如下,有两层语义:
- 查询一个接口变量底层绑定的底层变量的具体类型是什么;
- 查询一个接口变量底层绑定的底层变量是否还实现了其他接口。
switch v := i.(type)
case type1:
xxx
case type2:
xxx
default:
xxx
1.2.3 接口优点和使用形式
接口优点:
- 解耦:复杂系统进行垂直和水平的分割是常用的设计手段,在层与层之间使用接口进行抽象和解耦是一种好的编程策略,Go的非侵入式的接口使层与层之间的代码更加干净,具体类型和实现的接口之间不需要显式声明,增加了接口使用的自由度;
- 实现泛型:由于现阶段Go语言还不支持泛型,使用空接口作为函数或方法参数能够用在需要泛型的场景中。
接口使用形式:
- 作为结构内嵌字段;
- 作为函数或方法的形参;
- 作为函数或方法的返回值;
- 作为其他接口定义的嵌入字段。