接口是golang的核心
简介
Go语言的接口并不是其他语言(C++、Java、C#等)中所提供的接口概念。Go也不是一种典型的OO语言,它在语法上不支持类和继承的概念。
如果说goroutine和channel是Go并发的两大基石,那么接口是Go语言编程中数据类型的关键。在Go语言的实际编程中,几乎所有的数据结构都围绕接口展开,接口是Go语言中所有数据结构的核心。
Go语言中的接口是一些方法的集合(method set),它指定了对象的行为:如果它(任何数据类型)可以做这些事情,那么它就可以在这里使用。
在Go语言出现之前,接口主要作为不同组件之间的契约存在。对契约的实现是强制的,你必须声明你的确实现了该接口。
接口的特性是golang支持鸭子类型的基础,即“如果它走起来像鸭子,叫起来像鸭子(实现了接口要的方法),它就是一只鸭子(可以被赋值给接口的值)”。凭借接口机制和鸭子类型,golang提供了一种游离于类、继承、模板之外的更加灵活强大的选择。
优点
- writing generic algorithm (泛型编程)
我们现在要写一个泛型算法,形参定义采用 interface 就可以了。在具体调用的时候传入具体类型,并且具体类型实现了sort接口的方法。如上文实现
- hiding implementation detail (隐藏具体实现)
隐藏具体实现,这个很好理解。比如我设计一个函数给你返回一个 interface,那么你只能通过 interface 里面的方法来做一些操作,但是内部的具体实现是完全不知道的
非侵入式接口
在Go语言中,一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口,而像java等语言,必须显示声明接口。
接口赋值
接口赋值在Go语言中分为如下两种情况:
- 将对象实例赋值给接口
- 将一个接口赋值给另一个接口
//将对象实例赋值给接口
type IUSB interface{
//定义IUSB的接口方法
}
//方法定义在类外,绑定该类,以下为方便,备注写在类中
type MP3 struct{
//实现IUSB的接口,具体实现方式是MP3的方法
}
type Mouse struct{
//实现IUSB的接口,具体实现方式是Mouse的方法
}
//接口赋值给具体的对象实例MP3
var usb IUSB =new(MP3)
usb.Connect()
usb.Close()
//接口赋值给具体的对象实例Mouse
var usb IUSB =new(Mouse)
usb.Connect()
usb.Close()
只要两个接口拥有相同的方法列表(与次序无关)即是两个相同的接口,可以相互赋值
接口赋值只需要接口A的方法列表是接口B的子集(即假设接口A中定义的所有方法,都在接口B中有定义),那么B接口的实例可以赋值给A的对象。反之不成立,即子接口B包含了父接口A,因此可以将子接口的实例赋值给父接口。
即子接口实例实现了子接口的所有方法,而父接口的方法列表是子接口的子集,则子接口实例自然实现了父接口的所有方法,因此可以将子接口实例赋值给父接口。
type Writer interface{ //父接口
Write(buf []byte) (n int,err error)
}
type ReadWriter interface{ //子接口
Read(buf []byte) (n int,err error)
Write(buf []byte) (n int,err error)
}
var file1 ReadWriter=new(File) //子接口实例
var file2 Writer=file1 //子接口实例赋值给父接口
接口查询
value, ok := Interfacevariable.(implementType)
其中 Interfacevariable 是接口变量(接口值),implementType 为实现此接口的类型,value 返回接口变量实际类型变量的值,如果该类型实现了此接口返回 true。
//判断file1接口指向的对象实例是否是File类型
var file1 Writer=...
if file5,ok:=file1.(File);ok{
...
}
类型查询
在Go语言中,还可以更加直截了当地询问接口指向的对象实例的类型。
var v1 interface{} = ...
switch v := v1.(type) {
case int: // 现在v的类型是int
case string: // 现在v的类型是string
...
}
//利用反射也可以进行类型查询,详情可参阅reflect.TypeOf()方法
空接口
由于Go语言中任何对象实例都满足空接口interface{},所以interface{}看起来像是可以指向任何对象的Any类型。
var v1 interface{} = 1 // 将int类型赋值给interface{}
var v2 interface{} = "abc" // 将string类型赋值给interface{}
var v3 interface{} = &v2 // 将*interface{}类型赋值给interface{}
var v4 interface{} = struct{ X int }{1}
var v5 interface{} = &struct{ X int }{1}
当函数可以接受任意的对象实例时,我们会将其声明为interface{},最典型的例子是标准库fmt中PrintXXX系列的函数
func Printf(fmt string, args …interface{})
func Println(args …interface{})
interface的内存布局
带方法的interface底层使用的数据结构与空interface不同,它是实现运行时多态的基础。
例子:
内存结构:
不带方法的interface,_type指向实际类型
type eface struct {
_type *_type
data unsafe.Pointer
}
观察itable的结构,首先是描述type信息的一些元数据,然后是满足Stringger接口的函数指针列表(注意,这里不是实际类型Binary的函数指针集哦)。 因此如果通过接口进行函数调用,实际的操作其实就是s.tab->fun0。 是不是和C的虚表很像?接下来看看golang的虚表和C++的虚表区别在哪里。
先看C++,它为每种类型创建了一个方法集,而它的虚表实际上就是这个方法集本身或是它的一部分而已,当面临多继承时(或者叫实现多个接口时,这是很常见的),C++对象结构里就会存在多个虚表指针,每个虚表指针指向该方法集的不同部分,因此,C++方法集里面函数指针有严格的顺序。 许多C新手在面对多继承时就变得紧张,因为它的这种设计方式,为了保证其虚表能够正常工作,C++引入了很多概念,什么虚继承啊,接口函数同名问题啊,同一个接口在不同的层次上被继承多次的问题啊等等…… 就是老手也很容易因疏忽而写出问题代码出来。
再来看golang的实现方式,同C++一样,golang也为每种类型创建了一个方法集,不同的是接口的虚表是在运行时专门生成的。 可能细心的同学能够发现为什么要在运行时生成虚表。 因为太多了,每一种接口类型和所有满足其接口的实体类型的组合就是其可能的虚表数量,实际上其中的大部分是不需要的,因此golang选择在运行时生成它,例如,当例子中当首次遇见s := Stringer(b)这样的语句时,golang会生成Stringer接口对应于Binary类型的虚表,并将其缓存。
理解了golang的内存结构,再来分析诸如类型断言等情况的效率问题就很容易了,当判定一种类型是否满足某个接口时,golang使用类型的方法集和接口所需要的方法集进行匹配,如果类型的方法集完全包含接口的方法集,则可认为该类型满足该接口。 例如某类型有$m$个方法,某接口有$n$个方法,则很容易知道这种判定的时间复杂度为$O(m times n)$,不过可以使用预先排序的方式进行优化,实际的时间复杂度为$O(m+n)$。
使用interface的注意事项
- 将对象赋值给接口变量时会复制该对象
- 接口使用的是一个名为itab的结构体存储的 type iface struct{ tab *itab // 类型信息 data unsafe.Pointer // 实际对象指针 }
- 只有接口变量内部的两个指针都为nil的时候,接口才等于nil。
- interface实际上是一个引用(只保存了两个值),因此传递它并不会造成太多的损耗。
interface{}与 nil 的比较
package main
import (
"fmt"
"reflect"
)
type State struct{}
func testnil1(a, b interface{}) bool {
return a == b
}
func testnil2(a *State, b interface{}) bool {
return a == b
}
func testnil3(a interface{}) bool {
return a == nil
}
func testnil4(a *State) bool {
return a == nil
}
func testnil5(a interface{}) bool {
v := reflect.ValueOf(a)
return !v.IsValid() || v.IsNil()
}
func main() {
var a *State
fmt.Println(testnil1(a, nil))
fmt.Println(testnil2(a, nil))
fmt.Println(testnil3(a))
fmt.Println(testnil4(a))
fmt.Println(testnil5(a))
}
代码返回结果如下
false
false
false
true
true
一个interface{}类型的变量包含了2个指针,一个指针指向值的类型,另外一个指针指向实际的值。对一个interface{}类型的nil变量来说,它的两个指针都是0;但是var a *State传进去后,指向的类型的指针不为0了,因为有类型了, 所以比较为false。 interface 类型比较, 要是两个指针都相等, 才能相等。