Why Go Needs Goroutine IDs: A Proposal to Enhance Runtime Profiling
This article explains a Go proposal to add unique Goroutine identifiers and start program counters to the runtime profiling API, detailing the background problem, example code, the suggested API changes, community discussion, and the practical impact on performance analysis.
Hello, I'm Jianyu.
Today I share an interesting Go proposal that adds goid and gopc fields to runtime.GoroutineProfile.
Background
In Go performance analysis we often need to track Goroutine execution across samples, but existing methods lack a stable identifier.
Method 1: runtime.GoroutineProfile() returns StackRecord without any identifier to correlate the same Goroutine across sampling periods.
Method 2: runtime.Stack(..., true) provides stack traces but incurs high CPU cost, making it impractical for many Goroutines.
Without a suitable identifier, tools cannot associate data from the same Goroutine across multiple snapshots.
Example
Consider the following code that samples Goroutine profiles:
package main
import (
"fmt"
"runtime"
"time"
)
func worker(id int) {
for {
// simulate work
time.Sleep(time.Millisecond * 100)
fmt.Printf("Worker %d is running
", id)
}
}
func main() {
// start several worker goroutines
for i := 0; i < 3; i++ {
go worker(i)
}
// periodically collect goroutine profile
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for i := 0; i < 3; i++ {
<-ticker.C
// get current goroutine profile
profiles := make([]runtime.StackRecord, 10)
n, ok := runtime.GoroutineProfile(profiles)
if !ok {
profiles = make([]runtime.StackRecord, n)
n, _ = runtime.GoroutineProfile(profiles)
}
fmt.Printf("=== Sample %d ===
", i+1)
for j, profile := range profiles[:n] {
fmt.Printf("Goroutine %d: %d stack frames
", j, len(profile.Stack0))
// problem: cannot know this goroutine's ID, cannot correlate across samples
}
}
}Running this code shows that each StackRecord array cannot be linked to a specific Goroutine, making performance analysis difficult.
Proposal Details
The proposal originated from Sentry engineers who needed precise Goroutine identification for their Go SDK performance tracing.
They suggest adding two fields to StackRecord: goid: unique Goroutine identifier. gopc: Goroutine starting program counter.
Because modifying the existing StackRecord could break compatibility, a possible solution is to introduce a new structure for Goroutine analysis.
type GoroutineRecord struct {
ID int64 // goroutine ID
GoPC uintptr // goroutine start PC
Stack []uintptr // call stack
// other related fields
}
func GoroutineProfileWithID() []GoroutineRecord {
// implementation details...
}Using this API, one can retrieve Goroutine identity during sampling:
func trackGoroutines() {
// first sample
sample1 := runtime.GoroutineProfileWithID()
time.Sleep(time.Second)
// second sample
sample2 := runtime.GoroutineProfileWithID()
// now associate same Goroutine by ID
for _, g1 := range sample1 {
for _, g2 := range sample2 {
if g1.ID == g2.ID {
fmt.Printf("Goroutine %d exists in both samples
", g1.ID)
// can analyze behavior change
}
}
}
}Community Discussion
The proposal has attracted attention in the Go community. Core team member @cherrymui noted that similar support might be needed in the pprof format.
Implementing this requires copying the Goroutine ID and start PC into StackRecord within saveg(), but that could reduce the generality of StackRecord.
Many performance analysis tools suffer from the inability to track specific Goroutine lifecycles, limiting diagnostic precision.
Practical Impact
For a web service analyzing slow requests, the new fields enable precise tracking of the Goroutine handling a request across multiple sampling points, improving root‑cause analysis in microservice architectures.
// current limitation: cannot track specific request's goroutine
func analyzeSlowRequests() {
profiles1 := getGoroutineProfile()
time.Sleep(5 * time.Second)
profiles2 := getGoroutineProfile()
// cannot correlate goroutine to request
}
// with new API:
func analyzeSlowRequestsWithID() {
profiles1 := getGoroutineProfileWithID()
time.Sleep(5 * time.Second)
profiles2 := getGoroutineProfileWithID()
for _, g1 := range profiles1 {
for _, g2 := range profiles2 {
if g1.ID == g2.ID {
// analyze same goroutine across samples
}
}
}
}Conclusion
Adding goid and gopc addresses a fundamental gap in Go runtime performance analysis, and the proposal has reached an active state, indicating serious consideration by the Go core team.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Enthusiast
Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
