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.
1. The Official Design of Not Having goid
(https://github.com/golang/go/issues/22770)
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
2. Obtaining goid
in Pure Go
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
import "runtime"
func main() {
var buf = make([]byte, 64)
var stk = buf[:runtime.Stack(buf, false)]
print(string(stk))
}
After running, the following information will be output:
goroutine 1 [running]:
main.main()
/path/to/main.g
So, it is easy to parse the goid
information from the string obtained by runtime.Stack
:
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)
}
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.
3. Obtaining goid
from the g
Structure
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:
get_tls(CX)
MOVQ g(CX), AX // Move g into AX.
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:
#ifdef GOARCH_amd64
#define get_tls(r) MOVQ TLS, r
#define g(r) 0(r)(TLS*1)
#endif
After expanding the get_tls
macro function, the code to obtain the g
pointer is as follows:
MOVQ TLS, CX
MOVQ 0(CX)(TLS*1), 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:
MOVQ (TLS), AX
Based on the above method, we can wrap a getg
function to obtain the g
pointer:
// func getg() unsafe.Pointer
TEXT ·getg(SB), NOSPLIT, $0-8
MOVQ (TLS), AX
MOVQ AX, ret+0(FP)
RET
Then, in the Go code, obtain the value of goid
through the offset of the goid
member in the g
structure:
const g_goid_offset = 152 // Go1.10
func GetGroutineId() int64 {
g := getg()
p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset))
return *p
}
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:
var offsetDictMap = map[string]int64{
"go1.10": 152,
"go1.9": 152,
"go1.8": 192,
}
var g_goid_offset = func() int64 {
goversion := runtime.Version()
for key, off := range offsetDictMap {
if goversion == key || strings.HasPrefix(goversion, key) {
return off
}
}
panic("unsupported go version:"+goversion)
}()
Now, the goid
offset can finally automatically adapt to the released Go language versions.
4. Obtaining the Interface Object Corresponding to the g
Structure
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:
// func getg() interface{}
TEXT ·getg(SB), NOSPLIT, $32-16
// get runtime.g
MOVQ (TLS), AX
// get runtime.g type
MOVQ $type·runtime·g(SB), BX
// convert (*g) to interface{}
MOVQ AX, 8(SP)
MOVQ BX, 0(SP)
CALL runtime·convT2E(SB)
MOVQ 16(SP), AX
MOVQ 24(SP), BX
// return interface{}
MOVQ AX, ret+0(FP)
MOVQ BX, ret+8(FP)
RET
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
:
import (
"reflect"
)
func GetGoid() int64 {
g := getg()
gid := reflect.ValueOf(g).FieldByName("goid").Int()
return gid
}
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:
var g_goid_offset uintptr = func() uintptr {
g := GetGroutine()
if f, ok := reflect.TypeOf(g).FieldByName("goid"); ok {
return f.Offset
}
panic("can not find g.goid field")
}()
After having the correct goid
offset, obtain the goid
in the way mentioned before:
func GetGroutineId() int64 {
g := getg()
p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset))
return *p
}
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:
// func getg() interface{}
TEXT ·getg(SB), NOSPLIT, $32-16
NO_LOCAL_POINTERS
MOVQ $0, ret_type+0(FP)
MOVQ $0, ret_data+8(FP)
GO_RESULTS_INITIALIZED
// get runtime.g
MOVQ (TLS), AX
// get runtime.g type
MOVQ $type·runtime·g(SB), BX
// convert (*g) to interface{}
MOVQ AX, 8(SP)
MOVQ BX, 0(SP)
CALL runtime·convT2E(SB)
MOVQ 16(SP), AX
MOVQ 24(SP), BX
// return interface{}
MOVQ AX, ret_type+0(FP)
MOVQ BX, ret_data+8(FP)
RET
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.
5. Application of goid
: Local Storage
With the goid
, it is very easy to construct Goroutine local storage. We can define a gls
package to provide the goid
feature:
package gls
var gls struct {
m map[int64]map[interface{}]interface{}
sync.Mutex
}
func init() {
gls.m = make(map[int64]map[interface{}]interface{})
}
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:
func getMap() map[interface{}]interface{} {
gls.Lock()
defer gls.Unlock()
goid := GetGoid()
if m, _ := gls.m[goid]; m!= nil {
return m
}
m := make(map[interface{}]interface{})
gls.m[goid] = m
return m
}
After obtaining the private map
of the Goroutine, it is the normal interface for addition, deletion, and modification operations:
func Get(key interface{}) interface{} {
return getMap()[key]
}
func Put(key interface{}, v interface{}) {
getMap()[key] = v
}
func Delete(key interface{}) {
delete(getMap(), key)
}
Finally, we provide a Clean
function to release the map
resources corresponding to the Goroutine:
func Clean() {
gls.Lock()
defer gls.Unlock()
delete(gls.m, GetGoid())
}
In this way, a minimalist Goroutine local storage gls
object is completed.
The following is a simple example of using local storage:
import (
"fmt"
"sync"
"gls/path/to/gls"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
defer gls.Clean()
defer func() {
fmt.Printf("%d: number = %d\n", idx, gls.Get("number"))
}()
gls.Put("number", idx+100)
}(i)
}
wg.Wait()
}
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.
Leapcell: The Advanced Serverless Platform for Hosting Golang Applications
Finally, let me recommend the most suitable platform for deploying Go services: leapcell
1. Multi-Language Support
- Develop with JavaScript, Python, Go, or Rust.
2. Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
3. Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
4. Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
5. Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the documentation!
Leapcell Twitter: https://x.com/LeapcellHQ
Top comments (1)
One common method is to use the
runtime/pprof
package to extract the current Goroutine's stack trace and parse the Goroutine ID from there. However, it's important to note that this is more of a debugging tool and not intended for production code.Betbhai9