我可能并不会使用golang interface

语言: CN / TW / HK

谈到interface,我们大致应该会有这样的疑问

  • interface是什么?
  • 他跟面向对象语言中的接口有啥区别?
  • 他的底层原理是什么样的?
  • interface的优缺点是什么?
  • interface有哪些常见的应用场景?

上述大概涵盖了,我们的主要的疑问,有问题是好事儿,我们慢慢来看看。

1.什么是interface

在Go中,接口是一组方法签名。 当类型为接口中的所有方法提供定义时,就说实现了该接口。它与OOP世界非常相似。 接口指定类型应具有的方法,类型决定如何实现这些方法。

例如, WashingMachine 可以是具有方法签名 Cleaning()Drying() 的接口。 任何提供 Cleaning()Drying() 方法定义的类型都可以说是实现了 WashingMachine 接口。

2. 和其他语言中的接口的异同

很多面向对象语言都有接口这一概念,例如 Java 和 C#。Java 的接口不仅可以定义方法签名,还可以定义变量,这些定义的变量可以直接在实现接口的类中使用:

public interface PersonInterface {
    public String name = "defalut";
    public void sayHello();
}
复制代码

上述代码定义了一个必须实现的方法 sayHello 和一个会注入到实现类的变量 name 。在下面的代码中, PersonInterfaceImpl 就实现了 PersonInterface 接口:

public class PersonInterfaceImpl implements PersonInterface {
    public void sayHello() {
        System.out.println(MyInterface.hello);
    }
}
复制代码

Java 中的类必须通过上述方式显式地声明实现的接口,但是在 Go 语言中实现接口就不需要使用类似的方式。首先,我们简单了解一下在 Go 语言中如何定义接口。定义接口需要使用 interface 关键字,在接口中我们只能定义方法签名,不能包含成员变量,一个常见的 Go 语言接口是这样的:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

复制代码

如果一个类型需要实现 Handler 接口,那么它只需要实现 ServeHTTP(ResponseWriter, *Request) 方法,下面的 "github.com/julienschmidt/httprouter" 软件包的 Router 结构体就是 ServeHTTP 接口的一个实现:

// ServeHTTP makes the router implement the http.Handler interface.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)
复制代码

细心的读者可能会发现上述代码根本就没有 Handler 接口的影子,这是为什么呢?Go 语言中接口的实现都是隐式的,我们只需要实现 ServeHTTP(ResponseWriter, *Request) 方法实现了 Handler 接口。Go 语言实现接口的方式与 Java 完全不同:

  • 在 Java 中:实现接口需要显式的声明接口并实现所有方法;
  • 在 Go 中:实现接口的所有方法就隐式的实现了接口;

我们使用上述 Router 结构体时并不关心它实现了哪些接口,Go 语言只会在传递参数、返回参数以及变量赋值时才会对某个类型是否实现接口进行检查.

3. 他的底层原理是什么样的?

接口也是 Go 语言中的一种类型,它能够出现在变量的定义、函数的入参和返回值中并对它们进行约束。但是空接口类型 interface{} 是一个特殊的类型,他能够作为任何一种类型的接受类型。为了更好的深入后面的内容,我们先来了解一下函数和方法调用: Go中有4种不同类型的函数:

  • 顶级函数
  • 值接受者函数
  • 指针接受者函数
  • 函数字面量

5种不同类型的调用:

  • 直接调用顶级函数
  • 直接调用值接受者函数
  • 直接调用指针接受者函数
  • 接口上方法的间接调用
  • 函数值的间接调用

它们混合在一起,构成了功能和调用类型的10种可能的组合:

  • 直接调用顶级函数/
  • 直接调用带有值接收器的方法/
  • 直接调用带有指针接收器的方法/
  • 接口上方法的间接调用/包含值方法的值/
  • 接口上的方法的间接调用/包含带有值方法的指针
  • 接口上的方法的间接调用/包含带有指针方法的指针
  • 间接调用func值/设置为顶级func
  • 间接调用func值/设置为value方法
  • 间接调用func值/设置为指针方法
  • 间接调用func值/设置为字面量func

(斜杠将编译时已知的内容与仅在运行时发现的内容分隔开。)

我们将首先花几分钟来回顾这三种直接调用,然后在本章的其余部分中,我们将重点转移到接口和间接方法调用上。 我们不会在本章中介绍函数字面量,因为这样做首先需要我们熟悉闭包的机制..我们将不可避免地在适当的时候这样做。

