概念
在计算机程序设计中, 函数其实是一种抽象概念, 是一种编程接口; 通过抽象, 能够实现将复杂的系统分解成各种包装了复杂算法的不透明接口, 方便彼此相互调用, 实现分层, 扩展性, 便利性等等.
具体来讲, 函数一般是指一段独立的, 可重复利用的程序逻辑片段, 用来方便其他函数调用; 英文名称是 function, 有时候也称为 method,routine.
编译器最终将函数编译为机器指令, 保存在可执行文件中.
在进程的内存空间中, 一个函数只不过是一段包含机器指令的连续内存区域; 仅仅从结构上来讲, 和数组没什么区别.
在 Go 语言中, 函数 (function) 是一等公民(first-class citizen), 不仅仅是代码片段, 也是一种数据类型; 和其他数据类型一样有自己的类型信息.
函数类型
函数类型的定义有多处, 它们是等价的.
在 runtime/type.go 源文件中定义如下:
- type functype struct {
- typ _type
- inCount uint16
- outCount uint16
- }
在 reflect/type.go 和 internal/reflectlite/type.go 源文件中定义如下:
- // funcType represents a function type.
- //
- // A *rtype for each in and out parameter is stored in an array that
- // directly follows the funcType (and possibly its uncommonType). So
- // a function type with one method, one input, and one output is:
- //
- // struct {
- // funcType
- // uncommonType
- // [2]*rtype // [0] is in, [1] is out
- // }
- type funcType struct {
- rtype
- inCount uint16
- outCount uint16 // top bit is set if last input parameter is ...
- }
从 funcType 结构体的注释中可以看到, 函数类型的信息其实非常复杂.
其实完整的函数类型定义如下伪代码所示:
- type funcType struct {
- rtype // 基础类型信息
- inCount uint16 // 参数数量
- outCount uint16 // 返回值数量
- uncommon uncommonType // 方法信息
- inTypes [inCount]*rtype // 参数类型列表
- outTypes [outCount]*rtype // 返回值类型列表
- methods [uncommon.mcount]method // 方法列表
- }
uncommonType 和 method 定义在 reflect/type.go 源文件中, 用于存储和解析类型的方法信息.
- type uncommonType struct {
- pkgPath nameOff // 包路径名称偏移量
- mcount uint16 // 方法的数量
- xcount uint16 // 公共导出方法的数量
- moff uint32 // methods 相对本对象起始地址的偏移量
- _ uint32 // unused
- }
- // 非接口类型的方法
- type method struct {
- name nameOff // 方法名称偏移量
- mtyp typeOff // 方法类型偏移量
- ifn textOff // 通过接口调用时的地址偏移量; 接口类型本文不介绍
- tfn textOff // 直接类型调用时的地址偏移量
- }
- type nameOff int32 // offset to a name
- type typeOff int32 // offset to an *rtype
- type textOff int32 // offset from top of text section
nameOff 是相对 .rodata 节起始地址的偏移量.
typeOff 是相对 .rodata 节起始地址的偏移量.
textOff 是相对 .text 节起始地址的偏移量.
关于 reflect.name 的介绍, 请阅读 内存中的整数 .
函数类型结构分布示意图
完整的函数类型信息结构分布如下图所示:
每一种函数都有自己的类型信息, 只不过有的函数简单, 有的函数复杂, 并不是每一种函数类型包含上图中的所有字段.
简单的函数类型信息结构分布可能如下图所示:
或者
备注: 以上示意图中的浅灰色块表示内存对齐的填充, 不存储任何数据.
当然, 函数也可能有参数无返回值, 函数还可能无参数有返回值, 它们的类型信息结构还会有一点点不同, 想象一下, 不过只是一种简化的结构罢了.
通过本文的内存分析, 我们将会了解函数类型的每一个细节.
环境
- OS : Ubuntu 20.04.2 LTS; x86_64
- Go : go version go1.16.2 Linux/amd64
声明
操作系统, 处理器架构, Go 版本不同, 均有可能造成相同的源码编译后运行时的寄存器值, 内存地址, 数据结构等存在差异.
本文仅包含 64 位系统架构下的 64 位可执行程序的研究分析.
本文仅保证学习过程中的分析数据在当前环境下的准确有效性.
本文仅讨论普通函数和声明的函数类型, 不讨论接口, 实现, 闭包等知识点.
代码清单
- package main
- import (
- "errors"
- "fmt"
- "reflect"
- )
- // 声明函数类型
- type calc func(a, b int) (sum int)
- // 私有的方法 -> package scope
- //go:noinline
- func (f calc) foo(a, b int) int {
- return f(a, b) + 1
- }
- // Ree 公共导出的方法 -> public scope
- //go:noinline
- func (f calc) Ree(a, b int) int {
- return f(a, b) - 1
- }
- func main() {
- // 普通函数
- Print(fmt.Printf)
- // 函数类型实例
- var add calc = func(a, b int) (sum int) {
- return a + b
- }
- fmt.Println(add.foo(1, 2))
- fmt.Println(add.Ree(1, 2))
- Print(add)
- // 匿名函数
- Print(func() {
- fmt.Println("hello anonymous function")
- })
- // 方法; 闭包
- f := errors.New("hello error").Error
- Print(f)
- }
- //go:noinline
- func Print(i interface{}) {
- v := reflect.ValueOf(i)
- fmt.Println("类型", v.Type().String())
- fmt.Println("地址", v)
- fmt.Println()
- }
运行效果
以上代码清单, 主要打印输出了四个函数的类型和内存地址.
编译并运行, 输出如下:
在本文的内存分析过程中, 存在许多通过偏移量计算内存地址的操作.
主要涉及到 .text 和 .rodata 两个 section, 在本程序中它们的信息如下:
普通函数
以 fmt.Printf 这个常用的函数为例, 研究普通函数的类型信息.
从上面的运行输出结果可以看到, fmt.Printf 函数类型的字符串表示形式为:
func(string, ...interface {}) (int, error)
动态调试
在 Print 函数入口处设置断点, 查看 fmt.Printf 函数的类型信息.
将 fmt.Printf 函数的类型信息绘制成图表如下:
- rtype.size = 8
- rtype.ptrdata = 8
- rtype.hash = 0xd9fb8597
- rtype.tflag = 2 = reflect.tflagExtraStar
- rtype.align = 8
- rtype.fieldAlign = 8
- rtype.kind = 0x33
- rtype.equal = 0 = nil
- rtype.str = 0x00005c90 => *func(string, ...interface {
- }) (int, error)
- rtype.ptrToThis = 0
- funcType.inCount = 2
- funcType.outCount = 0x8002
- funcType.inTypes = [ 0x4a4860, 0x4a2f80 ]
- funcType.outTypes = [ 0x4a41e0, 0x4a9860 ]
指针常量
函数对象的大小是 8 字节(rtype.size), 而且包含 8 字节的指针数据(rtype.ptrdata), 所以我们可以将函数对象视为指针.
也就是说 fmt.Printf 其实是一个指针, 只不过这个指针是一个不可变的常量. 这与 C/C++ 是一致的, 函数名称就是一个指针常量.
类型名称
rtype.tflag = 2 = reflect.tflagExtraStar
fmt.Printf 函数有自己的数据类型, 但是该类型并没有名称.
数据类别
数据类别 (Kind) 的计算方法如下:
- const kindMask = (1 << 5) - 1
- func (t *rtype) Kind() Kind {
- return Kind(t.kind & kindMask)
- }
- 0x33 & 31 = 19 = reflect.Func
可变参数
fmt.Printf 函数的参数数量 (funcType.inCount) 是 2, 返回值数量也是 2, 可 funcType.outCount 值为什么是 0x8002?
原因是 funcType.outCount 字段不但需要记录函数返回值的数量, 还需要标记函数最后一个参数是否是可变参数类型; 如果是, 将 funcType.outCount 字段值的最高位设置为 1.
在 reflect/type.go 源文件中, 判断可变参数的方法如下:
- func (t *rtype) IsVariadic() bool {
- if t.Kind() != Func {
- panic("reflect: IsVariadic of non-func type" + t.String())
- }
- tt := (*funcType)(unsafe.Pointer(t))
- return tt.outCount&(1<<15) != 0
- }
返回值数量的计算方式是:
outCount := funcType.outCount & (1<<15 - 1)
令人好奇的是, 可变参数标记怎么没有保存在 funcType.outCount 字段中.
参数与返回值类型
在 fmt.Printf 函数定义中, 参数和返回值的类型依次是:
- string
- ...interface{
- }
- int
- error
在内存的函数类型信息中, 保存的是参数和返回值的类型指针; 通过这些指针查看它们的类型信息如下:
通过内存数据可以看到, fmt.Printf 函数的参数和返回值的数据类别 (Kind) 如下:
- reflect.String
- reflect.Slice
- reflect.Int
- reflect.Interface
关于整数及其类型的详细介绍, 请阅读 内存中的整数 .
关于字符串及其类型的详细介绍, 请阅读 内存中的字符串 .
在 Go 语言中, error 比较特殊, 它既是一个关键字, 又是一个接口定义. 关于接口类型, 之后将发布专题文章进行深入解析, 暂不介绍.
关于 slice, 内存中的 slice 一文曾对 []int 进行了详细介绍 .
很明显, fmt.Printf 函数的第二个参数不是[]int, 通过内存数据来看一看具体是什么类型的 slice.
通过上图可以看到, 编译器将源码中的可变参数类型...interface{}编译为[]interface {}, 从而把可变参数变成一个参数.
这种处理可变参数的方式, 和 Java 语言非常相似.
通过对 fmt.Printf 函数的类型深入分析和了解, 我们就很容易理解反射包 (reflect) 中函数相关的接口了; 有兴趣的话可以去看一看源码实现, 相信对比 fmt.Printf 函数的类型信息, 是比较简单的.
- type Type interface {
- ...... // 省略无关接口
- IsVariadic() bool
- NumIn() int
- NumOut() int
- In(i int) Type
- Out(i int) Type
- ...... // 省略无关接口
- }
声明的函数类型
在 Go 语言中, 通过 type 关键字可以定义任何数据类型, 非常非常地强悍.
在本文的代码清单中, 我们就使用 type 关键字定义了 calc 类型, 这明显是一个函数类型.
type calc func (a, b int) (sum int)
这种类型与 fmt.Printf 函数类型有什么区别吗? 使用上述相同的方法, 我们来深入研究下.
动态调试
从内存数据可以看出, calc 类型的 add 变量指向一个匿名函数, 该匿名函数被编译器命名为 main.main.func1.
calc 的类型信息非常复杂, 共 128 个字节, 整理成图表如下:
- rtype.size = 8
- rtype.ptrdata = 8
- rtype.hash = 0x405feca1
- rtype.tflag = 7 = reflect.tflagUncommon | reflect.tflagExtraStar | reflect.tflagNamed
- rtype.align = 8
- rtype.fieldAlign = 8
- rtype.kind = 0x33
- rtype.equal = 0 = nil
- rtype.str = 0x00002253 => *main.calc
- rtype.ptrToThis = 0x0000ec60
- funcType.inCount = 2
- funcType.outCount = 1
- funcType.inTypes = [ 0x4a41e0, 0x4a41e0 ]
- funcType.outTypes = [ 0x4a41e0 ]
- uncommonType.pkgPath = 0x0000034c => main
- uncommonType.mcount = 2
- uncommonType.xcount = 1
- uncommonType.moff = 0x48
- method[0].name = 0x000001a8 => Ree
- method[0].mtyp = 0xffffffff
- method[0].ifn = 0x00098240
- method[0].tfn = 0x00098240
- method[1].name = 0x000001f6 => foo
- method[1].mtyp = 0xffffffff
- method[1].ifn = 0x000981e0
- method[1].tfn = 0x000981e0
类型名称
rtype.tflag 字段包含 reflect.tflagNamed 标记, 表示该类型是有名称的.
calc 类型的名称为 calc, 获取方式定义在 reflect/type.go 源文件中:
- func (t *rtype) hasName() bool {
- return t.tflag&tflagNamed != 0
- }
- func (t *rtype) Name() string {
- if !t.hasName() {
- return ""
- }
- s := t.String()
- i := len(s) - 1
- for i >= 0 && s[i] != '.' {
- i--
- }
- return s[i+1:]
- }
- func (t *rtype) String() string {
- s := t.nameOff(t.str).name()
- if t.tflag&tflagExtraStar != 0 {
- return s[1:]
- }
- return s
- }
类型指针
rtype.ptrToThis = 0x0000ec60
该值是相对程序 .rodata section 的偏移量. 在本程序中,.rodata section 的起始地址是 0x49a000.
calc 类型的指针类型为 * calc, 类型信息保存在地址 0x49a000+0x0000ec60 处. 关于指针类型本文不再进一步介绍.
参数和返回值
calc 类型有 2 个参数和 1 个返回值, 而且都是 int 类型(信息保存在 0x4a41e0 地址处).
类型方法
方法本质上就是函数.
在 A Tour of Go (https://tour.golang.org/methods/1) 中, 对函数的定义为:
A method is a function with a special receiver argument.
calc 是函数类型, 函数类型居然能拥有自己的方法, 确实是巧妙的设计.
calc 类型的 rtype.tflag 字段包含 reflect.tflagUncommon 标记, 表示其类型信息中包含 uncommonType 数据.
uncommonType 对象的大小是 16 字节, calc 类型共有 3 个参数和返回值, 3 个类型指针占 24 个字节, 所以 [mcount]method 相对 uncommonType 对象的偏移是 16 + 24 = 40 字节.
通过计算得到如下结果:
calc 类型的 Ree 方法, 被重命名为 main.calc.Ree, 内存地址是 0x00098240 + 0x401000 = 0x499240. 它是一个导出函数, 所以 reflect.name.bytes[0] = 1.
calc 类型的 foo 方法, 被重命名为 main.calc.foo, 内存地址是 0x000981e0 + 0x401000 = 0x4991e0.
从内存分析结果可以看到, 如果一种数据类型定义了多个方法, 而且有的是名称以大写字母开头公共导出方法, 有的是名称以小写字母开头导私有方法, 那么编译器将公共的导出方法信息排序在前, 私有方法信息排序在后, 然后保存其数据类型信息中. 而且这个结论可以在 reflect/type.go 源码文件中定义的两个方法得到印证:
- func (t *uncommonType) methods() []method {
- if t.mcount == 0 {
- return nil
- }
- return (*[1 << 16]method)(add(unsafe.Pointer(t), uintptr(t.moff), "t.mcount > 0"))[:t.mcount:t.mcount]
- }
- func (t *uncommonType) exportedMethods() []method {
- if t.xcount == 0 {
- return nil
- }
- return (*[1 << 16]method)(add(unsafe.Pointer(t), uintptr(t.moff), "t.xcount > 0"))[:t.xcount:t.xcount]
- }
在本例中还可以看到, 无论是 Ree 方法, 还是 foo 方法, 它们对应的 method.mtyp 字段值都是 0xffffffff, 也就是 -1.
从 runtime/type.go 源文件中 resolveTypeOff 函数的注释可以了解到,-1 表示没有对应的类型信息.
也就是说, calc 类型的 Ree 和 foo 方法虽然也是函数, 但是他们没有对应的函数类型信息.
所以, Go 编译器并没有为每一个函数都生成对应的类型信息, 只是在需要的时候才会生成, 或者是运行时 (runtime) 根据需要生成.
匿名函数
代码清单中, 第三次调用 main.Print 函数输出了一个匿名函数的类型信息. 这个匿名函数没有形成闭包, 所以相对比较简单.
将其内存数据整理成图表如下:
该函数没有参数, 返回值, 方法, 所以其类型信息非常非常的简单. 相信已经不需要进一步介绍了.
总结
通过一步步的内存分析, 对 Go 语言的函数进行了深入的了解, 学习了很多知识, 解开了许多疑惑, 相信在实际开发中必定能游刃有余, 避免一些小坑.
来源: http://developer.51cto.com/art/202110/685999.htm