When developing microservices in the Go language, you need to track the access link for each request, and there is currently no good solution for this in Go.

It is easier to solve this problem in Java, using MDC to share a requested RequestId within a process.

There are two ways to realize link tracing in Go: one is to use a global map in the project, where key is the unique Id of goroutine and value is RequestId; the other is to use context.Context.

The following code is implemented based on the GIN framework.

1. Use the global map

In the MAP scheme, a map needs to be maintained globally. When a request comes in, a RequestId is generated for each request. In each log print, the RequestId is obtained from the map through the GOID and printed in the log.

The implementation of the code is simple:

var requestIdMap = make(map[int64]string) // The global Map

func main(a) {
	r := gin.Default()
	r.Use(Logger()) // Use middleware

	r.GET("/index".func(c *gin.Context) {
		Info("main goroutine") // Prints logs

		c.JSON(200, gin.H{
			"message": "index",
		})
	})
	r.Run()
}

func Logger(a) gin.HandlerFunc {
	return func(c *gin.Context) {
		requestIdMap[goid.Get()] = uuid.New().String() // Set each request in the logging middleware
		c.Next()
	}
}

func Info(msg string)  {
	now := time.Now()
	nowStr := now.Format("The 2006-01-02 15:04:05")
	fmt.Printf("%s [%s] %s\n", nowStr, requestIdMap[goid.Get()], msg) // Prints logs
}
Copy the code

This implementation is simple, but there are many problems.

The first problem is that in a Go program, multiple Goroutines may be involved in a single request, making it difficult to pass RequestId between gotoutines.

In the following code, if a new goroutine is started, RequestId cannot be obtained from the log:

func main(a) {
	r := gin.Default()
	r.Use(Logger())

	r.GET("/index".func(c *gin.Context) {
		Info("main goroutine")

		go func(a) {  // A new goroutine is started
			Info("goroutine1")
		}()

		c.JSON(200, gin.H{
			"message": "index",
		})
	})
	r.Run()
}
Copy the code

Getting a Goroutine ID is not a normal practice, but is usually done through a hack, which is no longer recommended. In addition, this global map can be used to ensure concurrency security. In actual use, it may also need to use locks, which will inevitably affect performance in high concurrency situations.

At the end of each request, requestId needs to be manually removed from the map, otherwise there will be a memory leak.

In general, the map approach is not very good.

2. Use Context

In the above code, we use a hack to get the Goroutine ID, which is not recommended for a long time. Instead, we use Context, which I’ll leave outin my previous article.

In the case of passing RequestId, you can also use Context. The benefits of using Context are obvious. Context lifecycle is the same as the request, and manual destruction is not required. Moreover, the Context is unique to each request, there are no concurrency security concerns, and the Context can be passed between goroutines.

The code to implement using Context is as follows:

func main(a) {
	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(a) {
			Info(ctx.(context.Context), "goroutine1")
		}()

		c.JSON(200, gin.H{
			"message": "index",
		})
	})
	r.Run()
}

func Logger(a) 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("The 2006-01-02 15:04:05")
	fmt.Printf("%s [%s] %s\n", nowStr, ctx.Value("RequestId"), msg)
}
Copy the code

In this way, all gotroutines can get the same RequestId in a single request without worrying about memory leaks and concurrency security.

But the problem with using Context is that you have to pass the Context every time, and a lot of people aren’t used to it yet. In fact, Go officials have long recommended using Context, usually using Context as the first argument to a function. If a function takes a structure as an argument, you can also use Context as a field in the structure.

In addition to being able to pass RequestId, Context can also be used to control the lifecycle of a Goroutine, as explained in the Context article.

3. Summary

Get the Goroutine ID instead of the goroutine ID, and use the Context, which is recommended by Go officials. In the previous example, we used the Context to pass the RequestId, and we can also use the Context to pass the value of a single request range. For example, for authentication tokens, you should get used to using Context in your code.

[1] blog.golang.org/context

The text/Rayjun