ProgrammingBackend Developer

How do goroutines and the Go scheduler work, and why is it important to manage concurrent task execution correctly?

Pass interviews with Hintsage AI assistant

Answer.

Goroutines are lightweight threads of execution built into the Go architecture since its early versions for achieving efficient concurrency. Historically, the idea of lightweight threads emerged as an attempt to circumvent the high cost of system threads, and also due to the high demand for scalable server applications. Go was originally designed as a language for server and network systems where millions of tasks need to be processed in parallel.

Problem: Concurrency can quickly lead to race conditions, deadlocks, and increased memory consumption if the lifecycle of goroutines is not monitored, their scheduling is not considered, and their termination is not managed properly.

Solution: Goroutines are launched using the go keyword. The execution of goroutines is managed by the Go scheduler, which uses an M:N model (M OS threads servicing N Go language goroutines). You can manage the lifecycle using channels, WaitGroup, context, and control over channel closure.

Code example:

package main import ("fmt"; "time") func worker(id int) { fmt.Printf("Worker %d started ", id) time.Sleep(time.Second) fmt.Printf("Worker %d done ", id) } func main() { for i := 1; i <= 3; i++ { go worker(i) } time.Sleep(2 * time.Second) }

Key features:

  • Instant and cheap creation of goroutines (tens of thousands of times cheaper than OS threads).
  • Direct interaction through channels, providing synchronization and data sharing.
  • The need for manual control over termination (which goroutines to wait for, who interrupts them, how to signal for stopping).

Tricky questions.

If the main function does not wait for a goroutine, will it always complete?

No, if main completes, the process will terminate regardless of the state of child goroutines, and not all tasks will be executed.

Is calling go func(...) from a loop guaranteed that each goroutine will receive its own value of the loop variables?

No, there is a problem with variable capture; goroutines may work with the same value of the slice/variable. You must use variable copying, for example, by passing it as an argument:

for i := 0; i < 3; i++ { go func(n int) { fmt.Println(n) }(i) }

Can one goroutine block the Go scheduler and prevent others from executing?

Yes, if it runs an infinite or very heavy loop without switch points (e.g., without time function calls or yield), it can hold the OS thread — although this contradicts Go's ideology of "cooperative multitasking." For example, a heavy function without blocking:

func busy() { for { // No waits or blocking calls } }

Common mistakes and anti-patterns

  • Launching goroutines without managing their termination
  • Capturing loop variables without passing them into anonymous functions
  • System overload due to "leaky goroutines" (leaks of non-terminating goroutines)
  • Ignoring synchronization errors when exchanging data through channels

Real-life example

Negative case

In a microservice, a goroutine for reading from the database is periodically launched, but it is forgotten to be terminated upon request cancellation. As a result, "hanging" goroutines are left, which eventually lead to the consumption of all RAM.

Pros:

  • High startup speed
  • Simplicity of scaling

Cons:

  • Memory leakage
  • Increased response time
  • Unpredictable termination

Positive case

Using context for controlling task cancellation, WaitGroup for managing the termination of all goroutines before stopping the application, and channels for correct data transfer between executors.

Pros:

  • Predictable lifecycle
  • Management of termination
  • Easily scalable

Cons:

  • Need to explicitly write cancellation and synchronization logic
  • Slightly more complex program architecture