3.1的直接调用概述

看一下下面的例子:

package main

func Add(a, b int32) int32 {
	return a + b 
}

type Adder struct{
	id int32 
}
//go:noinline
func (adder *Adder) AddPtr(a, b int32) int32 {
	return a + b
}
//go:noinline
func (adder Adder) AddVal(a, b int32) int32 {
	return a + b
}

func main() {
    Add(10, 32) // direct call of top-level function

    adder := Adder{id: 6754}
    adder.AddPtr(10, 32) // direct call of method with pointer receiver
    adder.AddVal(10, 32) // direct call of method with value receiver

    (&adder).AddVal(10, 32) // implicit dereferencing
}
复制代码

让我们快速查看为这4个调用中的每个调用生成的代码。

  • 直接调用顶级函数
0x0021 00033 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:41)	PCDATA	$0, $0
	0x0021 00033 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:41)	MOVQ	$137438953482, AX
	0x002b 00043 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:41)	MOVQ	AX, (SP)
	0x002f 00047 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:41)	CALL	"".Add(SB)
	0x0034 00052 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:43)	MOVL	$0, "".adder+24(SP)
复制代码

正如我们从第一章已经知道的那样,我们看到这转化为直接跳转到.text节中的全局函数符号,并将参数和返回值存储在调用者的堆栈框架中。

直接调用顶级函数:直接调用顶级函数会传递堆栈上的所有参数,并期望结果占据后续的堆栈位置。

  • 直接调用带有指针接收器的方法

首先,接收器通过 adder := Adder{id: 6754}

0x003c 00060 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:43)	MOVL	$6754, "".adder+24(SP)
复制代码

(我们的堆栈帧上的多余空间已作为帧指针前导码的一部分进行了预先分配,为简洁起见,此处未显示。) 然后是对 adder.AddPtr(10, 32) 的实际方法调用:

0x0044 00068 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	PCDATA	$2, $1
	0x0044 00068 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	LEAQ	"".adder+24(SP), AX
	0x0049 00073 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	PCDATA	$2, $0
	0x0049 00073 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	MOVQ	AX, (SP)
	0x004d 00077 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	MOVQ	$137438953482, AX
	0x0057 00087 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	MOVQ	AX, 8(SP)
	0x005c 00092 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:44)	CALL	"".(*Adder).AddPtr(SB)
复制代码

查看汇编输出,我们可以清楚地看到,对方法的调用(无论它具有值接收器还是指针接收器)与函数调用几乎相同,唯一的区别是接收器作为第一个参数传递。 在这种情况下,我们通过在帧的顶部加载有效地址(LEAQ) "".adder+28(SP) 的来做到这一点,从而使第一个参数成为· &adder . 请注意,编译器如何编码接收器的类型,以及它是直接在符号名称中的值还是指针:

"".(*Adder).AddPtr
复制代码

直接调用方法:为了对func值的间接调用和直接调用使用相同的生成代码,选择为方法(值和指针接收器)生成的代码,使其具有与顶层函数相同的调用约定。 以接收者为主导。

  • 使用值接收器直接调用方法

如我们所料,使用值接收器会产生与上面非常相似的代码。 看一下 adder.AddVal(10, 32) :

0x0061 00097 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:45)	MOVL	"".adder+24(SP), AX
	0x0065 00101 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:45)	MOVL	AX, (SP)
	0x0068 00104 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:45)	MOVQ	$137438953482, AX
	0x0072 00114 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:45)	MOVQ	AX, 4(SP)
	0x0077 00119 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:45)	CALL	"".Adder.AddVal(SB)
复制代码

不过,看起来似乎有些棘手:生成的程序集甚至在任何地方都没有引用 "".adder + 28(SP) ,即使我们的接收器当前位于该位置。 那么,这里到底发生了什么? 好吧,由于接收者是一个值,并且由于编译器能够静态推断该值,它不会从当前位置 (28(SP)) 复制现有值,而是直接在堆栈上创建一个新的,相同的 Adder 值,并将此操作与第二个参数的创建合并以保存 在此过程中再增加一条指令。再次注意该方法的符号名如何明确表示它期望值接收器。

隐式取消引用

我们还没有看到最后一个调用: (&adder).AddVal(10,32) .在这种情况下,我们使用指针变量来调用一个期望值接收器的方法。 Go会以某种方式自动取消引用指针并设法进行调用。 为何如此?

编译器如何处理这种情况取决于所指向的接收方是否已转义到堆中。

