Go interface实现分析

简介: 本文深入探讨了Go语言中接口的定义、实现及性能影响。接口作为一种“约定”,包含方法签名集合,无需依赖具体类型即可调用方法,隐藏了内部实现细节。文章分析了接口的两种实现方式(iface和eface)、按值与按指针实现的区别,以及nil接口与普通nil的区别。同时,通过反汇编代码对比了接口动态调用与类型直接调用的性能差异,指出接口调用存在内存逃逸和无法内联的问题。最后总结了接口的优势与局限性,强调在实际开发中需根据场景合理选择是否使用接口。

前言

接口(interface)代表一种“约定”或“协议”,是多个方法声明的集合。允许在非显示关联情况下,组合并调用其它类型的方法。接口无需依赖类型,带来的优点就是减少调用者可视化方法,隐藏类型内部结构和具体方法实现细节。虽然接口的优点有很多,但是接口的实现是在运行期实现的,所以存在其它额外的开销。在日常开发过程中是否选择接口需要根据场景进行合理的选择。

1、接口定义

一个接口需要包括方法签名,方法签名包括:方法名称、参数和返回列表。接口内不能有字段,而且不能定义自己的方法,这主要是由于字段和方法的定义需要分配内存。

体验AI代码助手

代码解读

复制代码

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() {}

>>>>1.1 如何确保类型实现接口

Go语言接口是隐式实现的,这意味着开发人员不需要声明它实现的接口。虽然这通常非常方便,但在某些情况下可能需要明确检查接口的实现。最好的方法就是依赖编译器实现,例如:

体验AI代码助手

代码解读

复制代码

package main

type Jedi interface {
    HasForce() bool
}

type Knight struct {}

var _ Jedi = (*Knight)(nil)       // 利用编译器检查接口实现

func main() {}

2、接口内部实现

接口调用是通过所属于它的方法集进行调用,而类型调用则通过它所属于的方法进行调用,它们之间有本质的差别。接下来说说接口是如何实现的,以及如何获取接口的方法集。

>>>>2.1 接口内部实现

runtime中有两种方式对接口实现,一种是iface类型,另一种是eface。

体验AI代码助手

代码解读

复制代码

// 接口内包含有方法的实现
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 按值实现接口和按指针实现接口区别

2.2.1 按值实现接口

体验AI代码助手

代码解读

复制代码

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 按指针实现接口

对以上代码进行稍加改动:

体验AI代码助手

代码解读

复制代码

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。

体验AI代码助手

代码解读

复制代码

// 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。

体验AI代码助手

代码解读

复制代码

// 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检查:

体验AI代码助手

代码解读

复制代码

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进行检查:

体验AI代码助手

代码解读

复制代码

v := reflet.ValueOf(a)
*(*unsae.Pointer)(v.ptr) == nil

4、interface性能问题

在文章刚开始就已经介绍了接口有很多优点,由于接口是在运行期实现的,所以它采用动态方法调用。相比类型直接(或静态)方法调用,性能肯定有消耗,但是这种性能的消耗不大,而主要影响是对象逃逸和无法内联。

>>>>4.1 接口动态调用对性能影响

实例1:

体验AI代码助手

代码解读

复制代码

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()
}

反汇编:

体验AI代码助手

代码解读

复制代码

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:

体验AI代码助手

代码解读

复制代码

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函数看不到效果。

反汇编:

体验AI代码助手

代码解读

复制代码

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 编译器对类型方法优化

体验AI代码助手

代码解读

复制代码

# 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 编译器对接口方法优化

体验AI代码助手

代码解读

复制代码

# 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、总结

今天仅对接口的具体实现进行了简单分析,接口有它的优势同时也有它的缺点。在日常工程开发过程中如何选择还是需要根据具体的场景进行具体分析。希望本篇文章对大家有所帮助。



