Go gives you no goroutine id and no goroutine-local storage, so how do you tag concurrent work with a request?
answer
- the runtime knows it; you do not
- thread-locals were the thing being prevented
- identity travels as data you pass
- the numbers in a panic dump are reused
basics
~20 sYou pass the identifier explicitly. Go hides goroutine ids so nothing can build thread-local storage on them; a run or request id travels as a function argument, as a field on a value the goroutine owns, or inside a context.Context.
solid answer
~50 sThere is no supported way to read a goroutine's id: the runtime numbers goroutines internally and prints those numbers in panic dumps and in the goroutine profile, but exposes no accessor, deliberately. The argument is that a readable id invites goroutine-local storage, which hides a function's real inputs and makes library behaviour depend on which goroutine happens to call it. So identity is carried explicitly instead — as a parameter, as a field on a struct the goroutine owns, or in a `context.Context` that each goroutine is given. Because the arguments of a `go` statement are evaluated in the calling goroutine, passing the id in snapshots it naturally. Scraping `runtime.Stack` output for the `goroutine N` line is the hack people reach for, and it is slow, unsupported and unsound: ids are reused after a goroutine exits.
code
go · 6 linestype runIDKey struct{}
ctx := context.WithValue(context.Background(), runIDKey{}, runID)
for _, h := range hosts {
go check(ctx, h) // each goroutine carries the id it should log with
}go deeper
Know that Go has no goroutine id you can read and no goroutine-local storage, and that anything a goroutine needs to know about its work is passed to it when you start it.
Explain the alternatives concretely: a parameter, a field on a struct the goroutine owns, or a context.Context carrying request-scoped values with an unexported key type.
Show the operational angle: how you make concurrent log lines correlate without hidden state, and why an id scraped from runtime.Stack is unsound once you realise ids are reused after a goroutine exits.
Argue the tradeoff you are inheriting: explicit identity costs plumbing in every signature but keeps a function's inputs visible and stops library behaviour depending on which goroutine called it. Decide what your codebase threads through and what it refuses to.
## What is missing, and on purpose Every goroutine has an internal number. You can see it: a panic prints `goroutine 17 [running]:`, and the goroutine profile groups stacks by them. What Go does not provide is any way for your program to *read* the current goroutine's id, and there is no goroutine-local storage either. This is a deliberate omission, not an oversight. The reasoning, argued by the Go team over many years of requests, is that an accessible id is the seed of goroutine-local storage, and goroutine-local storage makes a function's behaviour depend on invisible state attached to whoever called it. A library that reads its configuration from goroutine-local state behaves differently depending on which goroutine invoked it — and, worse, differently again when the caller does the natural Go thing and moves the work to a new goroutine, which inherits nothing. Making identity explicit keeps the data a function depends on visible in its signature. ## What you do instead Carry the identity in the value the work is done on: - **A parameter.** The plainest option, and the arguments of a `go` statement are evaluated in the calling goroutine, so passing an id snapshots it at that line. - **A field.** The goroutine works on a struct that already carries the run id, request id or worker index. - **A `context.Context`.** For request-scoped values that cross API boundaries — a request id, a trace id, an auth subject — `context.WithValue` is the sanctioned channel, and the context is passed to each goroutine as its first argument. Note the deliberate discipline around it: `WithValue` is for request-scoped data, not for smuggling optional parameters, and the key should be an unexported type so keys from different packages cannot collide. ```go type runIDKey struct{} ctx := context.WithValue(context.Background(), runIDKey{}, runID) for _, h := range hosts { go check(ctx, h) // every goroutine carries the run id explicitly } ``` The practical result is that log lines from concurrent work correlate because the *program* threaded the id through, not because the runtime kept a hidden map. ## The hack, and why it is a red flag The workaround people find is to call `runtime.Stack`, whose output begins with a line like `goroutine 42 [running]:`, and parse the number out of it. Reasons not to: - It is undocumented and unsupported; the format is not a stable interface. - It is slow — formatting a stack trace on a hot path is nothing like reading a variable. - **Ids are reused.** When a goroutine exits, its `g` structure goes back to a free list and the number can appear again on a completely unrelated goroutine, so anything keyed by id can silently attach one goroutine's state to another. In an interview, recognising this as a code smell — and naming the reuse problem — matters more than knowing the parsing trick exists. ## Where the ids are legitimately useful For humans, reading a dump. When a program panics, every goroutine's stack is printed with its number, and that number lets you follow one goroutine through a long dump or match a parked goroutine in the goroutine profile against the call site that started it. Diagnosis, not program logic. ## The same design, one step further The absence of an id is part of a wider pattern: a `go` statement hands back nothing at all. No id, no handle, no join, and no way to stop a goroutine from outside — `runtime.Goexit` terminates only the goroutine that calls it, running its deferred calls on the way out. Anything you want to know or control about a goroutine, you have to arrange when you start it. Once you internalise that, "how do I get the goroutine's id" stops being the question and "what did I pass in when I started it" becomes the answer. ## Coming from another language If you arrive from a runtime with thread ids and thread-locals, the honest translation is: the thing you used thread-local storage for is either a parameter now, or lives in a context. That is more typing at the call site and much less mystery at three in the morning, when the only question that matters is which request a log line belongs to.
- Why not parse runtime.Stack output for the goroutine number?It is unsupported and the format is not a stable interface, it is far too slow for a hot path, and ids are recycled after a goroutine exits — so state keyed by id can end up attached to an unrelated goroutine later. It is treated as a code smell in review.
- Where do goroutine numbers legitimately show up?In diagnostics meant for humans: the stack dump printed on an unrecovered panic, and the goroutine profile, which groups goroutine stacks so you can see how many are parked at the same call site. Useful for reading a dump; not something program logic should depend on.
- Is there any API that stops another goroutine?No. `runtime.Goexit` terminates only the goroutine that calls it, running its deferred calls first. Anything else has to be cooperative: the goroutine must be watching something that tells it to return.
saying these in an interview costs you the question
- Parses runtime.Stack to get an id for program logic
- Looks for a thread-local equivalent instead of passing values
- Assumes goroutine ids are stable and never reused
- Thinks one goroutine can be killed by another