情况1:接收者在堆栈上 如果接收器仍在堆栈上,并且其大小足够小,可以按几条指令进行复制(如此处的情况),则编译器只需将其值复制到堆栈的顶部,然后对它进行简单的方法调用即可。 无聊(尽管有效)。 让我们继续进行案例B。

情况2:接收者在堆上

如果接受者已经逃逸到了堆上,编译器需要采用一个巧妙的方法:它将产生一个新的方法(带有指针接受者),包裹 "".Adder.AddVal ,并且替换原始被包裹者调用 "".Adder.AddVal 为一个包裹者调用 "".(*Adder).AddVal

因此,包装器的唯一任务是确保接收者在传递给包装器之前已被正确解除引用,并且确保所涉及的所有参数和返回值都在调用者和包装器之间正确地来回复制。

注意:在汇编输出中,这些包装器方法被标记为<autogenerated>

下面是生成的包装器的带注释的清单,希望能帮助您理清头绪

"".(*Adder).AddVal STEXT dupok size=147 args=0x18 locals=0x28
	0x0000 00000 (<autogenerated>:1)	TEXT	"".(*Adder).AddVal(SB), DUPOK|WRAPPER|ABIInternal, $40-24
	... // 省略其他部分
	0x0026 00038 (<autogenerated>:1)	MOVL	$0, "".~r2+64(SP)
	0x002e 00046 (<autogenerated>:1)	CMPQ	""..this+48(SP), $0 // 检测接受者是否为空
	0x0034 00052 (<autogenerated>:1)	JNE	56
	0x0036 00054 (<autogenerated>:1)	JMP	115      // 如果为nil,跳到115 panic
	0x0038 00056 (<autogenerated>:1)	PCDATA	$2, $1
	0x0038 00056 (<autogenerated>:1)	PCDATA	$0, $1
	0x0038 00056 (<autogenerated>:1)	MOVQ	""..this+48(SP), AX
	0x003d 00061 (<autogenerated>:1)	TESTB	AL, (AX)
	0x003f 00063 (<autogenerated>:1)	PCDATA	$2, $0
	0x003f 00063 (<autogenerated>:1)	MOVL	(AX), AX  // 解引用指针接收器
	0x0041 00065 (<autogenerated>:1)	MOVL	AX, ""..autotmp_5+24(SP)
	0x0045 00069 (<autogenerated>:1)	MOVL	AX, (SP)  // 并将参数值移到参数1
	0x0048 00072 (<autogenerated>:1)	MOVL	"".a+56(SP), AX
	0x004c 00076 (<autogenerated>:1)	MOVL	AX, 4(SP)
	0x0050 00080 (<autogenerated>:1)	MOVL	"".b+60(SP), AX
	0x0054 00084 (<autogenerated>:1)	MOVL	AX, 8(SP)
	0x0058 00088 (<autogenerated>:1)	CALL	"".Adder.AddVal(SB)  // 调用被包装者方法
	0x005d 00093 (<autogenerated>:1)	MOVL	16(SP), AX  // copy被包装这返回值
	0x0061 00097 (<autogenerated>:1)	MOVL	AX, ""..autotmp_4+28(SP)
	0x0065 00101 (<autogenerated>:1)	MOVL	AX, "".~r2+64(SP)
	0x0069 00105 (<autogenerated>:1)	MOVQ	32(SP), BP
	0x006e 00110 (<autogenerated>:1)	ADDQ	$40, SP
	0x0072 00114 (<autogenerated>:1)	RET
	0x0073 00115 (<autogenerated>:1)	CALL	runtime.panicwrap(SB)
	0x0078 00120 (<autogenerated>:1)	UNDEF
复制代码

显然,考虑到为了往返传递参数而需要进行的所有复制,这种包装器可能会导致相当多的开销。 特别是在被包装的只是一些指令的情况下。 幸运的是,实际上,编译器会直接将包装内联到包装器中以分摊这些成本(至少在可行时)。

请注意符号定义中的 WRAPPER 指令,该指令指示该方法不应出现在回溯中(以免使最终用户感到困惑),也不能从被包装者引发的恐慌中恢复 。

WRAPPER:这是一个包装函数,不应视为禁用恢复。

如果包装的接收者为 nil ,则 runtime.panicwrap 函数会引发恐慌,这很容易解释。 这是其完整列表,以供参考