相关文章
|
6月前
|
Go 调度
GO语言函数的内部运行机制分析
以上就是Go语言中函数的内部运行机制的概述,展示了函数在Go语言编程中如何发挥作用,以及Go如何使用简洁高效的设计,使得代码更简单,更有逻辑性,更易于理解和维护。尽管这些内容深入了一些底层的概念,但我希望通过这种方式,将这些理论知识更生动、更形象地带给你,让你在理解的同时找到编程的乐趣。
88 5
|
7月前
|
存储 监控 算法
员工行为监控软件中的 Go 语言哈希表算法:理论、实现与分析
当代企业管理体系中,员工行为监控软件已逐步成为维护企业信息安全、提升工作效能的关键工具。这类软件能够实时记录员工操作行为,为企业管理者提供数据驱动的决策依据。其核心支撑技术在于数据结构与算法的精妙运用。本文聚焦于 Go 语言中的哈希表算法,深入探究其在员工行为监控软件中的应用逻辑与实现机制。
160 14
|
8月前
|
Java 编译器 Go
go的内存逃逸分析
内存逃逸分析是Go编译器在编译期间根据变量的类型和作用域,确定变量分配在堆上还是栈上的过程。如果变量需要分配在堆上,则称作内存逃逸。Go语言有自动内存管理(GC),开发者无需手动释放内存,但编译器需准确分配内存以优化性能。常见的内存逃逸场景包括返回局部变量的指针、使用`interface{}`动态类型、栈空间不足和闭包等。内存逃逸会影响性能,因为操作堆比栈慢,且增加GC压力。合理使用内存逃逸分析工具(如`-gcflags=-m`)有助于编写高效代码。
152 2
|
9月前
|
存储 缓存 监控
企业监控软件中 Go 语言哈希表算法的应用研究与分析
在数字化时代,企业监控软件对企业的稳定运营至关重要。哈希表(散列表)作为高效的数据结构,广泛应用于企业监控中,如设备状态管理、数据分类和缓存机制。Go 语言中的 map 实现了哈希表,能快速处理海量监控数据,确保实时准确反映设备状态,提升系统性能,助力企业实现智能化管理。
148 3
|
12月前
|
Go API 数据库
Go 语言中常用的 ORM 框架,如 GORM、XORM 和 BeeORM,分析了它们的特点、优势及不足,并从功能特性、性能表现、易用性和社区活跃度等方面进行了比较,旨在帮助开发者根据项目需求选择合适的 ORM 框架。
本文介绍了 Go 语言中常用的 ORM 框架,如 GORM、XORM 和 BeeORM,分析了它们的特点、优势及不足,并从功能特性、性能表现、易用性和社区活跃度等方面进行了比较,旨在帮助开发者根据项目需求选择合适的 ORM 框架。
938 4
|
12月前
|
中间件 Go API
Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架
本文概述了Go语言中几种流行的Web框架,如Beego、Gin和Echo,分析了它们的特点、性能及适用场景,并讨论了如何根据项目需求、性能要求、团队经验和社区支持等因素选择最合适的框架。
1171 1
|
安全 Go 开发者
代码之美:Go语言并发编程的优雅实现与案例分析
【10月更文挑战第28天】Go语言自2009年发布以来,凭借简洁的语法、高效的性能和原生的并发支持,赢得了众多开发者的青睐。本文通过两个案例,分别展示了如何使用goroutine和channel实现并发下载网页和构建并发Web服务器,深入探讨了Go语言并发编程的优雅实现。
195 2
|
算法 Java 编译器
你为什么不应该过度关注go语言的逃逸分析
【10月更文挑战第21天】逃逸分析是 Go 语言编译器的一项功能,用于确定变量的内存分配位置。变量在栈上分配时,函数返回后内存自动回收;在堆上分配时,则需垃圾回收管理。编译器会根据变量的使用情况自动进行逃逸分析。然而,过度关注逃逸分析可能导致开发效率降低、代码复杂度增加,并且对性能的影响相对较小。编译器优化通常比人工干预更准确,因此开发者应更多关注业务逻辑和整体性能优化。
|
编译器 Go
Go中init()执行顺序分析
文章分析了Go语言中`init()`函数的执行顺序和时机,指出全局变量初始化后先于`init()`函数执行,而`init()`函数在`main()`函数之前执行,且包的`init()`函数按包的导入顺序进行初始化。
194 1
|
算法 安全 Go
Python与Go语言中的哈希算法实现及对比分析
Python与Go语言中的哈希算法实现及对比分析
231 0

热门文章

最新文章