Concurrency Basics
10 examples to get you started with Concurrency - 7 basic and 3 intermediate.
Search across all documentation pages
10 examples to get you started with Concurrency - 7 basic and 3 intermediate.
mkdir conc && cd conc && go mod init example.com/conc.main.go and run with go run ..go test -race ./... when examples use shared state.Start work concurrently with go.
package main
import (
"fmt"
"time"
)
func main() {
go func() {
fmt.Println("from goroutine")
}()
time.Sleep(100 * time.Millisecond) // wait so output appears before exit
fmt.Println("from main")
}go schedules the function on a new goroutine; it returns immediately.main may exit before the goroutine prints.sync.WaitGroup or channels over time.Sleep in real code.Related: Concurrency in Go: Goroutines and Channels First - CSP mental model
Sender and receiver meet at the channel.
package main
import "fmt"
func main() {
ch := make(chan string)
go func() { ch <- "ping" }()
msg := <-ch
fmt.Println(msg)
}make(chan string) creates an unbuffered channel - zero capacity.Related: Channels: Buffered, Unbuffered & Closing Rules - buffer and close rules
Decouple sender and receiver up to capacity.
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch, <-ch)
}make(chan int, 2) allows two sends without a waiting receiver.Related: Channels: Buffered, Unbuffered & Closing Rules - when to buffer
Wait for a batch of goroutines to finish.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println("worker", n)
}(i)
}
wg.Wait()
fmt.Println("done")
}Add before go, Done in a defer inside the worker.Wait blocks until the counter returns to zero.Related: sync.Mutex, RWMutex & WaitGroup - WaitGroup details
Signal completion by closing from the sender.
package main
import "fmt"
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
for v := range ch {
fmt.Println(v)
}
}range drains until the channel is closed and empty.close; sending on a closed channel panics.ok == false in two-value form.Related: Channels: Buffered, Unbuffered & Closing Rules - ownership rules
select waits on channel operations (here, a single receive).
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
time.Sleep(50 * time.Millisecond)
ch <- "ready"
}()
select {
case msg := <-ch:
fmt.Println(msg)
}
}select blocks until one case can proceed.default branch for non-blocking behavior.Related: select Statement & Default Cases - multiplexing patterns
When multiple goroutines mutate data, lock it.
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
count := 0
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
count++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println(count)
}Mutex serializes critical sections - here count++.Lock with Unlock; defer mu.Unlock() after Lock is idiomatic.-race to catch missing locks.Related: sync.Mutex, RWMutex & WaitGroup - lock discipline
Avoid waiting forever on slow work.
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
select {
case msg := <-ch:
fmt.Println(msg)
case <-time.After(100 * time.Millisecond):
fmt.Println("timeout")
}
}time.After returns a channel that fires after the duration.context.Context deadlines instead of ad-hoc sleeps.Related: select Statement & Default Cases - timeout recipes
One producer, one consumer goroutine.
package main
import "fmt"
func main() {
jobs := make(chan int, 5)
done := make(chan struct{})
go func() {
for j := range jobs {
fmt.Println("processed", j)
}
close(done)
}()
for i := 1; i <= 3; i++ {
jobs <- i
}
close(jobs)
<-done
}jobs lets the worker range exit cleanly.done signals the consumer finished - alternative to WaitGroup.Related: Goroutines: Creation Cost & Scheduling Overview - scheduling many workers
Build with -race to catch unsynchronized access.
package main
import (
"sync"
"testing"
)
func TestCounter(t *testing.T) {
var wg sync.WaitGroup
n := 0
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
n++ // race: uncomment mu from example 7 to fix
}()
}
wg.Wait()
}go test -race - the race detector reports conflicting accesses.-race in CI for packages that use goroutines.Related: Race Detector: Running and Interpreting Output - reading race output
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 19, 2026