golang 分析调试高阶技巧


layout: post
title: “golang 调试高阶技巧”
date: 2020-6-03 1:44:09 +0800
categories: golang GC 垃圾回收


  • golang 高阶调试
    • Golang tools
      • nm
      • compile
      • objdump
      • pprof
      • trace
    • 单元测试
      • 执行单元测试
        • go test 运行
        • 编译,运行
      • 统计代码覆盖率
    • 程序 Debug
      • dlv 调试用法
        • 调试二进制
        • 调试进程
        • 调试 core 文件
        • 调试常用语法
          • 系统整理
          • 应用举例
      • gdb 调试
    • 小技巧
      • 不知道怎么断点函数?
      • 不知道调用上下文?
      • 不知道怎么开启 pprof ?
      • 为什么有时候单点调试的时候,总是非预期的执行代码?
    • 总结

golang 高阶调试

本文专注 golang debug 的一些技巧应用,以及相关工具的实用用法,再也不用怕 golang 怎么调试。golang 作为一门现代化语音,出生的时候就自带完整的 debug 手段:

  • golang tools 是直接集成在语言工具里,支持内存分析,cpu分析,阻塞锁分析等;
  • delve,gdb 作为最常用的 debug 工具,让你能够更深入的进入程序调试;
    • delve 当前是最友好的 golang 调试程序,ide 调试其实也是调用 dlv 而已,比如 goland;
  • 单元测试的设计深入到语言设计级别,可以非常方便执行单元测试并且生成代码覆盖率;

Golang tools

golang 从语言原生层面就集成了大量的实用工具,这些都是 Robert Griesemer, Rob Pike, Ken Thompson 这几位大神经验沉淀下的精华。你安装好 golang 之后,执行 go tool 就能看到内置支持的所有工具了。

1root@ubuntu:~# go tool 2addr2line 3asm 4buildid 5cgo 6compile 7cover 8dist 9doc 10fix 11link 12nm 13objdump 14pack 15pprof 16test2json 17trace 18vet

我这里专注挑选几个 debug 常用的:

  • nm:查看符号表(等同于系统 nm 命令)
  • objdump:反汇编工具,分析二进制文件(等同于系统 objdump 命令)
  • pprof:指标,性能分析工具
  • cover:生成代码覆盖率
  • trace:采样一段时间,指标跟踪分析工具
  • compile:代码汇编

nm

查看符号表的命令,等同于系统的 nm 命令,非常有用。在断点的时候,如果你不知道断点的函数符号,那么用这个命令查一下就知道了(命令处理的是二进制程序文件)。

1# exmple 为你编译的二进制文件 2go tool nm ./example

第一列是地址,第二列是类型,第三列是符号:

[图片上传失败…(image-1c9b7a-1594910164396)]

compile

汇编某个文件

go tool compile -N -l -S example.go 

你就能看到你 golang 语言对应的汇编代码了(注意了,命令处理的是 golang 代码文本),酷。

objdump

反汇编二进制的工具,等同于系统 objdump(注意了,命令解析的是二进制格式的程序文件)。

1go tool objdump example.o 2go tool objdump -s DoFunc example.o  // 反汇编具体函数

汇编代码这个东西在 90% 的场景可能都用不上,但是如果你处理过 c 的程序,在某些特殊场景,通过反汇编一段逻辑来推断应用程序行为将是你唯一的出路。因为线上的代码一般都是会开启编译优化,所以这里会导致你的代码对不上。再者,线上不可能让你随意 attach 进程,很多时候都是出 core 了,你就只有一个 core 文件去排查。

pprof

pprof 支持四种类型的分析:

  • CPU :CPU 分析,采样消耗 cpu 的调用,这个一般用来定位排查程序里耗费计算资源的地方;
  • Memroy :内存分析,一般用来排查内存占用,内存泄露等问题;
  • Block :阻塞分析,会采样程序里阻塞的调用情况;
  • Mutex :互斥锁分析,采样互斥锁的竞争情况;

