2.2.1 按值实现接口
- 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 个步骤:
1. 通过 i.(*data)变量获取复制品内的内容.
2. 获取 i.(*data).A 内存.
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 方法.
3,nil interface 和 nil 区别
nil interface 和 nil 有什么区别呢? 咱们可以通过两个 demo 来看看它们具体有什么区别.
>>>>3.1 nil
接口内部 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;
}
>>>>3.2 nil interface
如果接口内部 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;
}
>>>>3.3 接口 nil 检查
可以利用 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
4,interface 性能问题
在文章刚开始就已经介绍了接口有很多优点, 由于接口是在运行期实现的, 所以它采用动态方法调用. 相比类型直接 (或静态) 方法调用, 性能肯定有消耗, 但是这种性能的消耗不大, 而主要影响是对象逃逸和无法内联.
>>>>4.1 接口动态调用对性能影响
实例 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 内存逃逸
现在观察以下通过类型直接方法调用和通过接口动态方法调用编译器如何进行优化.
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 发生了逃逸.
5, 总结
今天仅对接口的具体实现进行了简单分析, 接口有它的优势同时也有它的缺点. 在日常工程开发过程中如何选择还是需要根据具体的场景进行具体分析. 希望本篇文章对大家有所帮助.
来源: https://juejin.im/post/5c18b9065188253ff14778a2