- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章Go语言之深入理解函数由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
在计算机程序设计中,函数其实是一种抽象概念,是一种编程接口;通过抽象,能够实现将复杂的系统分解成各种包装了复杂算法的不透明接口,方便彼此相互调用,实现分层、扩展性、便利性等等.
具体来讲,函数一般是指一段独立的、可重复利用的程序逻辑片段,用来方便其他函数调用;英文名称是function,有时候也称为method、routine.
编译器最终将函数编译为机器指令,保存在可执行文件中.
在进程的内存空间中,一个函数只不过是一段包含机器指令的连续内存区域;仅仅从结构上来讲,和数组没什么区别.
在Go语言中,函数(function)是一等公民(first-class citizen),不仅仅是代码片段,也是一种数据类型;和其他数据类型一样有自己的类型信息.
函数类型的定义有多处,它们是等价的.
在runtime/type.go源文件中定义如下:
在reflect/type.go和internal/reflectlite/type.go源文件中定义如下:
从funcType结构体的注释中可以看到,函数类型的信息其实非常复杂.
其实完整的函数类型定义如下伪代码所示:
uncommonType和method定义在reflect/type.go源文件中,用于存储和解析类型的方法信息.
函数类型结构分布示意图 。
完整的函数类型信息结构分布如下图所示:
每一种函数都有自己的类型信息,只不过有的函数简单,有的函数复杂,并不是每一种函数类型包含上图中的所有字段.
简单的函数类型信息结构分布可能如下图所示:
或者 。
备注:以上示意图中的浅灰色块表示内存对齐的填充,不存储任何数据.
当然,函数也可能有参数无返回值,函数还可能无参数有返回值,它们的类型信息结构还会有一点点不同,想象一下,不过只是一种简化的结构罢了.
通过本文的内存分析,我们将会了解函数类型的每一个细节.
操作系统、处理器架构、Go版本不同,均有可能造成相同的源码编译后运行时的寄存器值、内存地址、数据结构等存在差异.
本文仅包含 64 位系统架构下的 64 位可执行程序的研究分析.
本文仅保证学习过程中的分析数据在当前环境下的准确有效性.
本文仅讨论普通函数和声明的函数类型,不讨论接口、实现、闭包等知识点.
以上代码清单,主要打印输出了四个函数的类型和内存地址.
编译并运行,输出如下:
在本文的内存分析过程中,存在许多通过偏移量计算内存地址的操作.
主要涉及到 .text 和 .rodata 两个 section,在本程序中它们的信息如下:
以fmt.Printf这个常用的函数为例,研究普通函数的类型信息.
从上面的运行输出结果可以看到,fmt.Printf函数类型的字符串表示形式为:
动态调试 。
在Print函数入口处设置断点,查看fmt.Printf函数的类型信息.
将fmt.Printf函数的类型信息绘制成图表如下:
函数对象的大小是8字节(rtype.size),而且包含8字节的指针数据(rtype.ptrdata),所以我们可以将函数对象视为指针.
也就是说fmt.Printf其实是一个指针,只不过这个指针是一个不可变的常量。这与C/C++是一致的,函数名称就是一个指针常量.
rtype.tflag = 2 = reflect.tflagExtraStar 。
fmt.Printf函数有自己的数据类型,但是该类型并没有名称.
数据类别(Kind)的计算方法如下:
0x33 & 31 = 19 = reflect.Func 。
fmt.Printf函数的参数数量(funcType.inCount)是2,返回值数量也是2,可funcType.outCount值为什么是0x8002?
原因是funcType.outCount字段不但需要记录函数返回值的数量,还需要标记函数最后一个参数是否是可变参数类型;如果是,将funcType.outCount字段值的最高位设置为1.
在reflect/type.go源文件中,判断可变参数的方法如下:
返回值数量的计算方式是:
令人好奇的是,可变参数标记怎么没有保存在funcType.outCount字段中.
在fmt.Printf函数定义中,参数和返回值的类型依次是:
在内存的函数类型信息中,保存的是参数和返回值的类型指针;通过这些指针查看它们的类型信息如下:
通过内存数据可以看到,fmt.Printf函数的参数和返回值的数据类别(Kind)如下:
关于整数及其类型的详细介绍,请阅读 内存中的整数 .
关于字符串及其类型的详细介绍,请阅读 内存中的字符串 .
在Go语言中,error比较特殊,它既是一个关键字,又是一个接口定义。关于接口类型,之后将发布专题文章进行深入解析,暂不介绍.
关于slice,内存中的slice 一文曾对 []int 进行了详细介绍 .
很明显,fmt.Printf函数的第二个参数不是[]int,通过内存数据来看一看具体是什么类型的slice.
通过上图可以看到,编译器将源码中的可变参数类型...interface{}编译为[]interface {},从而把可变参数变成一个参数.
这种处理可变参数的方式,和Java语言非常相似.
通过对fmt.Printf函数的类型深入分析和了解,我们就很容易理解反射包(reflect)中函数相关的接口了;有兴趣的话可以去看一看源码实现,相信对比fmt.Printf函数的类型信息,是比较简单的.
在Go语言中,通过type关键字可以定义任何数据类型,非常非常地强悍.
在本文的代码清单中,我们就使用type关键字定义了calc类型,这明显是一个函数类型.
type calc func (a, b int) (sum int) 。
这种类型与fmt.Printf函数类型有什么区别吗?使用上述相同的方法,我们来深入研究下.
动态调试 。
从内存数据可以看出,calc类型的add变量指向一个匿名函数,该匿名函数被编译器命名为main.main.func1.
calc的类型信息非常复杂,共128个字节,整理成图表如下:
类型名称 。
rtype.tflag字段包含reflect.tflagNamed标记,表示该类型是有名称的.
calc类型的名称为calc,获取方式定义在reflect/type.go源文件中:
类型指针 。
该值是相对程序 .rodata section 的偏移量。在本程序中,.rodata section 的起始地址是 0x49a000.
calc类型的指针类型为*calc,类型信息保存在地址 0x49a000+0x0000ec60处。关于指针类型本文不再进一步介绍.
参数和返回值 。
calc类型有2个参数和1个返回值,而且都是int类型(信息保存在0x4a41e0地址处).
类型方法 。
方法本质上就是函数.
在 A Tour of Go (https://tour.golang.org/methods/1) 中,对函数的定义为:
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源码文件中定义的两个方法得到印证:
在本例中还可以看到,无论是Ree方法,还是foo方法,它们对应的method.mtyp字段值都是0xffffffff,也就是 -1.
从runtime/type.go源文件中resolveTypeOff函数的注释可以了解到,-1表示没有对应的类型信息.
也就是说,calc类型的Ree和foo方法虽然也是函数,但是他们没有对应的函数类型信息.
所以,Go编译器并没有为每一个函数都生成对应的类型信息,只是在需要的时候才会生成,或者是运行时(runtime)根据需要生成.
代码清单中,第三次调用main.Print函数输出了一个匿名函数的类型信息。这个匿名函数没有形成闭包,所以相对比较简单.
将其内存数据整理成图表如下:
该函数没有参数、返回值、方法,所以其类型信息非常非常的简单。相信已经不需要进一步介绍了.
通过一步步的内存分析,对Go语言的函数进行了深入的了解,学习了很多知识,解开了许多疑惑,相信在实际开发中必定能游刃有余,避免一些小坑.
原文链接:https://mp.weixin.qq.com/s/j0YYBnGsbEcD-WrKRDRSAw 。
最后此篇关于Go语言之深入理解函数的文章就讲到这里了,如果你想了解更多关于Go语言之深入理解函数的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
至少在某些 ML 系列语言中,您可以定义可以执行模式匹配的记录,例如http://learnyouahaskell.com/making-our-own-types-and-typeclasses -
这可能是其他人已经看到的一个问题,但我正在尝试寻找一种专为(或支持)并发编程而设计的语言,该语言可以在 .net 平台上运行。 我一直在 erlang 中进行辅助开发,以了解该语言,并且喜欢建立一个稳
As it currently stands, this question is not a good fit for our Q&A format. We expect answers to be
我正在寻找一种进程间通信工具,可以在相同或不同系统上运行的语言和/或环境之间使用。例如,它应该允许在 Java、C# 和/或 C++ 组件之间发送信号,并且还应该支持某种排队机制。唯一明显与环境和语言
我有一些以不同语言返回的文本。现在,客户端返回的文本格式为(en-us,又名美国英语): Stuff here to keep. -- Delete Here -- all of this below
问题:我希望在 R 中找到类似 findInterval 的函数,它为输入提供一个标量和一个表示区间起点的向量,并返回标量落入的区间的索引。例如在 R 中: findInterval(x = 2.6,
我是安卓新手。我正在尝试进行简单的登录 Activity ,但当我单击“登录”按钮时出现运行时错误。我认为我没有正确获取数据。我已经检查过,SQLite 中有一个与该 PK 相对应的数据。 日志猫。
大家好,感谢您帮助我。 我用 C# 制作了这个计算器,但遇到了一个问题。 当我添加像 5+5+5 这样的东西时,它给了我正确的结果,但是当我想减去两个以上的数字并且还想除或乘以两个以上的数字时,我没有
关闭。此题需要details or clarity 。目前不接受答案。 想要改进这个问题吗?通过 editing this post 添加详细信息并澄清问题. 已关闭 4 年前。 Improve th
这就是我所拥有的 #include #include void print(int a[], int size); void sort (int a[], int size); v
你好,我正在寻找我哪里做错了? #include #include int main(int argc, char *argv[]) { int account_on_the_ban
嘿,当我开始向数组输入数据时,我的代码崩溃了。该程序应该将数字读入数组,然后将新数字插入数组中,最后按升序排列所有内容。我不确定它出了什么问题。有人有建议吗? 这是我的代码 #include #in
我已经盯着这个问题好几个星期了,但我一无所获!它不起作用,我知道那么多,但我不知道为什么或出了什么问题。我确实知道开发人员针对我突出显示的行吐出了“错误:预期表达式”,但这实际上只是冰山一角。如果有人
我正在编写一个点对点聊天程序。在此程序中,客户端和服务器功能写入一个唯一的文件中。首先我想问一下我程序中的机制是否正确? I fork() two processes, one for client
基本上我需要找到一种方法来发现段落是否以句点 (.) 结束。 此时我已经可以计算给定文本的段落数,但我没有想出任何东西来检查它是否在句点内结束。 任何帮助都会帮助我,谢谢 char ch; FI
我的函数 save_words 接收 Armazena 和大小。 Armazena 是一个包含段落的动态数组,size 是数组的大小。在这个函数中,我想将单词放入其他称为单词的动态数组中。当我运行它时
我有一个结构 struct Human { char *name; struct location *location; int
我正在尝试缩进以下代码的字符串输出,但由于某种原因,我的变量不断从文件中提取,并且具有不同长度的噪声或空间(我不确定)。 这是我的代码: #include #include int main (v
我想让用户选择一个选项。所以我声明了一个名为 Choice 的变量,我希望它输入一个只能是 'M' 的 char 、'C'、'O' 或 'P'。 这是我的代码: char Choice; printf
我正在寻找一种解决方案,将定义和变量的值连接到数组中。我已经尝试过像这样使用 memcpy 但它不起作用: #define ADDRESS {0x00, 0x00, 0x00, 0x00, 0x0
我是一名优秀的程序员,十分优秀!