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:
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 } }
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:
Cons:
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:
Cons: