正文
在Golang中,每个goroutine协程都有一个goroutine id (goid),该goid没有向应用层暴露。但是,在很多场景下,开发者又希望使用goid作为唯一标识,将一个goroutine中的函数层级调用串联起来。比如,希望在一个http handler中将这个请求的每行日志都加上对应的goid以便于对这个请求处理过程进行跟踪和分析。
关于是否应该将goid暴露给应用层已经争论多年。基本上,Golang的开发者都一致认为不应该暴露goid(
faq: document why there is no way to get a goroutine ID
),主要有以下几点理由:
-
goroutine设计理念是轻量,鼓励开发者使用多goroutine进行开发,不希望开发者通过goid做goroutine local storage或thread local storage(TLS)的事情;
-
Golang开发者Brad认为TLS在C/C++实践中也问题多多,比如一些使用TLS的库,thread状态非常容易被非期望线程修改,导致crash.
-
goroutine并不等价于thread, 开发者可以通过syscall获取thread id,因此根本不需要暴露goid.
官方也一直推荐使用
context
作为上下文关联的最佳实践。如果你还是想获取goid,下面是我整理的目前已知的所有获取它的方式,希望你想清楚了再使用。
-
通过stack信息获取goroutine id.
-
通过修改源代码获取goroutine id.
-
通过CGo获取goroutine id.
-
通过汇编获取goroutine id.
-
通过汇编获取
伪
goroutine id.
在开始介绍各种方法前,先看一下定义在
src/runtime/runtime2.go
中保存goroutine状态的
g
结构:
type g struct {
// Stack parameters.
// stack describes the actual stack memory: [stack.lo, stack.hi).
// stackguard0 is the stack pointer compared in the Go stack growth prologue.
// It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
// stackguard1 is the stack pointer compared in the C stack growth prologue.
// It is stack.lo+StackGuard on g0 and gsignal stacks.
// It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
stack stack // offset known to runtime/cgo
stackguard0 uintptr // offset known to liblink
stackguard1 uintptr // offset known to liblink
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
m *m // current m; offset known to arm liblink
sched gobuf
syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc
syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc
stktopsp uintptr // expected sp at top of stack, to check in traceback
param unsafe.Pointer // passed parameter on wakeup
atomicstatus uint32
stackLock uint32 // sigprof/scang lock; TODO: fold in to atomicstatus
goid int64 // goroutine id
...
其中
goid int64
字段即为当前goroutine的id。
1. 通过stack信息获取goroutine id
package main
import (
"bytes"
"fmt"
"runtime"
"strconv"
)
func main() {
fmt.Println(GetGID())
}
func GetGID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return n
}
原理非常简单,将stack中的文本信息”goroutine 1234″匹配出来。但是这种方式有两个问题:
-
stack信息的格式随版本更新可能变化,甚至不再提供goroutine id,可靠性差。
-
性能较差,调用10000次消耗>50ms。
如果你只是想在个人项目中使用goid,这个方法是可以胜任的。维护和修改成本相对较低,且不需要引入任何第三方依赖。同时建议你就此打住,不要继续往下看了。
2. 通过修改源代码获取goroutine id
既然方法1效率较低,且不可靠,那么我们可以尝试直接修改源代码
src/runtime/runtime2.go
中添加
Goid
函数,将goid暴露给应用层:
func Goid() int64 {
_g_ := getg()
return _g_.goid
}
这个方式能解决法1的两个问题,但是会导致你的程序只能在修改了源代码的机器上才能编译,没有移植性,并且每次go版本升级以后,都需要重新修改源代码,维护成本较高。
3. 通过CGo获取goroutine id
那么有没有性能好,同时不影响移植性,且维护成本低的方法呢?那就是来自Dave Cheney的CGo方式:
文件id.c:
#include "runtime.h"
int64 ·Id(void) {
return g->goid;
}
文件id.go:
package id
func Id() int64
完整代码参见
junk/id
.