Goroutine Lifecycle and States
Every goroutine in a running Go program is, at any instant, in exactly one of a small set of states, recorded as the integer
atomicstatusfield on its runtimegstruct. The state is not merely descriptive bookkeeping — it doubles as a lock on the goroutine’s stack. The garbage collector, the scheduler, and the goroutine itself coordinate by atomically transitioning this field through a fixed state machine, and many runtime invariants reduce to “you may only touch this goroutine’s stack while it is in such-and-such a state” (golang/gosrc/runtime/runtime2.go). Understanding the lifecycle —_Gidle → _Grunnable → _Grunning, and the loops back through_Gsyscall,_Gwaiting,_Gpreempted,_Gcopystack, and finally_Gdead— explains how the scheduler, preemption, stack growth, and GC stack-scanning all fit together.
Mental Model
A goroutine is a lightweight thread of execution; the runtime multiplexes potentially millions of them onto a handful of OS threads (see GMP Scheduler Model). To do that safely, the runtime must always know, for each goroutine: is it running right now? is it waiting to run? is it blocked? is its stack safe to read or move? The atomicstatus field answers all of these in one word.
The runtime comment in runtime2.go states it plainly: “Beyond indicating the general state of a G, the G status acts like a lock on the goroutine’s stack (and hence its ability to execute user code).” So the state machine is two things at once — a description of what the goroutine is doing and a mutual-exclusion protocol over its stack. Whoever holds the goroutine in a particular state owns the right to read, write, or relocate that stack.
stateDiagram-v2 [*] --> _Gidle: newproc allocates g _Gidle --> _Grunnable: stack set up, queued _Grunnable --> _Grunning: scheduler picks it (execute) _Grunning --> _Grunnable: yield / preempted (Gosched) _Grunning --> _Gsyscall: enter syscall (entersyscall) _Gsyscall --> _Grunning: return from syscall (exitsyscall) _Grunning --> _Gwaiting: block (channel, mutex, sleep) _Gwaiting --> _Grunnable: unblocked (ready / goready) _Grunning --> _Gpreempted: async-preempted itself (suspendG) _Gpreempted --> _Gwaiting: a suspender takes responsibility _Grunning --> _Gcopystack: stack grow/shrink (newstack) _Gcopystack --> _Grunning: stack moved _Grunning --> _Gdead: goroutine returns (goexit) _Gdead --> _Gidle: reused from gFree list _Gdead --> [*]
The figure is the goroutine state machine. The insight: there is exactly one “hot” state — _Grunning — and every other state is either a queue (_Grunnable), a blocked condition (_Gsyscall, _Gwaiting, _Gpreempted), a transient transformation (_Gcopystack), or the grave (_Gdead). A goroutine spends almost all of a blocking program’s wall-clock time in _Gwaiting, and almost all of a CPU-bound program’s in _Grunning.
The States, One by One
The state constants are defined in src/runtime/runtime2.go. Their integer values matter because the _Gscan bit (below) is OR-ed onto them.
_Gidle (0). “This goroutine was just allocated and has not yet been initialized.” Lives for microseconds: the moment between malg carving out a g struct and the runtime giving it a stack and an entry function. A reused g (pulled off a free list) also briefly passes through here.
_Grunnable (1). “On a run queue. It is not currently executing user code. The stack is not owned.” This is the ready queue state — the goroutine has everything it needs to run and is waiting for a P with a free moment. It sits on either a per-P local run queue or the global run queue.
_Grunning (2). “May execute user code. The stack is owned by this goroutine. It is not on a run queue. It is assigned an M (g.m is valid) and it usually has a P (g.m.p is valid).” This is the only state in which user code actually runs. “Owns the stack” means: no other thread may read or relocate this stack — the goroutine itself is using it.
_Gsyscall (3). “Executing a system call. It is not executing user code. The stack is owned by this goroutine. It is assigned an M. It may have a P attached, but it does not own it.” The subtlety is the P: when a goroutine enters a blocking syscall, the runtime detaches the P so another M can run other goroutines. The syscalling goroutine keeps its M (which is in the kernel) but releases its P. The runtime comment warns: “Code executing in this state must not touch g.m.p.” See System Calls and the Scheduler.
_Gwaiting (4). “Blocked in the runtime. It is not executing user code. It is not on a run queue, but should be recorded somewhere (e.g., a channel wait queue) so it can be ready()d when necessary. The stack is not owned except that a channel operation may read or write parts of the stack under the appropriate channel lock.” This is the catch-all blocked state: blocked on a channel send/receive, a mutex, sync.Cond, time.Sleep, a select with no ready case. The goroutine is parked on some data structure that holds a pointer to it; whoever unblocks it calls ready()/goready() to move it back to _Grunnable.
_Gmoribund_unused (5). Reserved, unused, but the slot is kept because GDB scripts hardcode the numbering.
_Gdead (6). “Currently unused. It may be just exited, on a free list, or just being initialized. It is not executing user code. It may or may not have a stack allocated.” When a goroutine’s top-level function returns, goexit puts it in _Gdead and links the g onto the P’s gFree list for reuse — this is why goroutine creation is cheap, the g struct and often its stack are recycled rather than reallocated.
_Genqueue_unused (7). Reserved, unused.
_Gcopystack (8). “This goroutine’s stack is being moved. It is not executing user code and is not on a run queue. The stack is owned by the goroutine that put it in _Gcopystack.” Goroutine stacks start small — 2 KiB by default on the mainstream 64-bit platforms since Go 1.4 (stackMin = 2048 in stack.go, with stackSystem == 0 on Linux/macOS so fixedStack resolves to exactly 2048 bytes) — and grow by copying: newstack allocates a larger stack, copies the contents, and rewrites pointers. During that copy the goroutine must be frozen and its stack must not be scanned by the GC; _Gcopystack is the lock that enforces this. See Goroutine Stacks.
_Gpreempted (9). “Stopped itself for a suspendG preemption. It is like _Gwaiting, but nothing is yet responsible for ready()ing it. Some suspendG must CAS the status to _Gwaiting to take responsibility.” Introduced for Go 1.14’s asynchronous preemption: when a goroutine is preempted by signal, it parks itself in _Gpreempted. The distinction from _Gwaiting is ownership of the resumption — a _Gwaiting goroutine has a waker (the channel, the mutex); a _Gpreempted goroutine has none until a suspender claims it. See Goroutine Preemption.
_Gleaked (10). A goroutine the garbage collector has identified as permanently blocked — leaked. “_Gleaked represents a leaked goroutine caught by the GC.” The GC, while tracing, can prove that a _Gwaiting goroutine’s blocking primitive is unreachable from any runnable goroutine, hence can never be signalled; such a goroutine is moved to _Gleaked so the goroutine-leak profile can report it (Go 1.26 release notes). The constant — and its scan composite _Gscanleaked (0x100a) — is defined unconditionally in runtime2.go at the go1.26.3 tag, not behind a build tag; what is gated is the profiler that drives transitions into it. Leak detection is enabled by the GoroutineLeakProfile experiment (GOEXPERIMENT=goroutineleakprofile, off by default in 1.26 per internal/goexperiment/flags.go), so on a default build the state compiles in but is never reached. See Goroutine Leaks.
_Gdeadextra (11). A _Gdead goroutine attached to an “extra M” — an M created to service a cgo callback from a C-owned thread. See cgo Internals.
The _Gscan Bit — GC Stack Scanning
_Gscan is 0x1000, a bit OR-ed onto a base state to produce composites like _Gscanrunnable (0x1001), _Gscanrunning (0x1002), _Gscansyscall (0x1003), _Gscanwaiting (0x1004), _Gscanpreempted (0x1009), and (in Go 1.26) _Gscanleaked (0x100a) and _Gscandeadextra (0x100b). The runtime comment: “_Gscan combined with one of the above states other than _Grunning indicates that GC is scanning the stack.” The goroutine is not executing user code and the stack is owned by the goroutine that set the _Gscan bit. The formula atomicstatus &^ _Gscan (clear the scan bit) recovers the state the goroutine will return to once scanning finishes.
This is the precise point where “state = stack lock” pays off. To scan a goroutine’s stack for live pointers, the GC must guarantee the goroutine is not mutating that stack. It does so by atomically setting the _Gscan bit (castogscanstatus): the CAS succeeds only if the goroutine was in a scannable state, and while the bit is set the goroutine cannot transition (its own attempts to change state CAS-loop and spin). Once the scan completes, the GC clears the bit and the goroutine proceeds. _Gscanrunning is the one special case — it briefly blocks transitions while the GC asks a running goroutine to scan its own stack.
Mechanical Walk-through — A Goroutine’s Life
Consider go work():
- Creation.
newprocallocates (or recycles fromgFree) agstruct in_Gidle, sets up a small stack (2 KiB by default, or the GC-tunedstartingStackSizeaverage — see Goroutine Stacks), points the instruction pointer atwork, and transitions it to_Grunnable. It is placed on the current P’s local run queue (preferentially asrunnext). - Dispatch. A P, looking for work in
schedule/findrunnable, dequeues the goroutine.executetransitions it_Grunnable → _Grunning(viacasgstatus) and jumps intowork. - Running. While in
_Grunningthe goroutine executes user code. If it calls a function whose prologue detectsstackguard0 == stackPreempt, or it receives a preemption signal, it cooperatively or asynchronously yields. A bare yield (runtime.Gosched) goes_Grunning → _Grunnableand back onto a run queue; an async preemption goes_Grunning → _Gpreempted. - Blocking on a channel. If
workdoes<-chwith no sender,goparktransitions it_Grunning → _Gwaiting, links thegontoch’s receive wait queue, and callsscheduleto run something else. When a sender arrives, it callsgoready, moving the goroutine_Gwaiting → _Grunnable. - Syscall. If
workdoes a blockingread,entersyscalltransitions_Grunning → _Gsyscalland releases the P (so another goroutine can use it) while keeping the M (which is now in the kernel). On return,exitsyscalltries to reacquire a P: if it gets one quickly it goes straight back to_Grunning; if not, it goes to_Grunnableand waits. If the syscall ran long, sysmon may have already retaken the P. - Stack growth. If
workrecurses deeply and a function prologue finds the stack too small,newstacktransitions it_Grunning → _Gcopystack, allocates a doubled stack, copies frames, fixes up pointers, and transitions back to_Grunning. - Death. When
workreturns,goexittransitions_Grunning → _Gdead, clears theg’s fields, and links it onto the P’sgFreelist. A futuregostatement may pull this exactgback out, sending it_Gdead → _Gidle → _Grunnable.
Code: Inspecting and Transitioning State
// runtime-internal: every state transition goes through casgstatus,
// which is a CAS loop that also enforces _Gscan-bit rules.
func casgstatus(gp *g, oldval, newval uint32) {
// loop until the CAS succeeds; if _Gscan is set, the scanner
// owns the goroutine and we must wait for it to clear the bit.
for i := 0; !gp.atomicstatus.CompareAndSwap(oldval, newval); i++ {
// ... validation, sanity checks, possible osyield ...
}
}- Every legal transition funnels through
casgstatus(orcastogscanstatusfor setting the scan bit). The CAS is what makes the state a genuine lock: a transition fromoldvalfails if some other actor (the GC, the scheduler) has already moved the goroutine, e.g. set_Gscan. - There is no public Go API to read a goroutine’s state of another goroutine directly. The supported, indirect window is a goroutine dump.
From application code you observe states through tooling, not the g struct:
import "runtime"
buf := make([]byte, 1<<20)
n := runtime.Stack(buf, true) // all goroutines
os.Stderr.Write(buf[:n])The dump prints each goroutine with a human-readable state string — running, runnable, syscall, chan receive, select, IO wait, sleep, semacquire, etc. Those strings are produced from atomicstatus plus the waitreason field (a finer label attached when entering _Gwaiting). A SIGQUIT (Ctrl-\) on a stuck program prints the same dump — the canonical first step in diagnosing a hang.
Failure Modes and Common Misunderstandings
“A blocked goroutine costs a thread.” Only if blocked in a syscall. A _Gwaiting goroutine costs only its g struct and stack — no M, no P. This is why a Go server can hold a million idle connections: a million _Gwaiting goroutines, a few dozen OS threads. A goroutine in _Gsyscall, by contrast, does tie up an M (the M is in the kernel), though the runtime spins up replacement Ms so other goroutines keep running.
“_Grunnable means running.” It means ready to run, not running. A long backlog of _Grunnable goroutines with few Ps shows up as scheduler latency — the gap between _Grunnable and _Grunning. Go 1.26’s new /sched/goroutines runtime metrics expose per-state counts so you can quantify exactly this (Go 1.26 release notes).
Goroutine leaks are _Gwaiting forever. A leaked goroutine is not a special bug-state historically — it is an ordinary _Gwaiting goroutine parked on a channel that will never receive a signal. It is never collected (its g and stack are reachable from the wait queue) and never runs. The Go 1.26 experimental profiler now promotes such goroutines to _Gleaked when the GC can prove the blocking primitive is unreachable. See Goroutine Leaks and Goroutine Leak via Blocked Channel.
Confusing _Gpreempted with _Gwaiting. They look identical (both parked, not running) but differ in who resumes them. A _Gwaiting goroutine has a designated waker; a _Gpreempted one is in limbo until a suspendG caller CASes it to _Gwaiting and accepts responsibility. Getting this wrong in runtime code risks a goroutine that is preempted and then never rescheduled.
The _Gscan bit and “why is my goroutine briefly stuck.” During a GC mark phase, every goroutine’s stack is scanned exactly once. While its _Gscan bit is set the goroutine cannot change state — if it tries (e.g. it wants to block on a channel) it CAS-loops. These pauses are microseconds and are part of the GC’s concurrent design, but they are why even a “concurrent” GC has tiny per-goroutine stalls.
Alternatives and Comparisons
OS threads have an analogous lifecycle — new, runnable, running, blocked, terminated — but the kernel owns the transitions and a blocked thread still consumes a kernel thread structure and a fixed (often megabytes) stack. Go’s goroutine states are managed entirely in user space by the runtime, transitions are a single atomic CAS, and a blocked (_Gwaiting) goroutine costs only a small g struct and a growable stack. Erlang processes have a similar tiny-cost model with states runnable / running / waiting / exiting; BEAM, like Go, schedules them in user space. The distinctive Go twist is the dual role of the state word as a stack lock for the garbage collector — Erlang’s per-process heaps and copying collector do not need a global stack-scan handshake, so its process states carry no equivalent of the _Gscan bit.
Production Notes
The goroutine dump is the workhorse. On a hung service, SIGQUIT prints every goroutine with its state and (for _Gwaiting) its wait reason and the stack frame where it parked. A pile of goroutines all in chan receive on the same line is a classic leaked-channel pattern; a pile in semacquire points at lock contention; a pile in IO wait is normal for a network server. pprof’s goroutine profile (/debug/pprof/goroutine) aggregates the same data into a count-per-stack profile — a steadily climbing count for one stack is the signature of a leak.
Go 1.26 adds two things directly relevant here. First, the /sched/goroutines metrics family gives live per-state counts (waiting, runnable, etc.) plus /sched/goroutines-created — cheap to scrape continuously, unlike a full dump. Second, the experimental goroutineleak profile (GOEXPERIMENT=goroutineleakprofile, endpoint /debug/pprof/goroutineleak) reports goroutines the GC has proven leaked — and it is the machinery behind the new _Gleaked state (Go 1.26 release notes). The Go team aims to make that profile default in Go 1.27.
See Also
- Goroutine — what a goroutine is and how it is created
- GMP Scheduler Model — the G-M-P triple that schedules these states
- Goroutine Stacks — the growable stacks the state word locks
- Goroutine Preemption — the
_Gpreemptedtransition and signal preemption - System Calls and the Scheduler — the
_Gsyscallstate and P handoff - Sysmon System Monitor — retakes Ps from
_Gsyscallgoroutines - Goroutine Leaks —
_Gwaitingforever and the new_Gleakedstate - Scheduler Tracing and GODEBUG schedtrace — observing state counts
- Go Garbage Collector — why the state word is also a stack lock
- Go Internals MOC — Section 6, The Runtime and Scheduler