DEV Community

Cover image for How to Get the Goroutine ID?
Leapcell
Leapcell

Posted on

How to Get the Goroutine ID?

Image description

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")
}
Enter fullscreen mode Exit fullscreen mode

After running, the following information will be output:

panic: leapcell

goroutine 1 [running]:
main.main()
    /path/to/main.go:4 +0x40
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

After running, the following information will be output:

goroutine 1 [running]:
main.main()
    /path/to/main.g
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
}()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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")
}()
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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{})
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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())
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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

Image description

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)

Collapse
 
zarabevan profile image
ZaraBevan

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