// 如果通过一个nil指针接受者调用被包装的值方法panicwrap将产生恐慌
// 从生成的包装器代码中调用它。
func panicwrap() {
	pc := getcallerpc()
	name := funcname(findfunc(pc))
	// name is something like "main.(*T).F".
	// We want to extract pkg ("main"), typ ("T"), and meth ("F").
	// Do it by finding the parens.
	i := bytealg.IndexByteString(name, '(')
	if i < 0 {
		throw("panicwrap: no ( in " + name)
	}
	pkg := name[:i-1]
	if i+2 >= len(name) || name[i-1:i+2] != ".(*" {
		throw("panicwrap: unexpected string after package name: " + name)
	}
	name = name[i+2:]
	i = bytealg.IndexByteString(name, ')')
	if i < 0 {
		throw("panicwrap: no ) in " + name)
	}
	if i+2 >= len(name) || name[i:i+2] != ")." {
		throw("panicwrap: unexpected string after type name: " + name)
	}
	typ := name[:i]
	meth := name[i+2:]
	panic(plainError("value method " + pkg + "." + typ + "." + meth + " called using nil *" + typ + " pointer"))
}
复制代码

这就是函数和方法调用的全部内容,我们现在将重点介绍主要内容:接口。

3.2 接口的解析

  • 数据结构的概况 在理解它们如何工作之前,我们首先需要构建组成接口的数据结构的心智模型,以及它们在内存中是如何布局的。 为此,我们将快速浏览一下 runtime 包,以了解从Go实现的角度来看,接口实际上是什么样子的。

iface 结构体

type iface struct {
	tab  *itab
	data unsafe.Pointer
}
复制代码

因此,接口是一个非常简单的结构,它维护2个指针:

  • tab 保存了一个 itab 对象的地址,它嵌入了描述接口类型和它所指向的数据类型的数据结构。
  • data 是指向该接口保存的值的原始(例如: unsafe )指针。

虽然这个定义非常简单,但它已经为我们提供了一些有价值的信息:因为接口只能保存指针,所以我们封装到接口中的任何具体值都必须有它的地址。

通常,这会导致堆分配,因为编译器采用保守的路由并迫使接收器转义。

即使标量类型也是如此!

package main


type Addifier interface{ 
	Add(a, b int32) int32 
}

type Adder struct{ 
	name string 
}


//go:noinline
func (adder Adder) Add(a, b int32) int32 {
	return a + b 
}

func main() {
    adder := Adder{name: "myAdder"}
    adder.Add(10, 32)	      // doesn't escape
    Addifier(adder).Add(10, 32) // escapes
}
复制代码
➜  simpletest go tool compile -m demo2.go
demo2.go:14:7: Adder.Add adder does not escape
demo2.go:21:13: Addifier(adder) escapes to heap
<autogenerated>:1: (*Adder).Add .this does not escape
<autogenerated>:1: leaking param: .this
➜  simpletest
复制代码

我们可以清楚地看到,每次创建新的 Addifier 接口并使用我们的 adder 变量对其进行初始化时,实际上都会发生 sizeof(Adder) 的堆分配。

在本章的后面,我们将看到与接口一起使用时,即使简单的标量类型也可以导致堆分配。

让我们将注意力转向下一个数据结构: itab

// 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
	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 inter.
}
复制代码

itab 是接口类型的核心。

首先,它嵌入 _type ,它是运行时内任何Go类型的内部表示。

_type 描述类型的每个方面:其名称,其特征(例如大小,对齐方式...),以及某种程度上的行为方式(例如比较,哈希...)!

在本示例下, _type 字段描述了接口保存的值的类型,即 data 指针指向的值。

其次,我们找到一个指向 interfacetype 的指针,它只是 _type 的包装,其中包含一些特定于接口的额外信息。

如您所料, inter 字段描述了接口本身的类型。

最后, fun 数组包含构成接口的虚拟/调度表的函数指针。

请注意, // variable sized 的注释,这意味着声明此数组的大小无关紧要。我们将在本章后面看到,编译器负责分配支持该数组的内存,并且独立于此处指示的大小进行分配。 同样,运行时始终使用原始指针访问此数组,因此边界检查不适用于此处。

_type 数据结构

// Needs to be in sync with ../cmd/link/internal/ld/decodesym.go:/^func.commonsize,
// ../cmd/compile/internal/gc/reflect.go:/^func.dcommontype and
// ../reflect/type.go:/^type.rtype.
type _type struct {
	size       uintptr
	ptrdata    uintptr // size of memory prefix holding all pointers
	hash       uint32
	tflag      tflag
	align      uint8
	fieldalign uint8
	kind       uint8
	alg        *typeAlg
	// 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
}
复制代码

