| name | go-concurrency |
| description | Go concurrency mastery — goroutines, channels, context, sync primitives, patterns, performance. |
Go Concurrency Patterns
Goroutine Lifecycle
Basic Goroutine
go func() {
fmt.Println("running in background")
}()
go func(msg string) {
fmt.Println(msg)
}("hello")
go func() {
defer cleanup()
doWork()
}()
Goroutine Leaks - How to Avoid
func process() {
ch := make(chan int)
go func() {
for val := range ch {
fmt.Println(val)
}
}()
}
func process(ctx context.Context) error {
ch := make(chan int)
done := make(chan struct{})
go func() {
defer close(done)
for {
select {
case val, ok := <-ch:
if !ok {
return
}
fmt.Println(val)
case <-ctx.Done():
return
}
}
}()
close(ch)
<-done
return nil
}
Channel Patterns
Buffered vs Unbuffered
unbuf := make(chan int)
buf := make(chan int, 100)
Fan-in Pattern
func fanIn[T any](ctx context.Context, channels ...<-chan T) <-chan T {
out := make(chan T)
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(c <-chan T) {
defer wg.Done()
for {
select {
case v, ok := <-c:
if !ok {
return
}
select {
case out <- v:
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Fan-out Pattern
func fanOut[T any, R any](ctx context.Context, workers int, input <-chan T, work func(T) R) <-chan R {
out := make(chan R)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for item := range input {
result := work(item)
select {
case out <- result:
case <-ctx.Done():
return
}
}
}()
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Pipeline Pattern
func generate(ctx context.Context, nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
select {
case out <- n:
case <-ctx.Done():
return
}
}
}()
return out
}
func square(ctx context.Context, in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
result := n * n
select {
case out <- result:
case <-ctx.Done():
return
}
}
}()
return out
}
func consume(ctx context.Context, in <-chan int) []int {
var results []int
for n := range in {
results = append(results, n)
}
return results
}
func pipeline(ctx context.Context) []int {
nums := generate(ctx, 1, 2, 3, 4, 5)
squared := square(ctx, nums)
return consume(ctx, squared)
}
Worker Pool Pattern
type Job struct {
ID int
Data string
}
type Result struct {
JobID int
Value string
Err error
}
func workerPool(ctx context.Context, jobs <-chan Job, workers int) <-chan Result {
results := make(chan Result)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for job := range jobs {
result := processJob(job)
select {
case results <- result:
case <-ctx.Done():
return
}
}
}(i)
}
go func() {
wg.Wait()
close(results)
}()
return results
}
func processJob(job Job) Result {
time.Sleep(100 * time.Millisecond)
return Result{
JobID: job.ID,
Value: "processed: " + job.Data,
}
}
func main() {
ctx := context.Background()
jobs := make(chan Job, 100)
go func() {
for i := 0; i < 50; i++ {
jobs <- Job{ID: i, Data: fmt.Sprintf("item-%d", i)}
}
close(jobs)
}()
results := workerPool(ctx, jobs, 5)
for result := range results {
fmt.Printf("Job %d: %s\n", result.JobID, result.Value)
}
}
Context for Cancellation and Deadlines
Context Hierarchy
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
d := time.Now().Add(10 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), d)
defer cancel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
Propagation Pattern
func (s *Service) ProcessOrder(ctx context.Context, orderID string) error {
order, err := s.repo.GetOrder(ctx, orderID)
if err != nil {
return fmt.Errorf("get order: %w", err)
}
apiCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
result, err := s.externalAPI.Validate(apiCtx, order)
if err != nil {
return fmt.Errorf("validate: %w", err)
}
return nil
}
Graceful Shutdown Pattern
type Server struct {
http *http.Server
grpc *grpc.Server
shutdown chan struct{}
}
func (s *Server) Start(ctx context.Context) error {
go func() {
if err := s.http.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("HTTP server error: %v", err)
}
}()
go func() {
if err := s.grpc.Serve(s.lis); err != nil {
log.Printf("gRPC server error: %v", err)
}
}()
<-ctx.Done()
return s.shutdown()
}
func (s *Server) shutdown() error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
var wg sync.WaitGroup
errs := make(chan error, 2)
wg.Add(1)
go func() {
defer wg.Done()
errs <- s.http.Shutdown(ctx)
}()
wg.Add(1)
go func() {
defer wg.Done()
s.grpc.GracefulStop()
errs <- nil
}()
wg.Wait()
close(errs)
for err := range errs {
if err != nil {
return err
}
}
return nil
}
Select for Multiplexing
func multiplex(ctx context.Context, ch1, ch2 <-chan int) {
for {
select {
case v, ok := <-ch1:
if !ok {
fmt.Println("ch1 closed")
return
}
fmt.Printf("ch1: %d\n", v)
case v, ok := <-ch2:
if !ok {
fmt.Println("ch2 closed")
return
}
fmt.Printf("ch2: %d\n", v)
case <-ctx.Done():
fmt.Println("context cancelled")
return
case <-time.After(100 * time.Millisecond):
fmt.Println("timeout - no data")
}
}
}
Synchronization Primitives
Mutex for State Protection
type SafeCounter struct {
mu sync.RWMutex
value int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *SafeCounter) Value() int {
c.mu.RLock()
defer c.mu.RUnlock()
return c.value
}
Once for Initialization
var (
instance *Database
once sync.Once
)
func GetDatabase() *Database {
once.Do(func() {
instance = &Database{conn: connect()}
})
return instance
}
WaitGroup for Coordination
func processItems(items []Item) {
var wg sync.WaitGroup
semaphore := make(chan struct{}, 10)
for _, item := range items {
wg.Add(1)
go func(i Item) {
defer wg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
process(i)
}(item)
}
wg.Wait()
}
Cond for Waiting on Conditions
type Queue struct {
mu sync.Mutex
cond *sync.Cond
items []Item
}
func NewQueue() *Queue {
q := &Queue{
items: make([]Item, 0),
}
q.cond = sync.NewCond(&q.mu)
return q
}
func (q *Queue) Push(item Item) {
q.mu.Lock()
defer q.mu.Unlock()
q.items = append(q.items, item)
q.cond.Signal()
}
func (q *Queue) Pop() Item {
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items) == 0 {
q.cond.Wait()
}
item := q.items[0]
q.items = q.items[1:]
return item
}
Atomic for Simple Counters
type Metrics struct {
requests atomic.Int64
errors atomic.Int64
}
func (m *Metrics) RecordRequest() {
m.requests.Add(1)
}
func (m *Metrics) RecordError() {
m.errors.Add(1)
}
func (m *Metrics) GetStats() (int64, int64) {
return m.requests.Load(), m.errors.Load()
}
ErrorGroup Pattern
import "golang.org/x/sync/errgroup"
func processItems(ctx context.Context, items []Item) error {
g, gctx := errgroup.WithContext(ctx)
results := make(chan Result, len(items))
for _, item := range items {
item := item
g.Go(func() error {
result, err := processItem(gctx, item)
if err != nil {
return err
}
results <- result
return nil
})
}
if err := g.Wait(); err != nil {
return err
}
close(results)
return nil
}
Rate Limiting Pattern
import "golang.org/x/time/rate"
type RateLimiter struct {
limiter *rate.Limiter
}
func NewRateLimiter(rps int) *RateLimiter {
return &RateLimiter{
limiter: rate.NewLimiter(rate.Limit(rps), rps),
}
}
func (rl *RateLimiter) Allow() bool {
return rl.limiter.Allow()
}
func (rl *RateLimiter) Wait(ctx context.Context) error {
return rl.limiter.Wait(ctx)
}
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !rl.Allow() {
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
Backpressure Handling
func backpressureProducer(ctx context.Context, out chan<- Item, rate int) <-chan Item {
results := make(chan Item)
go func() {
defer close(results)
ticker := time.NewTicker(time.Second / time.Duration(rate))
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
item := produce()
select {
case out <- item:
case <-ctx.Done():
return
}
}
}
}()
return results
}
Detecting Race Conditions
go test -race ./...
go run -race main.go
var counter int
func increment() {
counter++
}
var (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock()
counter++
mu.Unlock()
}
Performance Considerations
Best Practices Summary
- Always cancel contexts - Use
defer cancel()
- Never block goroutines indefinitely - Use context or timeouts
- Prefer channels over mutex for orchestration
- Prefer mutex over channels for state protection
- Limit concurrency with worker pools or semaphores
- Always check for race conditions in tests
- Use errgroup for coordinated goroutines
- Close channels from the sender side only
- Never close a channel from the receiver
- Use select for multiplexing and timeouts