本文介绍了Go语言中接口(interface)的内部实现、nil interface和nil的区别以及使用时的一些坑。
上篇文章回顾: Elasticsearch SQL用法详解
接口(interface)表明一种“约定”或“协议”,是多个方法声明的集合。容许在非显示关联状况下,组合并调用其它类型的方法。接口无需依赖类型,带来的优势就是减小调用者可视化方法,隐藏类型内部结构和具体方法实现细节。虽然接口的优势有不少,可是接口的实现是在运行期实现的,因此存在其它额外的开销。在平常开发过程当中是否选择接口须要根据场景进行合理的选择。
bash
一个接口须要包括方法签名,方法签名包括:方法名称、参数和返回列表。接口内不能有字段,并且不能定义本身的方法,这主要是因为字段和方法的定义须要分配内存。运维
package main
import (
"fmt"
"reflect"
)
type Ser interfacee {
A(a int)
B()
}
type X int
func (X) A(b int) {}
func (*X) B() {}
var o X
var _ Ser = &o
func main() {}复制代码
Go语言接口是隐式实现的,这意味着开发人员不须要声明它实现的接口。虽然这一般很是方便,但在某些状况下可能须要明确检查接口的实现。最好的方法就是依赖编译器实现,例如:函数
package main
type Jedi interface {
HasForce() bool
}
type Knight struct {}
var _ Jedi = (*Knight)(nil) // 利用编译器检查接口实现
func main() {}复制代码
接口调用是经过所属于它的方法集进行调用,而类型调用则经过它所属于的方法进行调用,它们之间有本质的差异。接下来讲说接口是如何实现的,以及如何获取接口的方法集。性能
runtime中有两种方式对接口实现,一种是iface类型,另外一种是eface。优化
// 接口内包含有方法的实现
type iface struct {
tab *itab
data unsafe.Pointer // 实际对象指针
}
// 类型信息
type itab struct {
inter *interfacetype // 接口类型
_type *_type // 实际类型对象
fun [1]uintptr // 实际对象方法地址
}
// 接口内不包含方法的实现,即nil interface.
type eface struct {
_type *_type
data unsafe.Pointer
}复制代码
2.2.1 按值实现接口ui
type T struct {}
type Ter interface{
A()
B()
}
func(t T) A(){}
func(t *T) B(){}
var o T
var i Ter = o复制代码
当将o实现接口Ter时,实际上是将T类型内存拷贝一份,而后i.data指向新生成复制品的内存地址。当调用i.A()方法时,通过如下3个步骤:this
1. 经过i.(*data)变量获取复制品内的内容。spa
2. 获取i.(*data).A内存。3d
3. 调用i.(*data).A()方法。指针
当调用i.B()方法时,因为receiver的是*T.B()和T.A()是不同的,调用通过也存在区别:
1. 经过i.(*data)变量获取其内容(此时的内容指向类型T的指针)。
2. 因为i.(*data)变量获取的内容是地址,因此须要进行取地址操做。但Go内部实现禁止对该复制品进行取地址操做,因此没法调用i.B()方法。
因此代码进行编译时会报错:
T does not implement Ter (B method has pointer receiver)
2.2.2 按指针实现接口
对以上代码进行稍加改动:
var o T
var i Ter = &o复制代码
此时经过调用i.A()和i.B()方法时是如何实现的呢?
1. 经过i.(*data)变量获取复制品内容(此时内容为指向类型T的指针)。
2. 获取复制品内容(即T类型地址),而后调用类型T的A和B方法。
2.2.3 接口方法集合
经过以上对接口实现分析,能够得出接口的方法集是:
1. 类型T的方法集包含全部receiver T方法。
2. 类型*T的方法集合包含全部Receiver T + *T方法。
nil interface和nil有什么区别呢?我们能够经过两个demo来看看它们具体有什么区别。
接口内部tab和data均为空时,接口才为nil。
// go:noinline
func main() {
var i interface{}
if i == nil {
println(“The interface is nil.“)
}
}
(gdb) info locals;
i = {_type = 0x0, data = 0x0}
(gdb) ptype i
type = struct runtime.eface {
runtime._type *_type;
void *data;
}复制代码
若是接口内部data值为nil,但tab不为空时,此时接口为nil interface。
// go:noinline
func main() {
var o *int = nil
var i interface{} = o
if i == nil {
println("Nil")
}
println(i)
}
(gdb) info locals;
i = {_type = 0x1050fe0 <type.*+25568>, data = 0x0}
o = 0x0
(gdb) ptype i
type = struct runtime.eface {
runtime._type *_type;
void *data;
}复制代码
能够利用reflect(反射)进行nil检查:
fun main() {
var o *int = nil
var a interface{} = o
var b interface{}
println(a == nil, b == nil) // false, true
v := reflect.ValueOf(a)
if v.Isvalid() {
println(v.IsNil()) // true, This is nil interface
}
}
(gdb) ptype v
type = struct reflect.Value {
struct reflect.rtype *typ;
void *ptr;
reflect.flag flag;
}复制代码
固然也能够经过unsafe进行检查:
v := reflet.ValueOf(a)
*(*unsae.Pointer)(v.ptr) == nil复制代码
在文章刚开始就已经介绍了接口有不少优势,因为接口是在运行期实现的,因此它采用动态方法调用。相比类型直接(或静态)方法调用,性能确定有消耗,可是这种性能的消耗不大,而主要影响是对象逃逸和没法内联。
实例1:
package main
type T struct{}
func (t *T) A() {}
func (t *T) B() {}
type Ter interface{
A()
B()
}
func main() {
var t T
var ter Ter = &t
ter.A()
ter.B()
}复制代码
反汇编:
TEXT main.main(SB) /Users/David/data/go/go.test/src/Demo/main.go
main.go:21 0x104ab90 65488b0c25a0080000 MOVQ GS:0x8a0, CX
main.go:21 0x104ab99 483b6110 CMPQ 0x10(CX), SP
main.go:21 0x104ab9d 7652 JBE 0x104abf1
main.go:21 0x104ab9f 4883ec20 SUBQ $0x20, SP
main.go:21 0x104aba3 48896c2418 MOVQ BP, 0x18(SP)
main.go:21 0x104aba8 488d6c2418 LEAQ 0x18(SP), BP
main.go:22 0x104abad 488d054cd80000 LEAQ runtime.rodata+55200(SB), AX
main.go:22 0x104abb4 48890424 MOVQ AX, 0(SP)
main.go:22 0x104abb8 e86303fcff CALL runtime.newobject(SB)
main.go:27 0x104abbd 488d059c710200 LEAQ go.itab.*main.T,main.Ter(SB), AX
main.go:27 0x104abc4 8400 TESTB AL, 0(AX)
main.go:22 0x104abc6 488b442408 MOVQ 0x8(SP), AX
main.go:22 0x104abcb 4889442410 MOVQ AX, 0x10(SP)
main.go:27 0x104abd0 48890424 MOVQ AX, 0(SP)
main.go:27 0x104abd4 e8f7feffff CALL main.(*T).A(SB)
main.go:27 0x104abd9 488b442410 MOVQ 0x10(SP), AX
main.go:28 0x104abde 48890424 MOVQ AX, 0(SP)
main.go:28 0x104abe2 e849ffffff CALL main.(*T).B(SB)
main.go:29 0x104abe7 488b6c2418 MOVQ 0x18(SP), BP
main.go:29 0x104abec 4883c420 ADDQ $0x20, SP
main.go:29 0x104abf0 c3 RET
main.go:21 0x104abf1 e82a88ffff CALL runtime.morestack_noctxt(SB)
main.go:21 0x104abf6 eb98 JMP main.main(SB)
:-1 0x104abf8 cc INT $0x3
:-1 0x104abf9 cc INT $0x3
:-1 0x104abfa cc INT $0x3
:-1 0x104abfb cc INT $0x3
:-1 0x104abfc cc INT $0x3
:-1 0x104abfd cc INT $0x3
:-1 0x104abfe cc INT $0x3
:-1 0x104abff cc INT $0x3复制代码
经过以上反汇编代码能够看到接口调用方法是经过动态调用方式进行调用。
实例2:
package main
type T struct{}
func (t *T) A() {
println("A")
}
func (t *T) B() {
println("B")
}
type Ter interface{
A()
B()
}
func main() {
var t T
t.A()
t.B()
}复制代码
以上代码在函数A和B内输出print,主要防止被内联以后,在main函数看不到效果。
反汇编:
TEXT main.main(SB) /Users/David/data/go/go.test/src/Demo/main.go
main.go:21 0x104aad0 65488b0c25a0080000 MOVQ GS:0x8a0, CX
main.go:21 0x104aad9 483b6110 CMPQ 0x10(CX), SP
main.go:21 0x104aadd 765e JBE 0x104ab3d
main.go:21 0x104aadf 4883ec18 SUBQ $0x18, SP
main.go:21 0x104aae3 48896c2410 MOVQ BP, 0x10(SP)
main.go:21 0x104aae8 488d6c2410 LEAQ 0x10(SP), BP
main.go:9 0x104aaed e8de6afdff CALL runtime.printlock(SB)
main.go:9 0x104aaf2 488d055bbf0100 LEAQ go.string.*+36(SB), AX
main.go:9 0x104aaf9 48890424 MOVQ AX, 0(SP)
main.go:9 0x104aafd 48c744240802000000 MOVQ $0x2, 0x8(SP)
main.go:9 0x104ab06 e80574fdff CALL runtime.printstring(SB)
main.go:9 0x104ab0b e8406bfdff CALL runtime.printunlock(SB)
main.go:13 0x104ab10 e8bb6afdff CALL runtime.printlock(SB)
main.go:13 0x104ab15 488d053abf0100 LEAQ go.string.*+38(SB), AX
main.go:13 0x104ab1c 48890424 MOVQ AX, 0(SP)
main.go:13 0x104ab20 48c744240802000000 MOVQ $0x2, 0x8(SP)
main.go:13 0x104ab29 e8e273fdff CALL runtime.printstring(SB)
main.go:13 0x104ab2e e81d6bfdff CALL runtime.printunlock(SB)
main.go:13 0x104ab33 488b6c2410 MOVQ 0x10(SP), BP
main.go:13 0x104ab38 4883c418 ADDQ $0x18, SP
main.go:13 0x104ab3c c3 RET
main.go:21 0x104ab3d e8de88ffff CALL runtime.morestack_noctxt(SB)
main.go:21 0x104ab42 eb8c JMP main.main(SB)
:-1 0x104ab44 cc INT $0x3
:-1 0x104ab45 cc INT $0x3
:-1 0x104ab46 cc INT $0x3
:-1 0x104ab47 cc INT $0x3
:-1 0x104ab48 cc INT $0x3
:-1 0x104ab49 cc INT $0x3
:-1 0x104ab4a cc INT $0x3
:-1 0x104ab4b cc INT $0x3
:-1 0x104ab4c cc INT $0x3
:-1 0x104ab4d cc INT $0x3
:-1 0x104ab4e cc INT $0x3
:-1 0x104ab4f cc INT $0x3复制代码
经过使用接口和类型两种方式发现,接口采用动态方法调用而类型方法调用被编译器直接内联了(直接将方法调用展开在了方法调用处,减小了内存调用stack开销)。因此采用类型直接方法调用性能优于使用接口调用。
如今观察如下经过类型直接方法调用和经过接口动态方法调用编译器如何进行优化。
4.2.1 编译器对类型方法优化
# Demo
./main.go:8:6: can inline (*T).A
./main.go:12:6: can inline (*T).B
./main.go:21:6: can inline main
./main.go:23:8: inlining call to (*T).A
./main.go:24:8: inlining call to (*T).B
./main.go:8:10: (*T).A t does not escape
./main.go:12:10: (*T).B t does not escape
./main.go:23:6: main t does not escape
./main.go:24:6: main t does not escape
<autogenerated>:1:0: leaking param: .this
<autogenerated>:1:0: leaking param: .this复制代码
4.2.2 编译器对接口方法优化
# Demo
./main.go:8:6: can inline (*T).A
./main.go:12:6: can inline (*T).B
./main.go:8:10: (*T).A t does not escape
./main.go:12:10: (*T).B t does not escape
./main.go:26:9: &t escapes to heap
./main.go:26:19: &t escapes to heap
./main.go:22:9: moved to heap: t
<autogenerated>:1:0: leaking param: .this
<autogenerated>:1:0: leaking param: .this复制代码
经过编译器对程序优化输出得出,当使用接口方式进行方法调用时main函数内的&t发生了逃逸。
今天仅对接口的具体实现进行了简单分析,接口有它的优点同时也有它的缺点。在平常工程开发过程当中如何选择仍是须要根据具体的场景进行具体分析。但愿本篇文章对你们有所帮助。
本文首发于公众号“小米运维”,点击查看原文