如上所述, _type 结构给出了Go类型的完整描述。值得庆幸的是,这些字段大多数都是不言而喻的。

nameOfftypeOff 类型是链接器嵌入到最终可执行文件中的元数据的int32偏移量。该元数据在运行时加载到 runtime.moduledata 结构中,如果您曾经查看过ELF文件的内容,那么它应该看起来非常相似。

运行时提供了一些帮助程序,这些帮助程序实现了必要的逻辑,以便通过 moduledata 结构跟踪这些偏移量,例如 resolveNameOffresolveTypeOff .

func resolveNameOff(ptrInModule unsafe.Pointer, off nameOff) name {}
func resolveTypeOff(ptrInModule unsafe.Pointer, off typeOff) *_type {}
复制代码

即,假设 t_type ,则调 用resolveTypeOff(t,t.ptrToThis) 返回 t 的副本。

interfacetype 结构体:

type interfacetype struct {
	typ     _type
	pkgpath name
	mhdr    []imethod
}

type imethod struct {
	name nameOff
	ityp typeOff
}
复制代码

如前所述, interfacetype 只是一个 _type 的包装器,在其上添加了一些额外的特定于接口的元数据。

在当前的实现中,此元数据主要由偏移量列表组成,这些偏移量指向接口 ([]imethod) 公开的方法的相应名称和类型。

这是iface内联所有子类型表示时的外观的概述。 希望这将有助于连接所有的点:

type iface struct { // `iface`
    tab *struct { // `itab`
        inter *struct { // `interfacetype`
            typ struct { // `_type`
                size       uintptr
                ptrdata    uintptr
                hash       uint32
                tflag      tflag
                align      uint8
                fieldalign uint8
                kind       uint8
                alg        *typeAlg
                gcdata     *byte
                str        nameOff
                ptrToThis  typeOff
            }
            pkgpath name
            mhdr    []struct { // `imethod`
                name nameOff
                ityp typeOff
            }
        }
        _type *struct { // `_type`
            size       uintptr
            ptrdata    uintptr
            hash       uint32
            tflag      tflag
            align      uint8
            fieldalign uint8
            kind       uint8
            alg        *typeAlg
            gcdata     *byte
            str        nameOff
            ptrToThis  typeOff
        }
        hash uint32
        _    [4]byte
        fun  [1]uintptr
    }
    data unsafe.Pointer
}
复制代码

本节介绍构成接口的不同数据类型,以帮助我们开始构建涉及整个机械的各种齿轮的思维模型,以及它们如何相互配合。

在下一节中,我们将学习如何实际计算这些数据结构。

3.3 创建一个接口

现在,我们已经快速浏览了所有涉及的数据结构,我们将集中讨论如何实际分配和初始化它们。

package main


type Mather interface {
    Add(a, b int32) int32
    Sub(a, b int64) int64
}

type Adder struct{
	id int32
}
//go:noinline
func (adder Adder) Add(a, b int32) int32 {
	return a + b
}
//go:noinline
func (adder Adder) Sub(a, b int64) int64 {
	return a - b
}

func main() {
    m := Mather(Adder{id: 6754})

    // 这个调用仅仅确定接口是使用的,
    // 没有这个调用,连接器将看到接口是定义了的,但是事实上并没有被使用。
    // 并因此会被优化掉
    m.Add(10, 32)
}
复制代码

NOTE: For the remainder of this chapter, we will denote an interface I that holds a type T as <I,T>. E.g. Mather(Adder{id: 6754}) instantiates an iface<Mather, Adder>.

注意:接下来,我们将使用<I,T>标识一个持有T类型的接口I,例如Mather(Adder{id:6754})实例一个iface为<Mather,Adder>

让我们放大一下的实例化 iface<Mather, Adder> :

m := Mather(Adder{id: 6754})
复制代码

这行Go代码实际上引起了相当多的麻烦,因为编译器生成的汇编清单可以证明:

0x001d 00029 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVL	$0, ""..autotmp_1+28(SP)
	0x0025 00037 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVL	$6754, ""..autotmp_1+28(SP)
	0x002d 00045 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVL	$6754, (SP)
	0x0034 00052 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	CALL	runtime.convT32(SB)
	0x0039 00057 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	PCDATA	$2, $1
	0x0039 00057 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVQ	8(SP), AX
	0x003e 00062 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVQ	AX, ""..autotmp_2+32(SP)
	0x0043 00067 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	PCDATA	$2, $2
	0x0043 00067 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	LEAQ	go.itab."".Adder,"".Mather(SB), CX
	0x004a 00074 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVQ	CX, "".m+40(SP)
	0x004f 00079 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVQ	AX, "".m+48(SP)
