Golang语言的函数调用信息
函数的调用信息是程序中比较重要运行期信息, 在很多场合都会用到(比如调试或日志).
Go语言 runtime
包的 runtime.Caller
/ runtime.Callers
/ runtime.FuncForPC
等几个函数提供了获取函数调用者信息的方法.
这几个函数的文档链接:
- http://golang.org/pkg/runtime/#Caller
- http://golang.org/pkg/runtime/#Callers
- http://golang.org/pkg/runtime/#FuncForPC
本文主要讲述这几个函数的用法.
runtime.Caller
的用法
函数的签名如下:
runtime.Caller
返回当前 goroutine
的栈上的函数调用信息. 主要有当前的 pc
值和调用的文件和行号等信息. 若无法获得信息, 返回的 ok
值为 false
.
其输入参数 skip
为要跳过的栈帧数, 若为 0
则表示 runtime.Caller
的调用者.
注意:由于历史原因, runtime.Caller
和 runtime.Callers
中的 skip
含义并不相同, 后面会讲到.
下面是一个简单的例子, 打印函数调用的栈帧信息:
其中 skip = 0
为当前文件("caller.go")的 main.main
函数, 以及对应的行号. 这里省略的无关代码, 因此输出的行号和网页展示的位置有些差异.
另外的 skip = 1
和 skip = 2
也分别对应2个函数调用. 通过查阅 runtime/proc.c
文件的代码, 我们可以知道对应的函数分别为 runtime.main
和 runtime.goexit
.
整理之后可以知道, Go的普通程序的启动顺序如下:
-
runtime.goexit
为真正的函数入口(并不是main.main
) - 然后
runtime.goexit
调用runtime.main
函数 - 最终
runtime.main
调用用户编写的main.main
函数
runtime.Callers
的用法
函数的签名如下:
runtime.Callers
函数和 runtime.Caller
函数虽然名字相似(多一个后缀s
), 但是函数的参数/返回值和参数的意义都有很大的差异.
runtime.Callers
把调用它的函数Go程栈上的程序计数器填入切片 pc
中. 参数 skip
为开始在 pc 中记录之前所要跳过的栈帧数, 若为0则表示 runtime.Callers
自身的栈帧, 若为1则表示调用者的栈帧. 该函数返回写入到 pc
切片中的项数(受切片的容量限制).
下面是 runtime.Callers
的例子, 用于输出每个栈帧的 pc
信息:
输出新的 pc
长度和 skip
大小有逆相关性. skip = 0
为 runtime.Callers
自身的信息.
这个例子比前一个例子多输出了一个栈帧, 就是因为多了一个runtime.Callers
栈帧的信息(前一个例子是没有runtime.Caller
信息的(注意:没有s
后缀)).
那么 runtime.Callers
和 runtime.Caller
有哪些关联和差异?
runtime.Callers
和 runtime.Caller
的异同
因为前面2个例子为不同的程序, 输出的 pc
值并不具备参考性. 现在我们看看在同一个例子的输出结果如何:
比如输出结果可以发现, 4280962
和 4290608
两个 pc
值是相同的. 它们分别对应 runtime.main
和 runtime.goexit
函数.
runtime.Caller
输出的 4198456
和 runtime.Callers
输出的 4198635
并不相同. 这是因为, 这两个函数的调用位置并不相同, 因此导致了 pc
值也不完全相同.
最后就是 runtime.Callers
多输出一个 4305334
值, 对应runtime.Callers
内部的调用位置.
由于Go语言(Go1.2)采用分段堆栈, 因此不同的 pc
之间的大小关系并不明显.
runtime.FuncForPC
的用途
函数的签名如下:
其中 runtime.FuncForPC
返回包含给定 pc
地址的函数, 如果是无效 pc
则返回 nil
.
runtime.Func.FileLine
返回与 pc
对应的源码文件名和行号. 安装文档的说明, 如果pc
不在函数帧范围内, 则结果是不确定的.
runtime.Func.Entry
对应函数的地址. runtime.Func.Name
返回该函数的名称.
下面是 runtime.FuncForPC
的例子:
根据测试, 如果是无效 pc
(比如0
), runtime.Func.FileLine
一般会输出当前函数的开始行号. 不过在实践中, 一般会用 runtime.Caller
获取文件名和行号信息, runtime.Func.FileLine
很少用到(如何独立获取pc
参数?).
定制的 CallerName
函数
基于前面的几个函数, 我们可以方便的定制一个 CallerName
函数. 函数 CallerName
返回调用者的函数名/文件名/行号等用户友好的信息.
函数实现如下:
其中在执行 runtime.Caller
调用时, 参数 skip + 1
用于抵消 CallerName
函数自身的调用.
下面是基于 CallerName
的输出例子:
这样就可以方便的输出函数调用者的信息了.
Go语言中函数的类型
在Go语言中, 除了语言定义的普通函数调用外, 还有闭包函数/init函数/全局变量初始化等不同的函数调用类型.
为了便于测试不同类型的函数调用, 我们包装一个 PrintCallerName
函数. 该函数用于输出调用者的信息.
观察输出结果, 可以发现以下几个规律:
- 全局变量的初始化调用者为
main.init
函数 - 自定义的
init
函数有一个数字后缀, 根据出现的顺序进编号. 比如main.init·1
和main.init·2
等. - 闭包函数采用
main.func·001
格式命名, 安装闭包定义结束的位置顺序进编号.
比如以下全局变量的初始化调用者为 main.init
函数:
var a = PrintCallerName(0, "main.a")var b = PrintCallerName(0, "main.b")
以下两个 init
函数根据出现顺序分别对应 main.init·1
和 main.init·2
:
func init() { // main.init·1
//}func init() { // main.init·2
//}
以下三个闭包根据定义结束顺序分别为 001
/ 002
/ 003
:
因为, 这些特殊函数调用方式的存在, 我们需要进一步完善 CallerName
函数.
改进的 CallerName
函数
两类特殊的调用是 init
类函数调用 和 闭包函数调用.
改进后的 CallerName
函数对 init
类函数调用者统一处理为 init
函数. 将闭包函数调用这处理为调用者的函数名.
处理的思路:
- 如果是
init
类型的函数调用(匹配正则表达式"init·d+$"
), 直接作为init
函数范返回 - 如果是
func
闭包类型(匹配正则表达式"func·d+$"
), 跳过当前栈帧, 继续递归处理 - 返回普通的函数调用类型
CallerName
函数的不足之处
有以下的代码:
myInit
为一个全局变量, 被赋值为一个闭包函数. 然后在 init
和 main
函数分别调用 myInit
这个闭包函数输出的结果 会因为调用环境的不同而有差异.
从直观上看, myInit
闭包函数在执行时, 最好输出 main.myInit
函数名. 但是 main.myInit
只是一个绑定到闭包函数的变量, 而闭包的真正名字是 main.func·???
. 在运行时是无法得到 main.myInit
这个名字的.
因此在 gettext-go 中内部用的 callerName
函数采用将 main.func·???
统一处理为 main.func
的, 然后作为 gettext.Gettext
翻译函数的上下文.
gettext-go 的 callerName
函数实现在这里: caller.go. 测试文件在这里: caller_test.go.
不同Go程序启动流程
基于函数调用者信息可以很容易的验证各种环境的程序启动流程.
我们需要建立一个独立的 caller
目录, 里面有三个测试代码.
caller/main.go
主程序:
分析输出数据我们可以发现, 测试代码和例子代码的启动流程和普通的程序流程都不太一样.
测试代码的启动流程:
-
runtime.goexit
还是入口 - 但是
runtime.goexit
不在调用runtime.main
函数, 而是调用testing.tRunner
函数 -
testing.tRunner
函数由go test
命令生成, 用于执行各个测试函数
例子代码的启动流程:
-
runtime.goexit
还是入口 - 然后
runtime.goexit
调用runtime.main
函数 - 最终
runtime.main
调用go test
命令生成的main.main
函数, 在_test/_testmain.go
文件 - 然后调用
testing.Main
, 改函数执行各个例子函数
另外, 从这个例子我们可以发现, 我们自己写的 main.main
函数所在的 main
包也可以被其他包导入. 但是其他包导入之后的 main
包里的 main
函数就不再是main.main
函数了. 因此, 程序的入口也就不是自己写的 main.main
函数了.
2015.06.09补充: 更深入的可以看下这个文章 GO语解惑:从源码分析GO程序的入口
总结
Go语言 runtime
包的 runtime.Caller
/ runtime.Callers
/ runtime.FuncForPC
等函数虽然看起来比较简单, 但是功能却非常强大.
这几个函数不仅可以解决一些实际的工程问题(比如 gettext-go 中用于获取翻译的上下文信息), 而且非常适合用于调试和分析各种Go程序的运行时信息.
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- hadoop数据类型及自定义
- 惊!u202a错误,百分之九十都不知道的隐藏在文件路径里的惊天秘密!(干货收藏)
- 百度站点收录 - 什么叫自动推送
- 虚拟机安装Centos后的一些配置
- CentOS下的JDK安装
- python 技术篇-3行代码搞定图像文字识别,pytesseract库实现
- hadoop2.6.0完全分布式手动安装
- Python 库安装问题:ModuleNotFoundError: No module named 'windows'. 解决方法
- Python各种文件删除函数的功能区分!
- Python 技术篇-轻松操作windows系统电脑鼠标指针移动、点击
- Typora Picgo自动使用图床上传图片
- 【Python】文件的选择性压缩和全压缩,一般人不告诉的实用小技巧!
- 搭建hadoop集群虚拟机试验环境
- PLSQL-简单的语句块及变量的定义
- Python 技术篇-使用PIL库等比例压缩、缩小图片