Tony Bai · Go 语言第一课
Tony Bai
资深架构师,tonybai.com 博主
21492 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 59 讲
开篇词 (1讲)
结束语 (1讲)
Tony Bai · Go 语言第一课
15
15
1.0x
00:00/00:00
登录|注册

27|即学即练:跟踪函数调用链,理解代码更直观

使用build tag控制Trace功能
将AST转换回Go源码
在AST中注入Trace
解析Go源码为AST
instrument命令行工具
example_test.go
trace.go
go mod init
改进Trace函数输出格式
使用缩进表示层次
输出带Goroutine ID的日志
获取Goroutine ID
runtime.FuncForPC
runtime.Caller
使用runtime包
需手动调用Trace函数
缺少层次感
并发应用中Goroutine混淆
手动传入函数名
函数入口与出口日志
defer语句
Trace函数
使用defer跟踪函数执行
跟踪函数执行过程
延迟释放资源
捕捉panic
函数与方法设计
变量声明
为Trace增加开关功能
不建议依赖Goroutine ID
Trace代码不适合生产环境
运行注入Trace的代码
对Go源文件注入Trace
构建instrument工具
实现自动注入工具
创建独立的trace模块
输出层次感
增加Goroutine标识
自动获取函数名
问题与改进
实现简单的函数跟踪
引子
defer关键字
Go语言基础
思考题
注意事项
使用instrument工具
自动注入Trace函数
解决方案
实战项目
基础知识
Go语言函数调用链跟踪工具

该思维导图由 AI 生成,仅供参考

你好,我是 Tony Bai。
时间过得真快!转眼间我们已经完成了这门课基础篇 Go 语法部分的学习。在这一部分中,我们从变量声明开始,一直学到了 Go 函数与方法的设计,不知道你掌握得怎么样呢?基础篇的重点是对 Go 基础语法部分的理解,只有理解透了,我们才能在动手实践的环节运用自如。
同时,基础篇也是整个课程篇幅最多的部分,想必学到这里,你差不多也进入了一个“疲劳期”。为了给你的大脑“充充电”,我在这一讲,也就是基础篇的最后一讲中安排了一个小实战项目,适当放松一下,也希望你在实现这个实战项目的过程中,能对基础篇所学的内容做一个回顾与总结,夯实一下 Go 的语法基础。

引子

在前面的第 23 讲中,我曾留过这样的一道思考题:“除了捕捉 panic、延迟释放资源外,我们日常编码中还有哪些使用 defer 的小技巧呢?”
这个思考题得到了同学们的热烈响应,有同学在留言区提到:使用 defer 可以跟踪函数的执行过程。没错!这的确是 defer 的一个常见的使用技巧,很多 Go 教程在讲解 defer 时也会经常使用这个用途举例。那么,我们具体是怎么用 defer 来实现函数执行过程的跟踪呢?这里,我给出了一个最简单的实现:
// trace.go
package main
func Trace(name string) func() {
println("enter:", name)
return func() {
println("exit:", name)
}
}
func foo() {
defer Trace("foo")()
bar()
}
func bar() {
defer Trace("bar")()
}
func main() {
defer Trace("main")()
foo()
}
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