复制代码

我们分为三部分来说明

  • 1.分配接受者
0x0025 00037 (/Users/zhaojunwei/workspace/src/just.for.test/interface/simpletest/demo2.go:16)	MOVL	$6754, ""..autotmp_1+28(SP)
复制代码

常数十进制值6754(与我们的 Adder ID对应)存储在当前堆栈帧的开头。它存储在此处,以便编译器以后可以通过其地址引用它。 我们将在第3部分中了解原因。

  • 2.设置itab
0x0043 00067 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	LEAQ	go.itab."".Adder,"".Mather(SB), CX
	0x004a 00074 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$0, $1
	0x004a 00074 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVQ	CX, "".m+40(SP)

复制代码

看起来编译器已经创建必要的 itab 代表 iface<Mater,Adder> 接口,并使它通过一个全局的代码 go.itab."".Adder,"".Mather ,让我们可以使用。

我们正在构建 iface <Mather,Adder> 接口,为此,我们正在加载此全局 go.itab."".Adder,"".Mather 符号的有效地址在当前堆栈帧的顶部。再一次,我们将在第3部分中看到原因。

从语义上讲,这为我们提供了以下伪代码的含义:

tab := getSymAddr(`go.itab.main.Adder,main.Mather`).(*itab)
复制代码

那就是我们接口的一半!

现在,在我们探讨它的同时,让我们更深入地了解一下 go.itab."".Adder,"".Mather 像往常一样,编译器的-S标志可以告诉我们很多信息:

go.itab."".Adder,"".Mather SRODATA dupok size=40
	0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
	0x0010 8a 3d 5f 61 00 00 00 00 00 00 00 00 00 00 00 00  .=_a............
	0x0020 00 00 00 00 00 00 00 00                          ........
	rel 0+8 t=1 type."".Mather+0
	rel 8+8 t=1 type."".Adder+0
	rel 24+8 t=1 "".(*Adder).Add+0
	rel 32+8 t=1 "".(*Adder).Sub+0
复制代码

整齐。 让我们逐一分析。

第一部分声明该符号及其属性:

go.itab."".Adder,"".Mather SRODATA dupok size=40
复制代码

和往常一样,由于我们直接查看由编译器生成的中间目标文件(即,链接器尚未运行),因此符号名称仍缺少程序包名称。 在这方面没有新内容。

除此之外,我们在这里得到的是一个40字节的全局对象符号,该符号将存储在二进制文件的 .rodata 节中。

请注意 dupok 指令,该指令告诉链接器该符号在链接时多次出现是合法的:链接器将不得不任意选择其中一个。

第二部分是与符号关联的40字节数据的十六进制转储。 即,它是itab结构的序列化表示形式:

0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
	0x0010 8a 3d 5f 61 00 00 00 00 00 00 00 00 00 00 00 00  .=_a............
	0x0020 00 00 00 00 00 00 00 00                          ........
复制代码

如您所见,此时大多数数据只是一堆零。 稍后我们会看到,链接器将负责填充它们。

请注意,在所有这些零中,实际上是如何设置4个字节的,偏移量为 0x10 + 4 。 如果我们回顾一下itab结构的声明并注释其字段的各个偏移量:

type itab struct { // 40 bytes on a 64bit arch
    inter *interfacetype // offset 0x00 ($00)
    _type *_type	 // offset 0x08 ($08)
    hash  uint32	 // offset 0x10 ($16)
    _     [4]byte	 // offset 0x14 ($20)
    fun   [1]uintptr	 // offset 0x18 ($24)
			 // offset 0x20 ($32)
}
复制代码

我们看到偏移量 0x10 + 4 与哈希 uint32 字段匹配:即对应于我们 main.Adder 类型的哈希值已经在目标文件中了。

第三部分也是最后一部分列出了链接器的一堆重定位指令:

rel 0+8 t=1 type."".Mather+0
	rel 8+8 t=1 type."".Adder+0
	rel 24+8 t=1 "".(*Adder).Add+0
	rel 32+8 t=1 "".(*Adder).Sub+0
复制代码

rel 0+8 t=1 type."".Mather+0 告诉链接器使用全局对象符号 type."".Mather 的地址填充首八个字节的内容。