我们这里详细以内存占用分析举例(其他的类似),pprof 这个是内存分析神器。基本上,golang 有了这个东西,99% 的内存问题(比如内存泄露,内存占用过大等等)都是可以非常快的定位出来的。首先,对于 golang 的内存分析(或者其他的锁消耗,cpu 消耗)我们明确几个重要的点:

  • golang 内存 pprof 是采样的,每 512KB 采样一次;
  • golang 的内存采样的是堆栈路径,而不是类型信息;
  • golang 的内存采样入口一定是通过mProf_MallocmProf_Free 这两个函数。所以,如果是 cgo 分配的内存,那么是没有机会调用到这两个函数的,所以如果是 cgo 导致的内存问题,go tool pprof 是分析不出来的;

详细原理,可以复习另一篇文章:内存分析;

分析的形式有两种:

  1. 如果是 net/http/pporf 方式开启的,那么可以直接在控制台上输入,浏览器就能看;
  2. 另一种方式是先把信息 dump 到本地文件,然后用 go tool 去分析(我们以这个举例,因为这种方式才是生产环境通用的方式)
1# 查看累计分配占用 2go tool pprof -alloc_space ./29075_20190523_154406_heap 3# 查看当前的分配占用 4go tool pprof -inuse_space ./29075_20190523_154406_allocs

你也可以不指定类型,直接 go tool pprof ./xxx ,进入分析之后,调用 o 选项,指定类型:

我写了一个 demo 程序,然后 dump 出了一份 heap 的 pprof 采样文件,我们先通过这个 pprof 得出一些结论,最后我再贴出源代码,再品一品。

1go tool pprof ./29075_20190523_154406_heap 2(pprof) o 3... 4 sample_index = inuse_space //: [alloc_objects | alloc_space | inuse_objects | inuse_space] 5... 6(pprof) alloc_space 7(pprof) top 8Showing nodes accounting for 290MB, 100% of 290MB total 9 flat flat% sum% cum cum% 10 140MB 48.28% 48.28% 140MB 48.28% main.funcA (inline) 11 100MB 34.48% 82.76% 190MB 65.52% main.funcB (inline) 12 50MB 17.24% 100% 140MB 48.28% main.funcC (inline) 13 0 0% 100% 290MB 100% main.main 14 0 0% 100% 290MB 100% runtime.main

这个 top 信息表明了这么几点信息:

  • main.funcA 这个函数现场分配了 140M 的内存,main.funcB 这个函数现场分配了 100M 内存,main.funcC 现场分配了 50M 内存;
    • 现场的意思:纯粹自己函数直接分配的,而不是调用别的函数分配的;
    • 这些信息通过 flat 得知;
  • main.funcA 分配的 140M 内存纯粹是自己分配的,没有调用别的函数分配过内存;
    • 这个信息通过 main.funcA flat 和 cum 都为 140 M 得出;
  • main.funcB 自己分配了 100MB,并且还调用了别的函数,别的函数里面涉及了 90M 的内存分配;
    • 这个信息通过 main.funcB flat 和 cum 分别为 100 M,190M 得出;
  • main.funcC 自己分配了 50MB,并且还调用了别的函数,别的函数里面涉及了 90M 的内存分配;
    • 这个信息通过 main.funcC flat 和 cum 分别为 50 M,140 M 得出;
  • main.main :所有分配内存的函数调用都是走这个函数出去的。main 函数本身没有函数分配,但是他调用的函数分配了 290M;

demo 的源代码:

1package main 2 3import ( 4 "net/http" 5 _ "net/http/pprof" 6) 7 8func funcA() []byte { 9 a := make([]byte, 10*1024*1024) 10 return a 11} 12 13func funcB() ([]byte, []byte) { 14 a := make([]byte, 10*1024*1024) 15 b := funcA() 16 return a, b 17} 18 19func funcC() ([]byte, []byte, []byte) { 20 a := make([]byte, 10*1024*1024) 21 b, c := funcB() 22 return a, b, c 23} 24 25func main() { 26 for i := 0; i < 5; i++ { 27 funcA() 28 funcB() 29 funcC() 30 } 31 32 http.ListenAndServe("0.0.0.0:9999", nil) 33}

dump 命令

curl -sS 'http://127.0.0.1:9999/debug/pprof/heap?seconds=5' -o heap.pporf 

对照着代码,再品一品。

trace

程序 trace 调试

