使用 Go 语言开发微服务的时候,需要追踪每一个请求的访问链路,这块在 Go 中目前没有很好的解决方案。
在 Go 中实现链路追踪有两种思路:一种是在项目中使用一个全局的 map, key 是 goroutine 的唯一 Id,value 是 RequestId,另一种思路可以使用 context.Context 来实现。
下面的代码基于 gin 框架来实现。
1、使用全局map来实现
var requestIdMap = make(map[int64]string) // 全局的 Map
func main() {
r := gin.Default()
r.Use(Logger()) // 使用中间件
r.GET("/index", func(c *gin.Context) {
Info("main goroutine") // 打印日志
c.JSON(200, gin.H{
"message": "index",
})
})
r.Run()
}
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
requestIdMap[goid.Get()] = uuid.New().String() // 在日志中间件中为每个请求设定
c.Next()
}
}
func Info(msg string) {
now := time.Now()
nowStr := now.Format("2006-01-02 15:04:05")
fmt.Printf("%s [%s] %s\n", nowStr, requestIdMap[goid.Get()], msg) // 打印日志
}
这样的实现很简单,但是问题也很多。
第一个问题就是,在 Go 程序中,一次请求可能会涉及到多个 goroutine,用这种方式很难在多个 gotoutine 之间传递 RequestId。
在下面的代码中,如果新启动了一个 goroutine,就会导致日志中获取不到 RequestId:
func main() {
r := gin.Default()
r.Use(Logger())
r.GET("/index", func(c *gin.Context) {
Info("main goroutine")
go func() { // 这里新启动了一个一个 goroutine
Info("goroutine1")
}()
c.JSON(200, gin.H{
"message": "index",
})
})
r.Run()
获取 goroutine id 也不是一种常规的做法,一般要通过 hack 的方式来获取,这种做法已经不推荐了。而且这个全局的 map 为了并发安全,在实际的使用中,可以还需要用到锁,在高并发的情况下必然会影响性能。
在每个请求结束的时候,还需要手动的把 requestId 从 map 中删除,否则就会造成内存泄漏。
总的来说,使用 map 这种方式来实现并不是很好。
2. 使用 Context 来实现
推荐使用 Context。
在传递 RequestId 的场景中,同样也可以使用 Context 来实现,使用 Context 好处很明显,Context 生命周期与请求相同,不需要手动销毁。而且Context 是每个请求独享的,也不用担心并发安全的问题,Context 还可以在 goroutine 之间传递。
使用 Context 实现的代码如下:
func main() {
r := gin.Default()
r.Use(Logger())
r.GET("/index", func(c *gin.Context) {
ctx, _ := c.Get("ctx")
Info(ctx.(context.Context) , "main goroutine")
go func() {
Info(ctx.(context.Context), "goroutine1")
}()
c.JSON(200, gin.H{
"message": "index",
})
})
r.Run()
}
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
valueCtx := context.WithValue(c.Request.Context(), "RequestId", uuid.New().String())
c.Set("ctx", valueCtx)
c.Next()
}
}
func Info(ctx context.Context, msg string) {
now := time.Now()
nowStr := now.Format("2006-01-02 15:04:05")
fmt.Printf("%s [%s] %s\n", nowStr, ctx.Value("RequestId"), msg)
}
这样在一个请求中,所有的 gotroutine 都可以获取到同一个 RequestId,而且不用担心内存泄漏和并发安全。
但是使用 Context 也有个问题就是需要每次传递 Context,很多人还不习惯使用这种方式。其实 Go 官方早就推荐使用 Context了,通常会把 Context 作为函数的第一个参数。如果函数使用结构体作为参数,也可以直接把 Context 作为结构体的一个字段。