文章介绍了如何使用defer函数来跟踪函数调用链,并解决相关问题。通过具体的代码示例和问题解决过程,帮助读者更好地理解了如何使用defer函数来实现函数执行过程的跟踪。作者提出了一些问题,并逐一分析并解决了这些问题,如手动显式传入要跟踪的函数名、并发应用中函数链跟踪混在一起无法分辨等。通过借助Go标准库runtime包,实现了自动获取所跟踪函数的函数名,解决了手动显式传入函数名的问题。随后,文章进一步讨论了如何支持多Goroutine函数调用链的跟踪,并实现了输出带有Goroutine ID的函数跟踪信息。最终,通过增加缩进层次信息,实现了输出具有层次感的函数调用跟踪信息。整体而言,本文通过具体的代码示例和问题解决过程,帮助读者更好地理解了如何使用defer函数来跟踪函数调用链,以及如何解决相关问题。文章还介绍了如何利用instrument工具自动注入跟踪代码,为读者提供了实际操作的示例。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Tony Bai · Go 语言第一课》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(29)

  • 最新
  • 精选
  • Darren
    老师,问个小白的问题哈,就是Java和Python都支持注解增加能力,不会修改源代码。 我看您这节课的最终版本,就是工具修改源代码,那么go有没有类似Java和Python那种注解的增强能力?如果没有,那么是因为什么原因不支持呀?

    作者回复: go原生不支持注解功能。官方对此原因没有任何说明。go支持struct tag,一定程度具备了annotation的性质。

    2021-12-24
    4
    9
  • 路边的猪
    var mgroup = make(map[uint64]int) var mutex sync.Mutex func Trace() func() { pc, _, _, ok := runtime.Caller(1) if !ok { fmt.Println("报错") } funcccc := runtime.FuncForPC(pc) funname := funcccc.Name() gid := curGoroutineID() mutex.Lock() index := mgroup[gid] mgroup[gid] = index + 1 mutex.Unlock() s := "" for i := 0; i <= index; i++ { s = s + " " } fmt.Printf("g[%05d]:%s-> enter:%s\n", gid, s, funname) return func() { fmt.Printf("g[%05d]:%s<- exit :%s\n", gid, s, funname) } } 利用defer后面的表达式在入栈时求值这一特性,用一个缩紧变量就行了,闭包中的 indents -1 有点多此一举吧?

    作者回复: 嗯,不错的思路。

    2022-05-10
    4
  • 木木
    感谢,这节课觉得学到了很多。有个问题,在文中演示如何获得 Goroutine ID的trace例程里,waitGroup的作用是什么?我本来以为是像信号量一样的同步手段,但是想了一想发现并不是,因为wait在A1()之后。如果wait在A1()之前的话,可以保证让A2先执行完再执行A1。文中这种在A1()之后wait()的原因是什么?

    作者回复: waitgroup是go标准库sync包提供的一个功能特性,常用用于等待一组子goroutine的退出。可以看看go官方相关文档以及文档中的用法。

    2021-12-23
    3
    4
  • Geralt
    思考题的一个思路: 在instrument_trace目录下新建一个config目录,里面有dev.go和prod.go两个文件: dev.go //go:build dev package config const ShouldPrint = true ------ prod.go //go:build prod package config const ShouldPrint = false ------ 修改Trace()函数,在方法体内先判断ShouldPrint的值,若为false则返回一个空的匿名函数。 通过go build -tags dev(prod) 可以指定config目录下哪个文件参与编译。

    作者回复: 都用build tag了,应该就不需要shouldprint了吧。

    2021-12-23
    2
    4
  • 张申傲
    就喜欢这种实战,可以把前面的知识点都串起来,对于加深理解很有帮助~

    作者回复: 👍

    2021-12-22
    4
  • KingOfDark
    1. 对于go build的编译过程,有点疑问,就比如这里编译 go build demo.go ,会把依赖的包也都给重新编译吗? 还是说依赖包的都是提前编译好的(或者说只会有一次编译,之后不会重新编译了,只需要在链接即可?) 2. 对于思考题的使用build tags,有两种思路: 第一种思路,是 trace.go 有两个版本(文件名可以分别为 dev_trace.go, prod_trace.go),dev 版本的trace 是正常的打印逻辑,prod 版本直接返回空函数体 第二种思路,是 要编译/追踪的go源文件有两个版本,一个带有trace函数,一个不带trace函数(这个方法好像用不到 build tag 了,但是这样好像把defer的开销也省去了)

    作者回复: 1. 关于go增量编译,可以了解一下 https://tonybai.com/2022/03/21/go-native-support-incremental-build 2. 第一个思路✅。第二种思路维护起来过于麻烦了。

    2022-06-15
    2
    3
  • 木木
    一个问题:老师代码里好几处用到了类似 fd, ok :=decl.(*ast.FuncDecl) 这种写法,看了一下,ast是package,FuncDecl是一个struct,decl是一个ast.Decl类型的变量,给我搞晕了。请问等号右边的意思是什么?

    作者回复: 这不能怪你,因为这里使用了接口的类型断言(type assert)语法,可以先看看第28讲后,再回来看这段代码。

    2021-12-23
    2
    3
  • qinsi
    一些疑问: 1. 在输出带缩进的跟踪信息时,用一个map保存了不同goroutine的当前缩进。但似乎每个goroutine都只会访问自己的id对应的kv,不存在不同的goroutine访问同一个key的情况。这种情况下能否不加锁呢? 2. 在其他语言的生态中,实现无侵入的链路跟踪通常都是在语言的中间表示上做文章,比如JVM字节码或是LLVM IR。查了下go似乎也有自己的一种ssa ir,那么是否有可能也在这种ir上做做文章?

    作者回复: 1. go的map类型如果发现多个goroutine尝试对其进行写操作,但没有加锁,就可能抛出panic 2. 思路不错,但我对ssa ir了解不多,如果你对ssa ir很了解,建议你尝试一下,有成果后也可以分享出来。

    2021-12-22
    7
    3
  • Geek_as
    老师,我觉得那个map加锁好像不需要,map的确是不支持并发写,但我觉得这个并发写,应该是不支持多个gorunine对同一个key写,但是现在这个项目,每个gorunine是对属于自己的key进行操作,即每个key任何时刻最多只会被一个gorunine写,不存在并发问题

    作者回复: runtime层面对map并发读写的检测是整体的,不会考虑goroutine是否各自访问自己的数据。

    2022-04-26
    2
  • lesserror
    感谢大白老师,这一讲的内容很有启发性。有几个小疑问,劳烦有时间回复一下: 1. 文中说:“于是当 foo 函数返回后,这个闭包函数就会被执行。” 我想的是,这里是不是 foo 函数返回之前,闭包函数就会被执行呢? foo 函数返回后,是不是代表这个函数已经执行完毕了? 2. 第一个返回值代表的是程序计数(pc)。我打印pc变量,出来的是类似: 17343465、17343241、17343369······,这个计数究竟是什么呢,内存地址吗? 3. 文中的这两步操作:$go build github.com/bigwhite/instrument_trace/cmd/instrument $instrument version 我的理解是编译生成了可执行二进制文件后,需要放到 类似 bin目录中,才能全局 执行 “instrument version” 命令吧? 感觉老师这里还少了一步操作。 4. 不建议使用 Goroutine ID的最大原因是什么? 文中链接中的讨论组内容没有仔细看完。 5. 课后问题的比较优越的实现方案是什么?想听到老师的答案。 ps:问题有点多,但是确实属于我这节课看完后的疑惑,谢谢老师解答。

    作者回复: 1. 这里所谓的foo函数返回后,指的是defer函数被执行,deferred函数即是那个闭包函数。 2.pc是程序计数器,冯 ·诺伊曼计算机体系结构中的一个寄存器。可以自行google或baidu一下。 3. go build后,instrument程序会出现在当前目录下。 4. 最大原因还是避免被滥用。避免写出强依赖goroutine id的代码。因为强依赖goroutine将导致代码不好移植,同时也会导致并发模型复杂化。 5. 提示里有,使用build tag。关于build tag用法,可以参考go官方文档。

    2021-12-24
    2
收起评论
显示
设置
留言
29
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部