skip to content

The go Statement

Starting concurrent work takes one keyword, but the consequences are less obvious: arguments are evaluated at the go statement, nothing waits for the result, and when main returns every goroutine dies mid-flight. The loop-variable capture bug and its Go 1.22 fix is a near-certain question.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

In Go, what does the go keyword do when placed in front of a function call?

level: juniorimportance: must knowfreq 88%

answer

  1. the caller does not wait
  2. nothing at all comes back to you
  3. a statement, not an expression
  4. results have to travel over a channel

basics

~20 s

It runs that call in a new goroutine and returns immediately, so the caller carries on without waiting. It is a statement, not an expression: it produces no value, no handle and no id you can hold on to.

solid answer

~50 s

`go f(x)` starts `f(x)` as an independent goroutine and the calling goroutine continues with the next statement straight away. The function value and the arguments are evaluated first, in the calling goroutine; only the call itself is handed to the new goroutine, which the runtime schedules onto an OS thread for you. The operand must be a function or method call, so `go f` alone does not compile, and because `go` is a statement you cannot write `v := go f()` — anything `f` returns is discarded, so results have to come back over a channel you set up yourself. You also get nothing back to wait on or cancel with: no goroutine handle, no id, no kill API. A new goroutine costs about 2 KB of growable stack, which is why starting thousands is normal Go.

code

go · 6 lines
go
go check(host)              // ok: a function call
go conn.ping()              // ok: a method call
go func() { check(host) }() // ok: a call of a function literal

// go check                 // invalid: not a call
// v := go check(host)      // invalid: go is a statement, it has no value

go deeper

for a junior

Be ready to say in one sentence that go starts the call concurrently and the caller keeps going, and that you cannot capture a return value from it. Knowing that a channel is how results come back is enough here.

for a middle

Explain what the operand may be, that the function value and arguments are evaluated in the calling goroutine, and that the runtime multiplexes goroutines onto OS threads rather than creating one thread per go statement.

for a senior

Show that you treat the missing pieces as design work: no handle, no id, no kill means the code that writes go owns arranging results, shutdown and waiting. Interviewers listen for whether you notice that responsibility.

for a principal

Frame it as a language tradeoff: the go statement is deliberately minimal so lifecycle policy lives in library code rather than the runtime. Be able to say what conventions you would standardise on so that cheapness does not become sprawl.

## What the statement is A **goroutine** is a function executing concurrently with other goroutines in the same address space. The `go` statement is the only way to create one: ```go go check(host) ``` That single keyword turns an ordinary call into a concurrent one. The calling goroutine does not block: it proceeds to the statement after the `go` statement while `check(host)` runs somewhere else, possibly on another CPU, possibly interleaved on the same one. Every Go program already has one goroutine before it ever runs a `go` statement — the **main goroutine**, created by the runtime to run package initialisation and then `main`. Everything else descends from a `go` statement somebody wrote. ## What the operand may be The operand of `go` must be a **function or method call**. These are all legal: ```go go check(host) // ordinary function call go conn.ping() // method call go func() { check(host) }() // call of a function literal ``` These are not: - `go check` — an identifier, not a call. - `go x + 1` — an expression, not a call. - `v := go check(host)` — `go` is a statement, so it has no value to assign. That last point is the one newcomers trip on. `go` gives you nothing back. Whatever the function returns is thrown away — `go vet` will not even complain, because discarding a return value is legal Go. If the work produces a result, the goroutine has to send it somewhere: a channel, or a variable protected by a mutex. ## What it does *not* give you Compared with a thread API in most other languages, the `go` statement is deliberately bare: - **No handle.** There is no `Goroutine` object, no future, no join method. The `go` statement returns nothing at all. - **No id.** The runtime numbers goroutines internally and prints those numbers in panic dumps and profiles, but there is no supported API to read one, precisely so nobody builds goroutine-local storage on it. - **No kill.** Nothing can stop a goroutine from the outside. A goroutine ends when its function returns (or when the process exits). Stopping one early requires that it cooperate — it must be *asked*, on a channel or through a cancellable value it is watching. - **No join.** If the code that started a goroutine needs to know when it finished, it must arrange that itself: receive from a channel the goroutine closes or sends on, or use `sync.WaitGroup`. The runtime will not wait for anyone. A useful way to phrase it in an interview: `go` is cheap to write and cheap to run, and every bit of the lifecycle management that other languages hand you is left to you on purpose. ## What it costs A goroutine is not an OS thread. Starting one allocates a small runtime structure and a stack of roughly **2 KB**, which grows and shrinks on demand as the goroutine calls deeper. An OS thread typically reserves megabytes up front. The Go runtime multiplexes many goroutines onto a much smaller set of OS threads, so tens or hundreds of thousands of goroutines in one process is an ordinary design, not a stunt. Blocking a goroutine — on a channel, a mutex or network I/O — parks it and frees its thread for other goroutines rather than pinning a kernel thread. ## Evaluation order, in one line The function value and the parameters are evaluated **in the calling goroutine, at the `go` statement**; only the invocation happens in the new goroutine. So `go check(host)` snapshots `host` right there, while `go func() { check(host) }()` reads `host` later, whenever the goroutine actually runs. ## Lifetime A goroutine's lifetime is independent of the one that started it — with one blunt exception: when `main` returns, the process exits and every other goroutine dies where it stands, deferred calls unrun. Concurrency in Go is easy to start and entirely your responsibility to finish.

  • How does a value computed inside a goroutine get back to the code that started it?
    Over a channel, or into shared state guarded by a mutex. The `go` statement itself discards the call's return values, so the goroutine has to send the result somewhere the starter is reading. The usual shape is a channel created before the `go` statement and received from after it.
  • How many goroutines does a Go program have before it executes any go statement?
    One — the main goroutine. The runtime creates it to run package-level initialisation and then `main`. Every other goroutine in the process exists because some code ran a `go` statement.
  • Is a goroutine an OS thread?
    No. It is a runtime-managed unit of execution with its own small growable stack, multiplexed by the Go scheduler onto a pool of OS threads. Many goroutines share one thread, and a goroutine that blocks on a channel or on network I/O is parked without holding a thread.

saying these in an interview costs you the question

  • Says go f() returns a handle you can join on
  • Thinks each go statement starts a new OS thread
  • Expects to assign the goroutine's return value
  • Believes the runtime can kill a goroutine on request
open as a page

In Go, when are the function value and arguments of go f(x) evaluated?

level: middleimportance: must knowfreq 68%

basics

~20 s

Immediately, in the calling goroutine, at the go statement itself; only the call runs in the new goroutine. So go f(x) snapshots x on the spot, while go func(){ f(x) }() reads x later, whenever the goroutine happens to run.

open as a page

What happens to still-running goroutines when a Go program's main function returns?

level: juniorimportance: should knowfreq 70%

basics

~20 s

The process exits immediately and every other goroutine is killed where it stands. Their deferred calls never run and their output may never appear. Nothing waits for them, so main must wait explicitly if the work matters.

open as a page

How much stack does a new goroutine start with, and what happens when it needs more?

level: middleimportance: should knowfreq 55%

basics

~20 s

About 2 KB. When a call would overflow it, the Go runtime allocates a larger stack, copies the existing frames across and adjusts pointers into them, so stacks grow on demand instead of being reserved up front like a thread's.

open as a page

Go gives you no goroutine id and no goroutine-local storage, so how do you tag concurrent work with a request?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

You 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.

open as a page