接口在Go语言有着至关重要的地位。接口是Go语言这个类型系统的基石,让Go语言在基础编程哲学的探索上达到了前所未有的高度。
接口解除了类型依赖,有助于减少用户的可视方法,屏蔽了内部结构和实现细节。但是接口实现机制会有运行期开销,也不能滥用接口。相对于包,或者不会频繁变化的内部模块之间,不需要抽象出接口来强行分离。接口最常用的使用场景,是对包提供访问,或预留扩展空间。
接口内部的实现:
type iface struce{
tab *iTable
data unsafe.Pointer
}
接口结构是包含两个字段的数据结构,第一个包含一个指向内部表的指针,这个内部表叫做iTable,包含子所存储的值的类型信息。iTable包含了已存储的值的类型信息已经值关联的一组方法。第二个字段指向存储值的指针,指向赋值的这个对象。
从内部实现来看,接口本身也是一种结构类型,只是编译器会对其做很多的限制。
接口用来定义行为的类型,这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。如果用户定义的类型实现了某个接口声明的一组方法,那么这个用户定义的类型对象可以直接赋值给接口类型对象。这个赋值会把用户定义的类型值存入接口类型的值。
对接口值方法的调用会执行接口值里存储的用户定义的类型值对应的方法。任何用户定义的类型都可以实现任何接口。如果离开了用户定义类型的实现,接口并没有具体的行为,用户定义的类型通常称为实体类型。
接口通常以er作为名称后缀,方法名是声明组成部分,但参数名可不同或省略。
type tester interface{
test()
string() string
}
type data struce{}
func (data)test{}
func (data) string() string{return "test"}
func main(){
var d data
var t tester = d
t.test()
fmt.Println(t.string())
}
方法集定义了接口的实现规则。
type tester interface{ test() string() string }
type data struce{}
func (*data)test{}
func (data) string() string{return "test"}
func main(){
var d data
var t tester = d //错误
//由于test接受*data类型
var t tester = &d
t.test() fmt.Println(t.string())
}
为什么var t tester=d编译时会产生错误,由于定义的方法集中,test方法是指针接收者,而string方法是值接收者,定义方法集是我们需要了解定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法时关联到值,还是指针,或者两个都关联。
如果使用指针接受者来实现一个接口,那么只有指向类型的指针才能实现对应的接口。如果使用值接收者来实现一个接口,那么这个类型的值和指针都能够实现对应的接口。
如果接口没有任何方法声明,那么这个接口就是一个空接口(interface{}),它的用途类似面向对象里面的Object,可被赋值为任何类型的对象。
接口变量默认值为nil,如果实现接口的类型支持,可以做相等运算。
func main(){
var t1,t2 interface{}
fmt.Println(t1==nil,t1==t2)
t1,t2=100,100
fmt.Println(t1==t2)
t1,t2=map[string]int{},map[string]int{}
fmt.Println(t1==t2)
}
//输出:
true true
true
panic:tuntime err:...
可以像匿名函数一样,嵌入其他接口,目标类型方法集中必须拥有包含嵌入接口方法在内的全部方法才能实现该接口。
嵌入其他接口类型,相当于将其声明的方法导入,这就要求不能有同名的方法,因为不支持重载,不能嵌入自身类型,会导致递归错误。
超集接口变量可隐式转换成子集,反过来不行。
支持匿名接口类型,可直接定义变量,或作为结构字段类型。
type data struct{}
func (data)string() string{
return "test"
}
type node struct{
data interface{
string() string
}
}
func main(){
var t interface{
string() string
}=data{}
n:=node{
data:t,
}
fmt.Println(data.string())
}
我们无法修改接口存储的复制品:
func main(){
d:=data{100}
var t interface{} = d
p:=&t.(data) //错误,不能取t.(data)的地址
t.(data).x = 200 //错误,不能赋值
}
可以修改为:
func main(){
d:=data{100}
var t interface{} = &d
t.(*data).x = 200 //错误,不能取t.(data)的地址
}
我们可将接口变量还原为原始类型,或用来判断是否实现了某个更具体的接口类型。
type data int
func (d data) String() string{
return fmt.Println("data:",d)
}
func main(){
var d data = 15
var x interface{}=d
if n,ok:=x.(String);ok{ //接口查询
...
}
if n1,ok:=x.(data);ok{ //类型转换
fmt.Println(n1)
}
}
使用ok这种判断模式,即便转换失败也不会引发panic,还可以使用在switch语句在多种类型间做出判断,这样空接口可以有更大的使用空间。
在项目前期设计合理的接口并不容易,而在代码重构,模块拆分时再分离出接口,用来解耦很常见。