go tool trace -http=":6060" ./ssd_336959_20190704_105540_trace 

trace 这个命令允许你跟踪采集一段时间的信息,然后 dump 成文件,最后调用 go tool trace 分析 dump 文件,并且以 web 的形式打开。

单元测试

单元测试的重要性就不再论述。golang 里面 _test.go 结尾的文件认为是测试文件,golang 作为现代化的语言,语言工具层面支持单元测试。

执行单元测试

执行单元测试有两种方式:

  • go test 直接运行,这个是最简单的;
  • 先编译测试文件,再运行。这种方式更灵活;

go test 运行

1// 直接在你项目目录里运行 go test . 2go test . 3// 指定运行函数 4go test -run=TestPutAndGetKeyValue 5// 打印详细信息 6go test -v

编译,运行

本质上,golang 跑单测是先编译 *_test.go 文件,编译成二进制后,再运行这个二进制文件。你执行 go test 的时候,工具帮你做好了,这些动作其实也是可以拆开来自己做的。

编译生成单元测试可执行文件:

1// 先编译出 .test 文件 2$ go test -c

// 指定跑某一个文件 $ ./raftexample.test -test.timeout=10m0s -test.v=true -test.run=TestPutAndGetKeyValue

1 2这种方式通常会出现在以下几种场景: 3 41. 这台机器上编译,另一个地方跑单测; 52. debug 单测程序; 6 7### [](https://blog.csdn.net/qiya2007/article/details/107396182)统计代码覆盖率 8 9golang 的代码覆盖率是基于单测的,由单测作为出发点,来看你的业务代码覆盖率。 10 11操作很简单: 12 131. 加一个 `-coverprofile` 的参数,声明在跑单测的时候,记录代码覆盖率; 142. 使用 `go tool cover` 命令分析,得出覆盖率报告; 15

go test -coverprofile=coverage.out go tool cover -func=coverage.out

1 2类似如下: 3

root@ubuntu:~/opensource/readcode-etcd-master/src/go.etcd.io/etcd/contrib/raftexample# go tool cover -func=coverage.out go.etcd.io/etcd/v3/contrib/raftexample/httpapi.go:33: ServeHTTP 25.0% go.etcd.io/etcd/v3/contrib/raftexample/httpapi.go:108: serveHttpKVAPI 0.0% go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:41: newKVStore 100.0% go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:50: Lookup 100.0% go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:57: Propose 75.0% go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:71: readCommits 55.0% go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:107: getSnapshot 100.0% go.etcd.io/etcd/v3/contrib/raftexample/kvstore.go:113: recoverFromSnapshot 85.7% go.etcd.io/etcd/v3/contrib/raftexample/listener.go:30: newStoppableListener 75.0% go.etcd.io/etcd/v3/contrib/raftexample/listener.go:38: Accept 92.9% go.etcd.io/etcd/v3/contrib/raftexample/main.go:24: main 0.0% total: (statements) 57.1%

