Advanced Concurrency Basics
10 examples to get you started with Advanced Concurrency - 7 basic and 3 intermediate.
Search across all documentation pages
10 examples to get you started with Advanced Concurrency - 7 basic and 3 intermediate.
go mod init example.com/advconc.go get golang.org/x/sync/errgroup golang.org/x/time/rate.go run . from each snippet's package directory.Spin up N workers that read jobs until the channel closes.
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs {
fmt.Printf("worker %d job %d\n", id, j)
}
}
func main() {
const workers = 3
jobs := make(chan int, 10)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go worker(i, jobs, &wg)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
wg.Wait()
}close(jobs) signals workers to exit after draining.WaitGroup waits for workers before main returns.Related: Worker Pools & Task Queues - sizing and shutdown patterns
Limit in-flight goroutines without a dedicated worker set.
package main
import (
"fmt"
"sync"
)
func main() {
sem := make(chan struct{}, 2) // max 2 concurrent
var wg sync.WaitGroup
for i := 0; i < 6; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
fmt.Println("task", n)
}(i)
}
wg.Wait()
}golang.org/x/sync/semaphore when you need Acquire(ctx) with cancellation.Related: Rate Limiting & Backpressure - token buckets and shedding
Run related goroutines; first error cancels the rest.
package main
import (
"context"
"fmt"
"time"
"golang.org/x/sync/errgroup"
)
func main() {
g, ctx := errgroup.WithContext(context.Background())
g.Go(func() error {
return fmt.Errorf("task a failed")
})
g.Go(func() error {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Second):
return nil
}
})
if err := g.Wait(); err != nil {
fmt.Println("errgroup:", err)
}
}WithContext cancels ctx when any Go returns non-nil error.ctx in long-running Go functions.Wait blocks until all goroutines finish and returns the first error.Related: errgroup & golang.org/x/sync Extensions - SetLimit and patterns
One goroutine produces, another consumes through a channel.
package main
import (
"fmt"
)
func main() {
out := make(chan int)
go func() {
defer close(out)
for i := 1; i <= 3; i++ {
out <- i * 10
}
}()
for v := range out {
fmt.Println(v)
}
}close it when done.range over channel receives until close.Related: Fan-In, Fan-Out & Pipeline Stages - multi-stage graphs
Distribute items to workers, wait for completion.
package main
import (
"fmt"
"sync"
)
func main() {
items := []int{1, 2, 3, 4}
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(v int) {
defer wg.Done()
fmt.Println("process", v)
}(item)
}
wg.Wait()
}Related: Production Concurrency: Patterns Beyond Hello Goroutine - when to bound fan-out
Stop workers when context is done.
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Println("worker", id, "stop")
return
default:
fmt.Println("worker", id, "tick")
time.Sleep(200 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
go worker(ctx, 1)
<-ctx.Done()
time.Sleep(100 * time.Millisecond)
}select on ctx.Done() is mandatory in production loops.WithTimeout derives a deadline from the parent context.WaitGroup when multiple workers must exit before proceeding.Related: context Basics - WithCancel and deadlines
Lock-free counter for metrics and IDs.
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var n atomic.Int64
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
n.Add(1)
}()
}
wg.Wait()
fmt.Println("count", n.Load())
}atomic.Int64 is typed and clearer than raw int64 with AddInt64.go test -race if you mix atomics with non-atomic reads.Related: sync/atomic Operations - CAS and when atomics win
Graceful stop: cancel context, close jobs, wait for workers.
package main
import (
"context"
"fmt"
"sync"
"time"
)
func pool(ctx context.Context, jobs <-chan int, n int) *sync.WaitGroup {
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case j, ok := <-jobs:
if !ok {
return
}
fmt.Println(id, "handled", j)
}
}
}(i)
}
return &wg
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
jobs := make(chan int, 4)
wg := pool(ctx, jobs, 2)
for j := 1; j <= 4; j++ {
jobs <- j
}
close(jobs)
cancel()
wg.Wait()
}ctx.Done() and !ok on jobs covers both cancel and drain paths.wg.Wait() before exit confirms workers stopped.Related: Avoiding Goroutine Leaks & Channel Deadlocks - ownership rules
Simulate parallel I/O with shared cancel on first failure.
package main
import (
"context"
"errors"
"fmt"
"golang.org/x/sync/errgroup"
)
func fetch(ctx context.Context, name string, fail bool) error {
if fail {
return fmt.Errorf("%s unavailable", name)
}
select {
case <-ctx.Done():
return ctx.Err()
default:
fmt.Println("ok", name)
return nil
}
}
func main() {
g, ctx := errgroup.WithContext(context.Background())
g.Go(func() error { return fetch(ctx, "users", false) })
g.Go(func() error { return fetch(ctx, "billing", true) })
if err := g.Wait(); err != nil {
fmt.Println("abort:", err)
}
}ctx for siblings still running.http.NewRequestWithContext in real services.g.SetLimit(n) when fan-out must respect downstream quotas.Related: Cancellation Propagation in HTTP Handlers - request-scoped ctx
Token bucket limits how fast jobs enter the pool.
package main
import (
"context"
"fmt"
"time"
"golang.org/x/time/rate"
)
func main() {
lim := rate.NewLimiter(rate.Every(100*time.Millisecond), 1)
ctx := context.Background()
for i := 1; i <= 5; i++ {
if err := lim.Wait(ctx); err != nil {
panic(err)
}
fmt.Println("emit", i, time.Now().Format("15:04:05.000"))
}
}Every(d) sets steady rate; burst allows short spikes.Wait blocks; Allow is non-blocking for shed-load paths.ctx into Wait so cancel stops a throttled producer.Related: Rate Limiting & Backpressure - semaphores vs token bucket
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 18, 2026