rel 8+8 t=1 type."".Adder+0 使用 type."".Adder 的地址填充接下来的8字节。等等等等

链接器完成其工作并遵循所有这些指令后,我们40字节序列化的itab将完成。总体而言,我们现在正在研究类似于以下伪代码的内容:

tab := getSymAddr(`go.itab.main.Adder,main.Mather`).(*itab)

// 注意:在构建可执行程序时,链接器将去除符号的`type.`前缀,
所以在二进制.rodata部分符号名将是`main.Mather`和`main.Adder`
// 而不是`type.main.Mather` 和 `type.main.Adder`.
// 在玩转objdump时不要被这个绊倒。
tab.inter = getSymAddr(`type.main.Mather`).(*interfacetype)
tab._type = getSymAddr(`type.main.Adder`).(*_type)

tab.fun[0] = getSymAddr(`main.(*Adder).Add`).(uintptr)
tab.fun[1] = getSymAddr(`main.(*Adder).Sub`).(uintptr)
复制代码

我们已经准备好了一个易于使用的itab,现在,如果我们只附带一些数据,那将是一个不错的,完整的接口。

  • 3.设置数据
0x001d 00029 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVL	$0, ""..autotmp_1+28(SP)
	0x0025 00037 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVL	$6754, ""..autotmp_1+28(SP)
	0x002d 00045 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVL	$6754, (SP)
	0x0034 00052 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	CALL	runtime.convT32(SB)
	0x0039 00057 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$0, $1
	0x0039 00057 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVQ	8(SP), AX
	0x003e 00062 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVQ	AX, ""..autotmp_2+32(SP)
	0x0043 00067 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$0, $2
	0x0043 00067 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$1, $1
	0x0043 00067 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	LEAQ	go.itab."".Adder,"".Mather(SB), CX
	0x004a 00074 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$0, $1
	0x004a 00074 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVQ	CX, "".m+40(SP)
	0x004f 00079 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	PCDATA	$0, $0
	0x004f 00079 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:22)	MOVQ	AX, "".m+48(SP)
	0x0054 00084 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	"".m+40(SP), AX
复制代码

在第二部分中,我们已经将一个十进制常数 $6754 存储到 ""..autotmp_1+28(SP) 。这个值将作为参数传递给 runtime.convT32 ,看一下这个函数

func convT32(val uint32) (x unsafe.Pointer) {
	if val == 0 {
		x = unsafe.Pointer(&zeroVal[0])
	} else {
		x = mallocgc(4, uint32Type, false)
		*(*uint32)(x) = val
	}
	return
}
复制代码

从可执行文件重建Itab

在上一节中,我们转储了 go.itab."".Adder,"".Mather 直接从编译器生成的目标文件中查看最终大部分为零的blob(散列值除外):

go.itab."".Adder,"".Mather SRODATA dupok size=40
	0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
	0x0010 8a 3d 5f 61 00 00 00 00 00 00 00 00 00 00 00 00  .=_a............
	0x0020 00 00 00 00 00 00 00 00                          ........
复制代码

为了更好地了解如何将数据布局到链接器生成的最终可执行文件中,我们将遍历生成的ELF文件并手动重建构成 iface <Mather,Adder> 的itab的字节。 希望这将使我们能够在链接器完成工作后观察itab的外观。

首先,让我们构建iface二进制文件: GOOS = linux GOARCH = amd64 go build -o iface.bin iface.go

  • 1.寻找 .rodata

让我们打印部分标题以搜索 .rodatareadelf 可以帮助您:

➜  interfacetest GOOS=linux GOARCH=amd64 go build -o main.bin main.go 
➜  interfacetest readelf -St -W main.bin                             
There are 25 section headers, starting at offset 0x1c8:

节头:
  [号] 名称
       Type            Address          Off    Size   ES   Lk Inf Al
       旗标
  [ 0] 
       NULL            0000000000000000 000000 000000 00   0   0  0
       [0000000000000000]: 
  [ 1] .text
       PROGBITS        0000000000401000 001000 0517ae 00   0   0 16
       [0000000000000006]: ALLOC, EXEC
  [ 2] .rodata
       PROGBITS        0000000000453000 053000 030b00 00   0   0 32
       [0000000000000002]: ALLOC
复制代码

我们真正需要的是该部分的(十进制)偏移量,因此让我们应用一些pipe-foo:

