Home >Backend Development >Golang >How to Get the Goroutine ID?
In an operating system, each process has a unique process ID, and each thread has its own unique thread ID. Similarly, in the Go language, each Goroutine has its own unique Go routine ID, which is often encountered in scenarios like panic. Although Goroutines have inherent IDs, the Go language deliberately does not provide an interface to obtain this ID. This time, we will attempt to obtain the Goroutine ID through the Go assembly language.
According to the official relevant materials, the reason the Go language deliberately does not provide goid is to avoid abuse. Because most users, after easily getting the goid, will unconsciously write code that strongly depends on goid in subsequent programming. Strong dependence on goid will make this code difficult to port and also complicate the concurrent model. At the same time, there may be a vast number of Goroutines in the Go language, but it is not easy to monitor in real-time when each Goroutine is destroyed, which will also cause resources that depend on goid to not be recycled automatically (requiring manual recycling). However, if you are a Go assembly language user, you can completely ignore these concerns.
Note: If you forcibly obtain the goid, you might be "shamed" ?:
https://github.com/golang/go/blob/master/src/runtime/proc.go#L7120
To facilitate understanding, let's first try to obtain the goid in pure Go. Although the performance of obtaining the goid in pure Go is relatively low, the code has good portability and can also be used to test and verify whether the goid obtained by other methods is correct.
Every Go language user should know the panic function. Calling the panic function will cause a Goroutine exception. If the panic is not handled by the recover function before reaching the root function of the Goroutine, the runtime will print relevant exception and stack information and exit the Goroutine.
Let's construct a simple example to output the goid through panic:
package main func main() { panic("leapcell") }
After running, the following information will be output:
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
We can guess that the 1 in the Panic output information goroutine 1 [running] is the goid. But how can we obtain the panic output information in the program? In fact, the above information is just a textual description of the current function call stack frame. The runtime.Stack function provides the function of obtaining this information.
Let's reconstruct an example based on the runtime.Stack function to output the goid by outputting the information of the current stack frame:
package main func main() { panic("leapcell") }
After running, the following information will be output:
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
So, it is easy to parse the goid information from the string obtained by runtime.Stack:
package main import "runtime" func main() { var buf = make([]byte, 64) var stk = buf[:runtime.Stack(buf, false)] print(string(stk)) }
We won't elaborate on the details of the GetGoid function. It should be noted that the runtime.Stack function can not only obtain the stack information of the current Goroutine but also the stack information of all Goroutines (controlled by the second parameter). At the same time, the net/http2.curGoroutineID function in the Go language obtains the goid in a similar way.
According to the official Go assembly language documentation, the g pointer of each running Goroutine structure is stored in the local storage TLS of the system thread where the current running Goroutine is located. We can first obtain the TLS thread local storage, then obtain the pointer of the g structure from the TLS, and finally extract the goid from the g structure.
The following is to obtain the g pointer by referring to the get_tls macro defined in the runtime package:
goroutine 1 [running]: main.main() /path/to/main.g
The get_tls is a macro function defined in the runtime/go_tls.h header file.
For the AMD64 platform, the get_tls macro function is defined as follows:
import ( "fmt" "strconv" "strings" "runtime" ) func GetGoid() int64 { var ( buf [64]byte n = runtime.Stack(buf[:], false) stk = strings.TrimPrefix(string(buf[:n]), "goroutine") ) idField := strings.Fields(stk)[0] id, err := strconv.Atoi(idField) if err!= nil { panic(fmt.Errorf("can not get goroutine id: %v", err)) } return int64(id) }
After expanding the get_tls macro function, the code to obtain the g pointer is as follows:
get_tls(CX) MOVQ g(CX), AX // Move g into AX.
In fact, TLS is similar to the address of thread local storage, and the data in the memory corresponding to the address is the g pointer. We can be more straightforward:
#ifdef GOARCH_amd64 #define get_tls(r) MOVQ TLS, r #define g(r) 0(r)(TLS*1) #endif
Based on the above method, we can wrap a getg function to obtain the g pointer:
MOVQ TLS, CX MOVQ 0(CX)(TLS*1), AX
Then, in the Go code, obtain the value of goid through the offset of the goid member in the g structure:
MOVQ (TLS), AX
Here, g_goid_offset is the offset of the goid member. The g structure refers to runtime/runtime2.go.
In the Go1.10 version, the offset of goid is 152 bytes. So, the above code can only run correctly in Go versions where the goid offset is also 152 bytes. According to the oracle of the great Thompson, enumeration and brute force are the panacea for all difficult problems. We can also save the goid offsets in a table and then query the goid offset according to the Go version number.
The following is the improved code:
// func getg() unsafe.Pointer TEXT ·getg(SB), NOSPLIT, <pre class="brush:php;toolbar:false">const g_goid_offset = 152 // Go1.10 func GetGroutineId() int64 { g := getg() p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset)) return *p }-8 MOVQ (TLS), AX MOVQ AX, ret+0(FP) RET
Now, the goid offset can finally automatically adapt to the released Go language versions.
Although enumeration and brute force are straightforward, they do not support well the unreleased Go versions under development. We cannot know in advance the offset of the goid member in a certain version under development.
If it is inside the runtime package, we can directly obtain the offset of the member through unsafe.OffsetOf(g.goid). We can also obtain the type of the g structure through reflection and then query the offset of a certain member through the type. Because the g structure is an internal type, Go code cannot obtain the type information of the g structure from external packages. However, in the Go assembly language, we can see all symbols, so theoretically, we can also obtain the type information of the g structure.
After any type is defined, the Go language will generate corresponding type information for that type. For example, the g structure will generate a type·runtime·g identifier to represent the value type information of the g structure, and also a type·*runtime·g identifier to represent the pointer type information. If the g structure has methods, then go.itab.runtime.g and go.itab.*runtime.g type information will also be generated to represent the type information with methods.
If we can get the type·runtime·g representing the type of the g structure and the g pointer, then we can construct the interface of the g object. The following is the improved getg function, which returns the interface of the g pointer object:
package main func main() { panic("leapcell") }
Here, the AX register corresponds to the g pointer, and the BX register corresponds to the type of the g structure. Then, the runtime·convT2E function is used to convert the type to an interface. Because we are not using the pointer type of the g structure, the returned interface represents the value type of the g structure. Theoretically, we can also construct an interface of the g pointer type, but due to the limitations of the Go assembly language, we cannot use the type·*runtime·g identifier.
Based on the interface returned by g, it is easy to obtain the goid:
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
The above code directly obtains the goid through reflection. Theoretically, as long as the name of the reflected interface and the goid member does not change, the code can run normally. After actual testing, the above code can run correctly in Go1.8, Go1.9, and Go1.10 versions. Optimistically, if the name of the g structure type does not change and the reflection mechanism of the Go language does not change, it should also be able to run in future Go language versions.
Although reflection has a certain degree of flexibility, the performance of reflection has always been criticized. An improved idea is to obtain the offset of the goid through reflection and then obtain the goid through the g pointer and the offset, so that reflection only needs to be executed once in the initialization phase.
The following is the initialization code for the g_goid_offset variable:
package main import "runtime" func main() { var buf = make([]byte, 64) var stk = buf[:runtime.Stack(buf, false)] print(string(stk)) }
After having the correct goid offset, obtain the goid in the way mentioned before:
package main func main() { panic("leapcell") }
At this point, our implementation idea for obtaining the goid is complete enough, but the assembly code still has serious security risks.
Although the getg function is declared as a function type that prohibits stack splitting with the NOSPLIT flag, the getg function internally calls the more complex runtime·convT2E function. If the runtime·convT2E function encounters insufficient stack space, it may trigger stack splitting operations. When the stack is split, the GC will move the stack pointers in the function parameters, return values, and local variables. However, our getg function does not provide pointer information for local variables.
The following is the complete implementation of the improved getg function:
panic: leapcell goroutine 1 [running]: main.main() /path/to/main.go:4 +0x40
Here, NO_LOCAL_POINTERS means that the function has no local pointer variables. At the same time, the returned interface is initialized with zero values, and after the initialization is completed, GO_RESULTS_INITIALIZED is used to inform the GC. This ensures that when the stack is split, the GC can correctly handle the pointers in the return values and local variables.
With the goid, it is very easy to construct Goroutine local storage. We can define a gls package to provide the goid feature:
package main import "runtime" func main() { var buf = make([]byte, 64) var stk = buf[:runtime.Stack(buf, false)] print(string(stk)) }
The gls package variable simply wraps a map and supports concurrent access through the sync.Mutex mutex.
Then define an internal getMap function to obtain the map for each Goroutine byte:
goroutine 1 [running]: main.main() /path/to/main.g
After obtaining the private map of the Goroutine, it is the normal interface for addition, deletion, and modification operations:
import ( "fmt" "strconv" "strings" "runtime" ) func GetGoid() int64 { var ( buf [64]byte n = runtime.Stack(buf[:], false) stk = strings.TrimPrefix(string(buf[:n]), "goroutine") ) idField := strings.Fields(stk)[0] id, err := strconv.Atoi(idField) if err!= nil { panic(fmt.Errorf("can not get goroutine id: %v", err)) } return int64(id) }
Finally, we provide a Clean function to release the map resources corresponding to the Goroutine:
get_tls(CX) MOVQ g(CX), AX // Move g into AX.
In this way, a minimalist Goroutine local storage gls object is completed.
The following is a simple example of using local storage:
#ifdef GOARCH_amd64 #define get_tls(r) MOVQ TLS, r #define g(r) 0(r)(TLS*1) #endif
Through Goroutine local storage, different levels of functions can share storage resources. At the same time, to avoid resource leaks, in the root function of the Goroutine, the gls.Clean() function needs to be called through the defer statement to release resources.
Finally, let me recommend the most suitable platform for deploying Go services: leapcell
Explore more in the documentation!
Leapcell Twitter: https://x.com/LeapcellHQ
The above is the detailed content of How to Get the Goroutine ID?. For more information, please follow other related articles on the PHP Chinese website!