skip to content

What does the traceback header `goroutine 84 [chan receive, 12 minutes]:` tell you about that goroutine?

level: middleimportance: should knowfreq 50%

answer

  1. three separate facts on one line
  2. the bracket is a wait reason
  3. ids only ever increase
  4. duration appears only after minutes
  5. blocked is not the same as broken

basics

~20 s

It is goroutine number 84, the runtime parked it while it was receiving from a channel, and it has been blocked for about twelve minutes. The duration is printed only once a goroutine has been waiting for minutes.

solid answer

~50 s

Three facts. The id, 84, comes from an ever-increasing counter that is never reused, so a low id means an old goroutine and a run of consecutive ids means a batch created together; it is not an OS thread id. The bracketed word is the wait reason the runtime recorded when it parked the goroutine — here it is blocked receiving from a channel. The vocabulary is small: `running`, `runnable`, `syscall`, `IO wait` (parked on the network poller), `chan receive`, `chan send`, `select`, `sleep`, `semacquire`, and refinements such as `chan receive (nil chan)`. The trailing duration only appears once the wait has run into minutes, which is what makes it valuable. The reading rule matters most: a wait reason says where a goroutine is parked, not that anything is wrong — idle workers blocking on a receive is normal.

code

text · 5 lines
text
goroutine 84 [chan receive, 12 minutes]:
main.(*Consumer).worker(0xc0000ac000)
	/srv/worker/consume.go:74 +0x8c
created by main.(*Consumer).start in goroutine 17
	/srv/worker/consume.go:66 +0x11d

go deeper

for a junior

Be able to name the three parts out loud: which goroutine, what it is waiting on, and how long. Knowing that a blocked goroutine is normal is most of the value here.

for a middle

Explain the wait-reason vocabulary and what each implies about where the goroutine is parked, including the difference between IO wait, syscall and runnable.

for a senior

Show the judgment layer: a state plus a duration plus the number of goroutines sharing it is a finding, while a state on its own is not, and say what you would check next.

for a principal

Decide what the team treats as an alertable signal from goroutine states and counts, and how much runtime detail is worth exposing in dashboards versus read at crash time.

## Anatomy of the header line Every goroutine in a traceback opens with a line of the form ``` goroutine 84 [chan receive, 12 minutes]: ``` There are three pieces of information in it, and each answers a different question. **`goroutine 84` — which one.** Goroutine ids come from a counter that only increases, and they are not reused: a fresh goroutine always gets a number nobody has had. So a low id was created early — `goroutine 1` is the goroutine running `main` — and a block of near-consecutive ids means those goroutines were created together, in a burst. The id is purely a runtime bookkeeping number: it is not an OS thread id, it means nothing outside the process, and Go deliberately offers no supported API to read your own. Its value to a reader is ordering and cross-referencing — a `created by ... in goroutine 17` line elsewhere in the dump points at a specific parent. **`chan receive` — where it is parked.** This is the *wait reason* the runtime recorded when it parked the goroutine. The vocabulary is small and worth knowing: | state | what it means | |---|---| | `running` | executing right now (in a crash dump, usually the goroutine that crashed) | | `runnable` | ready to run, waiting for a processor to pick it up | | `syscall` | blocked inside a system call on an OS thread | | `IO wait` | parked on the network poller, waiting for a socket to become readable or writable | | `chan receive` / `chan send` | blocked on a channel operation | | `select` | blocked in a `select` with no ready case | | `sleep` | inside `time.Sleep` | | `semacquire` | contending for a lock or waiting on a counter | | `GC assist wait` | throttled by the collector while it allocates | Parenthesised refinements appear too — `chan receive (nil chan)` tells you the channel operand was nil, which blocks forever by definition and is a completely different bug from a channel with no sender. A goroutine pinned to its thread prints `locked to thread`. **`12 minutes` — how long.** The runtime records when it parked the goroutine and adds the elapsed time to the header once the wait has run into minutes; short waits print no duration at all. This is the single most useful field in a large dump, because it converts a state into a judgment. Three hundred goroutines in `chan receive` is what a healthy idle worker pool looks like. Three hundred goroutines in `chan receive` for forty minutes, in a service whose queue is known to be busy, is a finding. ## The reading rule **A wait reason describes a location, not a fault.** Blocking is the normal condition of most goroutines in a long-lived Go program: workers wait for work, servers wait for connections, tickers sleep. Nothing in the header says whether that is correct. What makes a state interesting is the combination of the reason, the duration, the count of goroutines sharing it, and what the code is supposed to be doing at that moment. A candidate who says "they're blocked on a channel, that's the bug" has not read the dump; a candidate who says "they're blocked on a channel, which is expected — what is odd is that it has been forty minutes and there is no sender anywhere in this dump" has. ## Two distinctions people get wrong `IO wait` versus `syscall`. `IO wait` means the goroutine handed its file descriptor to the runtime's network poller and parked; no OS thread is tied up, and this is what a goroutine blocked reading a socket normally looks like. `syscall` means the goroutine is sitting inside a blocking system call on a real thread — ordinary file reads and `cgo` calls land here — and while it does, that thread is consumed. A hundred goroutines in `IO wait` is cheap; a hundred in `syscall` costs a hundred threads. `runnable` versus `running`. `running` means executing at the instant of the dump. `runnable` means the goroutine has work to do and is queued, waiting for a processor. A dump thick with `runnable` goroutines is a CPU-saturation signal, not a blocking one. Note also that in a crash dump a goroutine executing on another thread often cannot have its stack walked safely, and prints `goroutine running on other thread; stack unavailable` in place of frames.

  • How is a goroutine in `IO wait` different from one in `syscall`?
    `IO wait` means the goroutine handed its file descriptor to the runtime's network poller and parked, so no OS thread is tied up — that is what a goroutine blocked on a socket normally looks like. `syscall` means it is sitting inside a blocking system call on a real thread, which ordinary file reads and cgo calls do, and that thread is consumed while it waits.
  • Does a goroutine's id tell you anything useful when reading a dump?
    Yes, ordering. Ids come from a counter that only increases and are never reused, so a lower number was created earlier and goroutine 1 is running `main`. A block of near-consecutive ids means a burst created together; ids spread across the whole range mean accumulation over the process's life. They mean nothing to the operating system.
  • In a crash dump, why do some goroutines print no frames at all?
    A goroutine executing on another thread at the instant of the dump usually cannot have its stack walked safely, so the runtime prints `goroutine running on other thread; stack unavailable` in place of frames. Very deep stacks are also truncated, with a line saying additional frames were elided.

saying these in an interview costs you the question

  • Reads any blocked state as evidence of a bug
  • Thinks the minutes figure is CPU time consumed
  • Treats the goroutine id as an OS thread id
  • Assumes IO wait means disk input and output
  • Believes goroutine ids are recycled so ordering means nothing