➜  interfacetest readelf -St -W main.bin | \ 
  grep -A 1 .rodata | \
  tail -n +2 | \
  awk '{print "ibase=16;"toupper($3)}' | \
  bc
339968
复制代码

这意味着将315392字节存储到二进制文件中应将我们放在.rodata节的开头。

现在,我们要做的就是将此文件位置映射到虚拟内存地址。

  • 2.查找 .rodata 的虚拟内存地址(VMA)

VMA是虚拟地址,一旦二进制文件已由OS加载到内存中,该节将被映射到该虚拟地址。 也就是说,这是我们在运行时用来引用符号的地址。

➜  interfacetest readelf -St -W main.bin | \ 
  grep -A 1 .rodata | \
  tail -n +2 | \
  awk '{print "ibase=16;"toupper($2)}' | \
  bc
4534272
复制代码

在这种情况下,我们关心VMA的原因是我们无法直接向readelf或objdump请求特定符号(AFAIK)的偏移量。 另一方面,我们所能做的就是索要特定符号的VMA。

结合一些简单的数学运算,我们应该能够在VMA和偏移量之间建立映射,并最终找到所需符号的偏移量。

因此,这就是到目前为止我们所知道的: .rodata 节位于ELF文件中的偏移 $ 315392(= 0x04d000) 处,它将在运行时映射到虚拟地址 $ 4509696(= 0x44d000)

4.动态调度

在本节中,我们最终将介绍接口的主要功能:动态调度。

具体来说,我们将研究动态调度是如何在后台进行的,以及我们需要为此付出多少。

  • 接口上的间接方法调用
package main 


type Mather interface {
    Add(a, b int32) int32
    Sub(a, b int64) int64
}

type Adder struct{
	id int32
}
//go:noinline
func (adder Adder) Add(a, b int32) int32 {
	return a + b
}
//go:noinline
func (adder Adder) Sub(a, b int64) int64 {
	return a - b
}

func main() {
    m := Mather(Adder{id: 6754})

    // This call just makes sure that the interface is actually used.
    // Without this call, the linker would see that the interface defined above
    // is in fact never used, and thus would optimize it out of the final
    // executable.
    m.Add(10, 32)
}
复制代码

我们已经对这段代码中的大部分操作进行了更深入的研究: iface <Mather,Adder> 接口是如何创建的,如何在最终的exectutable中进行布局,以及最终如何在运行时加载 。

我们只剩下一件事要看,那就是随后的实际间接方法调用: m.Add(10,32)

为了刷新我们的记忆,我们将放大接口的创建以及方法调用本身:

m := Mather(Adder{id: 6754})
m.Add(10, 32)
复制代码

值得庆幸的是,我们已经有了由第一行的实例化生成的程序集的完整注释版本( m:= Mather(Adder {id:6754}) ):

0x0054 00084 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	"".m+40(SP), AX
	0x0059 00089 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	TESTB	AL, (AX)
	0x005b 00091 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	24(AX), AX
	0x005f 00095 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	PCDATA	$0, $3
	0x005f 00095 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	PCDATA	$1, $0
	0x005f 00095 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	"".m+48(SP), CX
	0x0064 00100 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	PCDATA	$0, $0
	0x0064 00100 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	CX, (SP)
	0x0068 00104 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	$137438953482, CX
	0x0072 00114 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	CX, 8(SP)
	0x0077 00119 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	CALL	AX
复制代码

借助前几节中积累的知识,这几条说明应该易于理解。

0x005b 00091 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	24(AX), AX
复制代码

通过解引用AX并向前偏移24个字节,我们到达i.tab.fun,它对应于虚拟表的第一个条目。这提醒了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 inter.
}
复制代码

如上一节所述,我们直接从可执行文件中重建了最终的 itabiface.tab.fun [0] 是指向 main.(*Adder).add 的指针,是编译器生成的包装器 -包装我们原始的值接收器 main.Adder.add 方法的方法。

0x0068 00104 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	$137438953482, CX
	0x0072 00114 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	MOVQ	CX, 8(SP)
复制代码

我们在堆栈的顶部存储10和32,作为参数#2和#3。

0x0077 00119 (/Users/zhaojunwei/go/src/workspace/interfacetest/main.go:28)	CALL	AX
复制代码

最后,设置好所有堆栈后,我们便可以进行实际的调用。

现在,我们对接口和虚拟方法调用正常工作所需的整个机器有了清晰的了解。

持续更新中......

欢迎关注我们的微信公众号,每天学习Go知识

分享到: