Go 语言入门指南:聊聊Go中的接口 | 青训营

98 阅读5分钟

接口的动态和静态性质

接口的静态性质体现在接口类型变量具有静态类型,编译器会在编译阶段对所有接口类型变量的赋值操作进行类型检查,编译器会检查右值的类型是否实现了该接口方法集合中的所有方法。如果不满足,就会进行报错。

var err error = "hello"
cannot use "hello" (constant of type string) as error value in variable declaration: string does not implement error (missing method Error)

接口的动态性质体现在接口类型变量在运行时还存储了右值的真实类型信息,这个右值
的真实类型被称为接口类型变量的动态类型。

var err error
err = fmt.Errorf("error occur")
fmt.Println(err)

接口动态和静态性质的优点在于,接口类型变量在程序运行时可以被赋值为不同的动态类型变量,每次赋值后,接口类型变量中存储的动态类型信息都会发生变化,这让 Go 可以像动态语言(比如 Python)那样拥有使用 Duck Typing(鸭子类型)的灵活性。

何为鸭子类型?

在程序设计中,鸭子类型(Duck typing)是动态类型和某些静态语言的一种对象推断风格。这种风格适用于动态语言(比如PHP、Python等)和某些静态语言(比如Golang)一般来说,静态类型语言在编译时便已确定了变量的类型,但是Golang的实现是:在编译时推断变量的类型),支持"鸭子类型"的语言的解释器/编译器将会在解析(Parse)或编译时,推断对象的类型。

type Ducker interface {
	action()
}

type RealDuck struct {}

func (r RealDuck) action() {
	fmt.Println("ga ga ga")
}

type OtherDuck struct {}

func (r OtherDuck) action() {
	fmt.Println("other duck")
}

func doAction(e Ducker) {
	e.action()
}

func main() {
	a := RealDuck{}
	b := OtherDuck{}

	doAction(a) // ga ga ga
	doAction(b) // other duck
}

上面代码中,RealDuck和OtherDuck都实现了Ducker接口action方法,所以他们都是Ducker类型,如果一个没有实现action方法的对象,再对该对象使用duck类型就会报错。另外, 如果接口使用者定义了一个新的接口也拥有action方法, 那上面的RealDuckOtherDuck也可以当做新的接口来使用。

go接口经典误区 nil error 值 != nil

type Demo struct {
	error
}

func flag() bool {
	return false
}

func returnDemo() error {
	var d *Demo= nil
	if flag() {
		return &Demo{
			fmt.Errorf("demo error"),
		}
	}
	return d 
}

func main() {
	r := returnDemo()
	if r != nil {
		fmt.Println("r is not nil") // return
	}else {
		fmt.Println("r is nil")
	}
}

运行上面这段代码,我们会发现代码会进入 r!=nil 分支输出 r is not nil,但是,按正常逻辑,我们在returnDemo函数中定义了d==nil,按逻辑应该返回nil,那么r也应该是nil,最后应该走else分支,那么为什么为这样呢?我们就需要了解go接口的内部表示。

接口类型变量的内部表示

type iface struct {  
    tab *itab  
    data unsafe.Pointer  
}  
type eface struct {  
    _type *_type  
    data unsafe.Pointer
}

在runtime运行时包中发现,接口类型变量有两种类型表示,eface和iface,其中eface是空接口表示,iface是带有方法接口表示。这两个结构的共同点是它们都有两个指针字段,并且第二个指针字段的功能相同,都是指向当前赋值给该接口类型变量的动态类型变量的值。

eface中有一个 _type 属性,这是接口类型变量的动态类型的信息,它定义为:

type _type struct {  
    size uintptr  
    ptrdata uintptr // size of memory prefix holding all pointers  
    hash uint32  
    tflag tflag  
    align uint8  
    fieldAlign uint8  
    kind uint8  
    // function for comparing objects of this type  
    // (ptr to object A, ptr to object B) -> ==?  
    equal func(unsafe.Pointer, unsafe.Pointer) bool  
    // gcdata stores the GC type data for the garbage collector.  
    // If the KindGCProg bit is set in kind, gcdata is a GC program.  
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.  
    gcdata *byte  
    str nameOff  
    ptrToThis typeOff  
}

而iface除了要存储动态类型信息外,还要存储定义的方法等信息,它的 itab 属性定义如下:

type itab struct {  
    inter *interfacetype  
    _type *_type  
    hash uint32 // copy of _type.hash. Used for type switches.  
    _ [4]byte  
    fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement  
}

通过上面的接口定义,我们可以简单得出,每个接口类型变量在运行时的表示都是由两部分组成的,针对不同接口类型我们可以简化记作 eface(_type, data)和iface(tab, data),而且,Go 语言中每种类型都会有唯一的 _type 信息,无论是内置原生类型,还是自定义类型都有。Go 运行时会为程序内的全部类型建立只读的共享 _type 信息表,因此拥有相同动态类型的同类接口类型变量的 _type/tab 信息是相同的。

现在我们来分析接口几种等值比较情况:

nil 接口变量

  var i interface{}
	var err error
	fmt.Println(i == nil) // true
	fmt.Println(err == nil) // true
	fmt.Println(i==err) // true

可以看出,无论是空接口类型还是非空接口类型变量,一旦变量值为nil,他们的类型和数值信息均为nil,因此,打印结果全为true

空接口类型变量

  var a interface{}
	var b interface{}

	var c, d = 1, 2

	a,b = c,d
	fmt.Println(a==b) // false
	b = 1
	fmt.Println(a==b) // true
	b = int64(1) 
	fmt.Println(a==b) // false

通过对b重新赋值1可以看出,此时a和b相等,即类型信息和数据值都一样,而对b赋值为int64类型,此时,虽然数值一样,但是类型信息不一样,所以为false。

非空接口类型变量

  var e1 error
	var e2 error
	e1 = (*T)(nil) // 类型判断
	fmt.Println(e1==nil) // false
	e1 = T(1)
	e2 = T(2)
	fmt.Println(e1==e2) // false
	e2 = T(1)
	fmt.Println(e1==e2) // false

对于非空接口类型,与空接口类型一样,只用类型信息和数据都相同情况下,两个变量才相等。这时候我们就可以对误区 nil error 值 != nil 进行解答了,虽然returnDemo返回值为空,但是它的类型信息不为空,所以返回不是nil,自然就走if那个分支了。

小结

接口类型作为 Go重要组成部分,区别于其他语言, Go 接口的动态特性让 Go 拥有与动态语
言相近的灵活性,而静态特性又在编译阶段保证了这种灵活性的安全。

参考

go语言接口之duck typing juejin.cn/post/684490…