Golang界面全面介绍
界面介绍
如果说够程和信道是转到并发的两大基石,那么接口是转到语言编程中数据类型的关键。在去语言的实际编程中,几乎所有的数据结构都围绕接口展开,接口是转到语言中所有数据结构的核心。
去不是一种典型的面向对象的语言,它在语法上不支持类和继承的概念。
没有继承是否就无法拥有多态行为了呢?答案是否定的,进入语言引入了一种新类型 - 接口,它在效果上实现了类似于C ++的“多态”概念,虽然与C ++的多态在语法上并非完全对等,但至少在最终实现的效果上,它有多态的影子。
虽然去语言没有类的概念,但它支持的数据类型可以定义对应的方法(或多个)。本质上说,所谓的方法(或多个)其实就是函数,只不过与普通函数相比,这类函数是作用在某个数据类型上的,所以在函数签名中,会有个接收器(接收器)来表明当前定义的函数会作用在该接收器上。
去语言支持的除接口类型外的任何其它数据类型都可以定义其方法(而并非只有结构才支持方法),只不过实际项目中,方法(或多个)多定义在结构上而已。
从这一点来看,我们可以把去中的结构看作是不支持继承行为的轻量级的“类”。
从语法上看,接口定义了一个或一组方法(一个或多个),这些方法(一个或多个)只有函数签名,没有具体的实现代码(有没有联想起C ++中的虚函数?)。若某个数据类型实现了接口中定义的那些被称为 “方法” 的函数,则称这些数据类型实现(实现)了接口。这是我们常用的面向对象的方式,如下是一个简单的示例
type MyInterface interface{
Print()
}
func TestFunc(x MyInterface) {}
type MyStruct struct {}
func (me MyStruct) Print() {}
func main() {
var me MyStruct
TestFunc(me)
}
为什么接口
为什么要用接口呢?在Gopher China上的分享中,有大神给出了下面的理由:
写通用算法(泛型编程)
隐藏实现细节(隐藏具体实现)
提供拦截点
下面大体再介绍下这三个理由
写通用算法(泛型编程)
严格来说,在Golang中并不支持泛型编程。在C ++等高级语言中使用泛型编程非常的简单,所以泛型编程一直是Golang诟病最多的地方。但是使用接口我们可以实现泛型编程,如下是一个参考示例
package sort
// A type, typically a collection, that satisfies sort.Interface can be
// sorted by the routines in this package. The methods require that the
// elements of the collection be enumerated by an integer index.
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less reports whether the element with
// index i should sort before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}
...
// Sort sorts data.
// It makes one call to data.Len to determine n, and O(n*log(n)) calls to
// data.Less and data.Swap. The sort is not guaranteed to be stable.
func Sort(data Interface) {
// Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached.
n := data.Len()
maxDepth := 0
for i := n; i > 0; i >>= 1 {
maxDepth++
}
maxDepth *= 2
quickSort(data, 0, n, maxDepth)
}
排序函数的形参是一个接口,包含了三个方法:Len(),Less(i,j int),Swap(i,j int)。使用的时候不管数组的元素类型是什么类型(int,float ,string ...),只要我们实现了这三个方法就可以使用Sort函数,这样就实现了“泛型编程”。
这种方式,我在闪聊项目里面也有实际应用过,具体案例就是对消息排序。
下面给一个具体示例,代码能够说明一切,一看就懂:
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s: %d", p.Name, p.Age)
}
// ByAge implements sort.Interface for []Person based on
// the Age field.
type ByAge []Person //自定义
func (a ByAge) Len() int { return len(a) }
func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func main() {
people := []Person{
{"Bob", 31},
{"John", 42},
{"Michael", 17},
{"Jenny", 26},
}
fmt.Println(people)
sort.Sort(ByAge(people))
fmt.Println(people)
}
隐藏实现细节(隐藏具体实现)
隐藏具体实现,这个很好理解。比如我设计一个函数给你返回一个接口,那么你只能通过接口里面的方法来做一些操作,但是内部的具体实现是完全不知道的。
例如我们常用的上下文包,就是这样的,上下文最先由谷歌提供,现在已经纳入了标准库,而且在原有上下文的基础上增加了:cancelCtx,timerCtx,valueCtx。
刚好前面我们有专门说过背景下,现在再来回顾一下
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
表明上WithCancel函数返回的还是一个Context interface,但是这个接口的具体实现是cancelCtx struct。
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{
Context: parent,
done: make(chan struct{}),
}
}
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
Context //注意一下这个地方
done chan struct{} // closed by the first cancel call.
mu sync.Mutex
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
func (c *cancelCtx) Done() <-chan struct{} {
return c.done
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.err
}
func (c *cancelCtx) String() string {
return fmt.Sprintf("%v.WithCancel", c.Context)
}
尽管内部实现上下面三个函数返回的具体结构(都实现了上下文界面)不同,但是对于使用者来说是完全无感知的。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) //返回 cancelCtx
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) //返回 timerCtx
func WithValue(parent Context, key, val interface{}) Context //返回 valueCtx
提供拦截点
暂无更多,待补充
interface源码分析
说了这么多,然后可以再来瞧瞧具体源码的实现
interface底层结构
根据接口是否包含有方法,底层实现上用两种结构来表示:iface和eface.eface表示不含方法的接口结构,或者叫空接口。对于Golang中的大部分数据类型都可以抽象出来_type结构,同时针对不同的类型还会有一些其他信息。
type eface struct {
_type *_type
data unsafe.Pointer
}
type _type struct {
size uintptr // type size
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32 // hash of type; avoids computation in hash tables
tflag tflag // extra type information flags
align uint8 // alignment of variable with this type
fieldalign uint8 // alignment of struct field with this type
kind uint8 // enumeration for C
alg *typeAlg // algorithm table
gcdata *byte // garbage collection data
str nameOff // string form
ptrToThis typeOff // type for pointer to this type, may be zero
}
iface表示非空接口的底层实现。相比于空接口,非空要包含一些method.method的具体实现存放在itab.fun变量里。
type iface struct {
tab *itab
data unsafe.Pointer
}
// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be in sync with
// ../cmd/compile/internal/gc/reflect.go:/^func.dumptypestructs.
type itab struct {
inter *interfacetype
_type *_type
link *itab
bad int32
inhash int32 // has this itab been added to hash?
fun [1]uintptr // variable sized
}
试想一下,如果接口包含多个方法,这里只有一个有趣变量怎么存呢?
其实,通过反编译汇编是可以看出的,中间过程编译器将根据我们的转换目标类型的空接口还是非空接口,来对原数据类型进行转换(转换成< _type,unsafe.Pointer>或者< itab,unsafe.Pointer>)。这里对于struct满不满足接口的类型要求(也就是结构是否实现了接口的所有方法) ,是由编译器来检测的。
iface之itab
iface结构中最重要的是itab结构.itab可以理解为pair <interface type,具体类型>。当然itab里面还包含一些其他信息,比如接口里面包含的方法的具体实现。下面细说.itab的结构如下。
type itab struct {
inter *interfacetype
_type *_type
link *itab
bad int32
inhash int32 // has this itab been added to hash?
fun [1]uintptr // variable sized
}
其中interfacetype包含了一些关于接口本身的信息,比如包路径,包含的方法。上面提到的iface和eface是数据类型(内置和类型定义)转换成接口之后的实体的struct结构,而这里的interfacetype是我们定义界时候的一种抽象表示。
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
type imethod struct { //这里的 method 只是一种函数声明的抽象,比如 func Print() error
name nameOff
ityp typeOff
}
_type表示concrete type.fun表示的interface里面的方法的具体实现。比如接口类型包含了方法A,B,则通过fun就可以找到这两个方法的具体实现。
接口的内存布局
了解接口的内存结构是非常有必要的,只有了解了这一点,我们才能进一步分析诸如类型断言等情况的效率问题先看一个例子:
type Stringer interface {
String() string
}
type Binary uint64
func (i Binary) String() string {
return strconv.Uitob64(i.Get(), 2)
}
func (i Binary) Get() uint64 {
return uint64(i)
}
func main() {
b := Binary{}
s := Stringer(b)
fmt.Print(s.String())
}
根据上面接口的源码实现,可以知道,接口在内存上实际由两个成员组成,如下图,标签指向虚表,数据则指向实际引用的数据。虚表描绘了实际的类型信息及该接口所需要的方法集
观察itable的结构,首先是描述类型信息的一些元数据,然后是满足Stringger接口的函数指针列表(注意,这里不是实际类型二进制的函数指针集哦)。因此我们如果通过接口进行函数调用,实际的操作其实就是s.tab-> fun 0。是不是和C ++的虚表很像?接下来我们要看看golang的虚表和C ++的虚表区别在哪里。
先看C ++,它为每种类型创建了一个方法集,而它的虚表实际上就是这个方法集本身或是它的一部分而已,当面临多继承时(或者叫实现多个接口时,这是很常见的),C ++对象结构里就会存在多个虚表指针,每个虚表指针指向该方法集的不同部分,因此,C ++方法集里面函数指针有严格的顺序。许多C ++新手在面对多继承时就变得蛋疼菊紧了,因为它的这种设计方式,为了保证其虚表能够正常工作,C ++引入了很多概念,什么虚继承啊,接口函数同名问题啊,同一个接口在不同的层次上被继承多次的问题啊等等......就是老手也很容易因疏忽而写出问题代码出来。
我们再来看golang的实现方式,同C ++一样,golang也为每种类型创建了一个方法集,不同的是接口的虚表是在运行时专门生成的。可能细心的同学能够发现为什么要在运行时生成虚表。因为太多了,每一种接口类型和所有满足其接口的实体类型的组合就是其可能的虚表数量,实际上其中的大部分是不需要的,因此golang选择在运行时生成它,例如,当例子中当首次遇见s:= Stringer(b)这样的语句时,golang会生成Stringer接口对应于Binary类型的虚表,并将其缓存。
理解了golang的内存结构,再来分析诸如类型断言等情况的效率问题就很容易了,当判定一种类型是否满足某个接口时,golang使用类型的方法集和接口所需要的方法集进行匹配,如果类型的方法集完全包含接口的方法集,则可认为该类型满足该接口。例如某类型有米个方法,某接口有ñ个方法,则很容易知道这种判定的时间复杂度为O( MXN),不过可以使用预先排序的方式进行优化,实际的时间复杂度为O(M + N)。
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
为啥呢?
一个接口{}类型的变量包含了2个指针,一个指针指向值的类型,一个另外指针指向实际的值
对一个接口{}类型的零变量来说,它的两个指针都是0;但是变种a * State传进去后,指向的类型的指针不为0了,因为有类型了,所以比较为false。界面类型比较,要是两个指针都相等,才能相等。