1 2这样的话,你就知道每个函数的代码覆盖率。 3 4[](https://blog.csdn.net/qiya2007/article/details/107396182)程序 Debug 5-------------------------------------------------------------------- 6 7程序的调试主要由两个工具: 8 91. dlv 102. gdb 11 12这里推荐 dlv,因为 gdb 功能实在是有限,gdb 不理解 golang 的业务类型和协程。但是 gdb 有一个功能是无法替代的,就是 gcore 的功能。 13 14### [](https://blog.csdn.net/qiya2007/article/details/107396182)dlv 调试用法 15 16#### [](https://blog.csdn.net/qiya2007/article/details/107396182)调试二进制 17

dlv exec <path/to/binary> [flags]

1 2举例: 3

dlv exec ./example

1 2dlv 调试二进制,并带参数 3

dlv exec ./example -- --audit=./d

1 2#### [](https://blog.csdn.net/qiya2007/article/details/107396182)调试进程 3

dlv attach ${pid} [executable] [flags]

1 2进程号是必选的。 3 4举例: 5

dlv attach 12808 ./example

1 2#### [](https://blog.csdn.net/qiya2007/article/details/107396182)调试 core 文件 3 4dlv 调试core文件;并且标准输出导出到文件 5

dlv core <executable> <core> [flags]

1dlv core ./example core.277282 2``` 3 4#### [](https://blog.csdn.net/qiya2007/article/details/107396182)调试常用语法 5 6##### [](https://blog.csdn.net/qiya2007/article/details/107396182)系统整理 7 8**程序运行** 9 101. call :call 函数(注意了,这个会导致整个程序运行的) 112. continue :往下运行 123. next :单步调试 134. restart :重启 145. step :单步调试,某个函数 156. step-instruction :单步调试某个汇编指令 167. stepout :从当前函数跳出 17 18**断点相关** 19 201. break (alias: b) :设置断点 212. breakpoints (alias: bp) :打印所有的断点信息 223. clear :清理断点 234. clearall :清理所有的断点 245. condition (alias: cond) :设置条件断点 256. on :设置一段命令,当断点命中的时候 267. trace (alias: t) :设置一个跟踪点,这个跟踪点也是一个断点,只不过运行道德时候不会断住程序,只是打印一行信息,这个命令在某些场景是很有用的,比如你断住程序就会影响逻辑(业务有超时),而你仅仅是想打印某个变量而已,那么用这种类型的断点就行;; 27 28**信息打印** 29 30* args : 打印程序的传参 31* examinemem (alias: x) :这个是神器,解析内存用的,和 gdb 的 x 命令一样; 32* locals :打印本地变量 33* print (alias: p) :打印一个表达式,或者变量 34* regs :打印寄存器的信息 35* set :set 赋值 36* vars :打印全局变量(包变量) 37* whatis :打印类型信息 38 39**协程相关** 40 41* goroutine (alias: gr) :打印某个特定协程的信息 42* goroutines (alias: grs) :列举所有的协程 43* thread (alias: tr) :切换到某个线程 44* threads :打印所有的线程信息 45 46**栈相关** 47 48* deferred :在 defer 函数上下文里执行命令 49* down :上堆栈 50* frame :跳到某个具体的堆栈 51* stack (alias: bt) :打印堆栈信息 52* up :下堆栈 53 54**其他命令** 55 56* config :配置变更 57* disassemble (alias: disass) :反汇编 58* edit (alias: ed) :略 59* exit (alias: quit | q) :略 60* funcs :打印所有函数符号 61* libraries :打印所有加载的动态库 62* list (alias: ls | l) :显示源码 63* source :加载命令 64* sources :打印源码 65* types :打印所有类型信息 66 67以上就是完整的 dlv 的支持的命令,从这个来看,是完全满足我们的调试需求的(有的只适用于开发调试环节,比如线上的程序不可能让你随意单步调试的,有的使用于线上生产环节)。 68 69##### [](https://blog.csdn.net/qiya2007/article/details/107396182)应用举例 70 71**打印全局变量** 72 73``` 74(dlv) vars 75``` 76 77这个非常有用,帮助你看一些全局变量。 78 79**条件断点** 80 81``` 82# 先断点 83(dlv)84 85# 查看断点信息 86(dlv) bp 87 88# 然后定制条件 89(dlv) condition 2 i==2 && j==7 && z==32 90``` 91 92**查看堆栈** 93 94``` 95# 展示所有堆栈 96(dlv) goroutines 97# 所有堆栈展开 98(dlv) goroutines -t 99``` 100 101**解析内存** 102 103``` 104(dlv) x -fmt hex -len 20 0xc00008af38 105``` 106 107`x` 命令和 gdb 的 `x` 是一样的。 108 109### [](https://blog.csdn.net/qiya2007/article/details/107396182)gdb 调试 110 111gdb 对 golang 的调试支持是通过一个 python 脚本文件 `src/runtime/runtime-gdb.py` 来扩展的,所以功能非常有限。gdb 只能做到最基本的变量打印,却理解不了 golang 的一些特殊类型,比如 channel,map,slice 等,gdb 原生是无法调适 goroutine 协程的,因为这个是用户态的调度单位,gdb 只能理解线程。所以只能通过 python 脚本的扩展,把协程结构按照链表输出出来,支持的命令: 112 113\[图片上传失败…(image-c8e3d1-1594910164394)\] 114 115gdb当前只支持6个命令: 116 117**3个 cmd 命令** 118 1191. info goroutines;打印所有的goroutines 1202. goroutine ${id} bt;打印一个goroutine的堆栈 1213. iface;打印静态或者动态的接口类型 122 123**3个函数** 124 1251. len;打印string,slices,map,channels 这四种类型的长度 1262. cap;打印slices,channels 这两种类型的cap 1273. dtype;强制转换接口到动态类型。 128 129**打印全局变量** (注意单引号) 130 131``` 132(gdb) p 'runtime.firstmoduledata' 133``` 134 135由于 gdb 不理解 golang 的一些类型系统,所以调试打印的时候经常打印不出来,这个要注意下。 136 137**打印数组变量长度** 138 139``` 140(gdb) p $len(xxx) 141``` 142 143所以,我一般只用 gdb 来 gcore 而已。 144 145[](https://blog.csdn.net/qiya2007/article/details/107396182)小技巧 146--------------------------------------------------------------- 147 148### [](https://blog.csdn.net/qiya2007/article/details/107396182)不知道怎么断点函数? 149 150有时候不知道怎么断点函数:可以通过nm查询下,然后再断点,就一定能断到了。 151 152\[图片上传失败…(image-f2bd4b-1594910164394)\] 153 154\[图片上传失败…(image-e94d0b-1594910164394)\] 155 156### [](https://blog.csdn.net/qiya2007/article/details/107396182)不知道调用上下文? 157 158在你的代码里添加一行: 159 160``` 161debug.PrintStack() 162``` 163 164这样就能当前代码位置的堆栈给打印出来,这样你就直到怎么函数的调用路径了。 165 166### [](https://blog.csdn.net/qiya2007/article/details/107396182)不知道怎么开启 pprof ? 167 168pprof 功能有两种开启方式,对应两种包: 169 170* net/http/pprof : 使用在 web 服务器的场景; 171* runtime/pprof :使用在非服务器应用程序的场景; 172 173这两个本质上是一致的,`net/http/pporf` 也只是在 `runtime/pprof` 上的一层 web 封装。 174 175**`net/http/pprof` 方式** 176 177``` 178import _ "net/http/pprof" 179``` 180 181**`runtime/pprof` 方式** 182 183这种通常用于程序调优的场景,程序只是一个应用程序,跑一次就结束,你想找到瓶颈点,那么通常会使用到这个方式。 184 185``` 186 // cpu pprof 文件路径 187 f, err := os.Create("cpufile.pprof") 188 if err != nil { 189 log.Fatal(err) 190 } 191 // 开启 cpu pprof 192 pprof.StartCPUProfile(f) 193 defer pprof.StopCPUProfile() 194``` 195 196### [](https://blog.csdn.net/qiya2007/article/details/107396182)为什么有时候单点调试的时候,总是非预期的执行代码? 197 198这种情况一般是被编译器优化了,比如函数内联了,编译出的二进制删减了无效逻辑、无效参数。这种情况就会导致你 dlv 单步调试的时候,总是非预期的执行,或者打印某些变量打印不出来。这种情况解决方法就是:禁止编译优化。 199 200``` 201go build -gcflags "-N -l" 202``` 203 204[](https://blog.csdn.net/qiya2007/article/details/107396182)总结 205-------------------------------------------------------------- 206 207该篇文章系统的分享了 golang 程序调试的技巧和用法: 208 2091. 语言工具包里内置 tool 工具,支持汇编,反汇编,pprof 分析,符号表查询等实用功能; 2102. 语言工具包集成单元测试,代码覆盖率依赖于单元测试的触发; 2113. 常用 dlv/gdb 这两个工具作为大杀器,可以分析二进制,进程,core 文件; 212 213* * * 214 215坚持思考,方向比努力更重要。微信公众号关注我:奇伢云存储 216 217![扫码_搜索联合传播样式-白色版.png](https://img-hello-world.oss-cn-beijing.aliyuncs.com/62a682b1eac13edd6102157ef786cca4.png) 218 219 220 221 222 223本文转自 [https://blog.csdn.net/qiya2007/article/details/107396182](https://blog.csdn.net/qiya2007/article/details/107396182),如有侵权,请联系删除。
点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )