开篇
golang 在 http.Request 中提供了一个 Context 用于存储 kv 对,我们可以通过这个来存储请求相关的数据。在请求入口,我们把唯一的 requstID 存储到 context 中,在后续需要调用的地方把值取出来打印。如果日志是在 controller 中打印,这个很好处理,http.Request 是作为入参的。但如果是在更底层呢?比如说是在 model 甚至是一些工具类中。我们当然可以给每个方法都提供一个参数,由调用方把 context 一层一层传下来,但这种方式明显不够优雅。想想 java 里面是怎么做的 –ThreadLocal。虽然 golang 官方不太认可这种方式,但是我们今天就是要基于 goroutine id 实现它。
We wouldn’t even be having this discussion if thread local storage wasn’t useful. But every feature comes at a cost, and in my opinion the cost of threadlocals far outweighs their benefits. They’re just not a good fit for Go.
思路
每个 goroutine 有一个唯一的 id,但是被隐藏了,我们首先把它暴露出来,然后建立一个 map,用 id 作为 key,goroutineLocal 存储的实际数据作为 value。
获取 goroutine id
1. 修改 $GOROOT/src/runtime/proc.go 文件,添加 GetGoroutineId() 函数
func GetGoroutineId() int64 {
return getg().goid
}
其中 getg() 函数是获取当前执行的 g 对象,g 对象包含了栈,cgo 信息,GC 信息,goid 等相关数据,goid 就是我们想要的。
2. 重新编译源码
cd ~/go/src
GOROOT_BOOTSTRAP=’/Users/qiuxudong/go1.9′ ./all.bash
实现 GoroutineLocal
package goroutine_local
import (
“sync”
“runtime”
)
type goroutineLocal struct {
initfun func() interface{}
m *sync.Map
}
func NewGoroutineLocal(initfun func() interface{}) *goroutineLocal {
return &goroutineLocal{initfun:initfun, m:&sync.Map{}}
}
func (gl *goroutineLocal)Get() interface{} {
value, ok := gl.m.Load(runtime.GetGoroutineId())
if !ok && gl.initfun != nil {
value = gl.initfun()
}
return value
}
func (gl *goroutineLocal)Set(v interface{}) {
gl.m.Store(runtime.GetGoroutineId(), v)
}
func (gl *goroutineLocal)Remove() {
gl.m.Delete(runtime.GetGoroutineId())
}
简单测试一下
package goroutine_local
import (
“testing”
“fmt”
“time”
“runtime”
)
var gl = NewGoroutineLocal(func() interface{} {
return “default”
})
func TestGoroutineLocal(t *testing.T) {
gl.Set(“test0”)
fmt.Println(runtime.GetGoroutineId(), gl.Get())
go func() {
gl.Set(“test1”)
fmt.Println(runtime.GetGoroutineId(), gl.Get())
gl.Remove()
fmt.Println(runtime.GetGoroutineId(), gl.Get())
}()
time.Sleep(2 * time.Second)
}
可以看到结果
5 test0
6 test1
6 default
内存泄露问题
由于跟 goroutine 绑定的数据放在 goroutineLocal 的 map 里面,即使 goroutine 销毁了数据还在,可能存在内存泄露,因此不使用时要记得调用 Remove 清除数据