7.1 定义
接口代表一种调用契约,是多个方法声明的集合。
在某些动态语言中,接口(interface)也被称为协议。准备交互的双方,共同遵守事先约定的规则,使得在无须直到对方身份的情况下进行协作。接口要实现的是做什么,而不关心怎么做,谁来做。
接口解除了类型依赖,有助于减少用户可视方法,屏蔽内部结构和实现细节。似乎好处很多,但这并不意味着可以滥用接口,毕竟接口实现机制会有运行期间开销。对于相同包,或者不会频繁变化的内部模块之间,并不需要抽象出接口来强行分离。接口最常见的使用场景,是对包外提供访问,或预留扩展空间。
Go接口实现机制很简洁,只要目标类型方法集内包含接口声明的全部方法,就被视为实现了该接口,无需做显示声明,当然,目标类型可实现多个接口。
换句话说,我们可以先实现类型,而后再抽象出所需接口。这种非侵入式设计有很多好处。举例来说:在项目前期就设计出最合理的接口并不容易,而在代码重构,模块拆分时再分离出所需接口,用以解耦合就很常见。另外,在使用第三方库时,抽象出所需接口,即可屏蔽掉太多不需要关注的内容
从内部实现来看,接口自身也是一种结构类型,只是编译器会对其做出很多限制。
- 不能有字段
- 不能定义自己的方法
- 只能声明方法,不能实现
- 可嵌入其他接口类型
接口通常以er作为名称后缀,方法名是声明组成部分,但参数名可不同或省略
type tester interface {
test()
string(x string) string
}
type data struct{}
func (*data) test() {}
func (data) string(x string) string { return x }
func main() {
var d data
//var t tester = d //data does not implement tester (test method has pointer receiver)
var t tester = &d
t.test()
println(t.string("a"))
}
编译器根据方法集判断是否实现了接口,显然在上例中,只有*data才符合tester的要求
如果接口没有任何方法声明,那么就是一个空接口,它的用途类似面向对象里的根类型Object,可被赋值成任何类型的对象。
接口变量默认值是nil,如果实现接口的支持类型,可做相等运算
func main() {
var t1, t2 interface{}
println(t1 == t2, t1 == nil)
t1, t2 = 100, 100
println(t1 == t2)
}
可以像匿名字段那样,嵌入其他接口。目标类型方法集中必须包含嵌入接口方法在内的全部方法才算实现了该接口。
嵌入其他接口类型,相当于将其声明的方法集导入。这就要求不能有同名方法,因为不支持重载,还有,不能嵌入自身或循环嵌入,那会导致递归错误。
type stringer interface {
string() string
}
type tester interface {
stringer
test()
}
type data struct{}
func (*data) test() {}
func (data) string() string { return "" }
func main() {
var d data
var t tester = &d
println(t.string())
}
超集接口变量可隐式转换为子集,反过来不行
func pp(a stringer) {
println(a.string())
}
func main() {
var d data
var t tester = &d
pp(t) // 隐式转换超集为子集
var s stringer = t // 显示转换
println(s.string())
//var t2 tester = s //stringer does not implement tester (missing test method)
}
支持匿名接口类型,可直接用于变量定义,或作为结构字段类型
type data struct{}
func (data) string() string {
return ""
}
type node struct {
data interface { // 匿名接口类型
string() string
}
}
func main() {
var t interface { // 匿名接口变量
string() string
} = data{}
n := node{
data: t,
}
println(n.data.string())
}
7.2 执行机制
接口使用一个名为itab的结构存储运行期间所需的相关信息
type itab struct{
inter *interfacetype //接口类型
_type *_type //实际对象类型
fun [1]uintptr //实际对象方法地址
}
type iface struct{
tab *itab //类型信息
data unsafe.Pointer //实际对象指针
}
查看结构存储的具体内容
type Ner interface {
a()
b(int)
c(string) string
}
type N int
func (N) a() {}
func (*N) b(int) {}
func (*N) c(string) string { return "a" }
func main() {
var n N
var t Ner = &n
t.a()
}
很显然,相关类型信息里保存了接口和实际对象的元数据。同时,itab还用fun数组(不定长结构)保存了实际方法地址,从而实现在运行期间对目标方法的调用
除此之外接口还有一个重要特征:将对象赋值给接口变量时,会复制该对象。
type data struct {
x int
}
func main() {
d := data{100}
var t interface{} = d
println(t.(data).x) //100
}
无法修改接口存储的复制品,因为它也是unaddressable的
type data struct {
x int
}
func main() {
d := data{100}
var t interface{} = d
p := &t.(data) // cannot take the address of t.(data)
t.(data).x = 200 //cannot assign to t.(data).x
}
即便将其复制出来,用本地变量修改后,依然无法对t.(data)赋值。解决方法就是将对象指针赋值给接口,那么接口内存储的就是指针的复制品。
func main() {
d := data{100}
var t interface{} = &d
t.(*data).x += 200
fmt.Println(d) // 300
}
只有当接口内部的两个指针(itab,data)都为nil时,接口才为nil
由此造成的错误并不罕见,尤其在函数返回error时
type TestError struct{}
func (*TestError) Error() string {
return "error"
}
func test(x int) (int, error) {
var err *TestError
if x < 0 {
err = new(TestError)
x = 0
} else {
x += 100
}
return x, err // 注意,这个err是有类型的
}
func main() {
x, err := test(-1)
if err != nil {
log.Fatalln("err != nil")
}
println(x)
}
结果:
2019/03/07 16:38:52 err != nil
exit status 1
正确做法是明确返回nil
func main() {
x, err := test(100)
if err != nil {
log.Fatalln("err != nil")
}
println(x)
}
7.3 类型转换
类型推断可将接口变量还原为原始类型,或用来判断是否实现了某个更具体的接口类型。
func main() {
var d data = 15
var x interface{} = d
if n, ok := x.(fmt.Stringer); ok { // 转换为更具体的借口类型
fmt.Println(n)
}
if d2, ok := x.(data); ok { // 转换为原始类型
fmt.Println(d2)
}
//e := x.(error) //main.data is not error: missing method Erro
//fmt.Println(e)
}
结果
data:15
data:15
使用ok-idiom模式,即便转换失败也不会引发panic。还可用switch语句在多种类型间做出推断匹配,这样,空接口就有更多发挥空间。
func main() {
var x interface{} = func(x int) string {
return fmt.Sprintf("d:%d", x)
}
switch v := x.(type) {
case nil:
println("nil")
case *int:
println(*v)
case func(int) string:
println(v(100)) //d:100
case fmt.Stringer:
fmt.Println(v)
default:
println("unknown")
}
}
type switch 不支持fallthrought.
7.4 技巧
让编译器检查,确保实现了指定接口。
type x int
func init() {
var _ fmt.Stringer = x(0) //cannot use x(0) (type x) as type fmt.Stringer in assignment:
// x does not implement fmt.Stringer (missing String method)
}
定义函数类型,让相同签名的函数自动实现某个接口。
type FuncString func() string
func (f FuncString) String() string {
return f()
}
func main() {
var t fmt.Stringer = FuncString(func() string { //转换类型,使其实现Stringer 接口
return "hello test"
})
fmt.Println(t)
}