goroutine
| Since: | Go 1.0(2012) |
|---|
A goroutine is a lightweight thread provided by Go. By placing the go keyword before a function call, you can run that function concurrently in a separate execution flow.
Syntax
go functionName(args)
// Launches an anonymous function as a goroutine.
go func() {
// Code that runs concurrently.
}()
Goroutines are very lightweight — you can launch thousands to millions simultaneously. However, all goroutines are terminated when main() exits.
Goroutine characteristics
| Feature | Description |
|---|---|
| Lightweight | Far lighter than OS threads. The initial stack size is only a few KB, and grows dynamically as needed. |
| Managed by the Go runtime | The Go runtime maps goroutines onto OS threads. It efficiently schedules N goroutines across M OS threads (the M:N model). |
| Asynchronous execution | A function launched with go returns control immediately. The caller does not wait for the goroutine to finish. |
| Waiting for completion | Use a channel or sync.WaitGroup to wait for a goroutine to complete. |
| Panic propagation | A panic inside a goroutine does not propagate to the caller. Each goroutine must handle panics individually with recover(). |
Sample code
goroutine.go
package main
import (
"fmt"
"sync"
"time"
)
// worker performs a task and signals completion via WaitGroup.
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Notifies the WaitGroup when the work is done.
fmt.Printf("Worker %d: started\n", id)
time.Sleep(time.Millisecond * 100) // Simulates a time-consuming task.
fmt.Printf("Worker %d: done\n", id)
}
func main() {
// Launching a simple goroutine.
go fmt.Println("Output from another goroutine") // Runs asynchronously.
// Use WaitGroup to wait for goroutines to finish.
var wg sync.WaitGroup
// Launch 5 goroutines.
for i := 1; i <= 5; i++ {
wg.Add(1) // Increment the counter before launching the goroutine.
go worker(i, &wg)
}
wg.Wait() // Block until all goroutines have finished.
fmt.Println("All workers completed")
// Launching anonymous functions as goroutines.
// Pass the loop variable as an argument to capture its value correctly.
var wg2 sync.WaitGroup
results := make([]int, 5)
for i := 0; i < 5; i++ {
wg2.Add(1)
go func(idx int) {
defer wg2.Done()
results[idx] = idx * idx // Each goroutine processes its own index independently.
}(i) // Pass the loop variable i as an argument.
}
wg2.Wait()
fmt.Println("Squared results:", results) // Prints '[0 1 4 9 16]'.
}
This produces the following output:
go run goroutine.go Output from another goroutine Worker 1: started Worker 3: started Worker 2: started Worker 5: started Worker 4: started Worker 1: done Worker 3: done Worker 2: done Worker 5: done Worker 4: done All workers completed Squared results: [0 1 4 9 16]
The order of goroutine execution may vary between runs. The output above is just one example.
Communicating between goroutines with channels
Use channels to pass data between goroutines. Go's philosophy is "do not communicate by sharing memory; instead, share memory by communicating." Passing data through channels is generally preferred over protecting shared variables with a Mutex.
sample_goroutine_channel.go
package main
import (
"fmt"
"sync"
)
// Processes a job and sends the result to the channel
func processJob(id int, input string, result chan<- string, wg *sync.WaitGroup) {
defer wg.Done()
result <- fmt.Sprintf("Job%d: %s processed", id, input)
}
func main() {
jobs := []string{"Kiryu", "Majima", "Saejima", "Akiyama", "Shinada"}
results := make(chan string, len(jobs)) // buffered channel
var wg sync.WaitGroup
for i, job := range jobs {
wg.Add(1)
go processJob(i+1, job, results, &wg)
}
// Close the channel after all goroutines finish
go func() {
wg.Wait()
close(results)
}()
// Read results until the channel is closed
for r := range results {
fmt.Println(r)
}
}
This produces the following output:
go run sample_goroutine_channel.go Job1: Kiryu processed Job3: Saejima processed Job2: Majima processed Job4: Akiyama processed Job5: Shinada processed
Goroutines run concurrently, so the order of output may vary between runs.
Cancellation with context
To cancel a long-running goroutine from outside, use context.WithCancel or context.WithTimeout. If goroutines are not designed to be cancellable, they can leak — continuing to run even after the process should have stopped.
sample_goroutine_context.go
package main
import (
"context"
"fmt"
"time"
)
// A goroutine that monitors until the context is cancelled
func monitor(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s: stopping (reason: %v)\n", name, ctx.Err())
return
default:
fmt.Printf("%s: monitoring...\n", name)
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel() // Always defer cancel() to release resources
go monitor(ctx, "Kiryu")
// Wait until the context is cancelled
<-ctx.Done()
fmt.Println("Main: processing finished")
time.Sleep(100 * time.Millisecond) // Wait for the goroutine's final log
}
This produces the following output:
go run sample_goroutine_context.go Kiryu: monitoring... Kiryu: monitoring... Main: processing finished Kiryu: stopping (reason: context deadline exceeded)
The output may vary depending on goroutine scheduling and timing. The number of "monitoring..." lines may differ, and the "stopping (reason: ...)" line may not appear at all in some environments.
Common Mistakes
Common mistake 1: capturing loop variables
Capturing a loop variable directly in a closure causes all goroutines to reference the final value in Go versions below 1.22.
package main
import "sync"
func main() {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// fmt.Println(i) // Below Go 1.22, all goroutines may print "3"
}()
}
wg.Wait()
}
OK: Pass the variable as an argument to create a copy (safe for Go < 1.22).
sample_goroutine_mistakes.go
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Printf("goroutine %d\n", n)
}(i) // A copy of i is passed here
}
wg.Wait()
}
This produces the following output:
go run sample_goroutine_mistakes.go goroutine 0 goroutine 1 goroutine 2
Common mistake 2: passing WaitGroup by value
Passing a WaitGroup by value copies it, causing a separate counter. Always pass a pointer (&wg). Also, always call wg.Add(1) before launching the goroutine — calling it inside the goroutine can allow wg.Wait() to return early.
Overview
Goroutines are the foundation of concurrency in Go. Unlike OS threads, they are extremely lightweight, allowing thousands to millions of goroutines to run simultaneously. However, when goroutines share data, you must synchronize access to avoid race conditions.
When launching goroutines inside a loop, capturing the loop variable directly in a closure causes all goroutines to reference the same final value — a common bug. Always pass the variable as an argument instead.
To wait for goroutines to finish, use either sync.WaitGroup or a channel. For safe access to shared data, use sync.Mutex.
If you find any errors or copyright issues, please contact us.