GO

Go Fundamentals

18 lessons

Progress0%
1. Introduction to Go
1What is Go?
2. Variables and Data Types
1Data Types in Go
3. Control Flow
If, For, and SwitchDefer, Panic, Recover
4. Functions
Function BasicsError Handling
5. Structs and Methods
StructsMethods and Interfaces
6. Concurrency
Goroutines and ChannelsSelect and Sync
7. Maps & Slices Advanced
Slices Deep DiveMaps Operations & Patterns
8. Interfaces Deep Dive
Interface Composition & anyCommon Interfaces & Patterns
9. Packages & Modules
Package SystemGo Modules & Workspace
10. Testing & Standard Library
Testing in GoStandard Library Essentials
All Tutorials
GoConcurrency
Lesson 10 of 18 min
Chapter 6 · Lesson 2

Select and Sync

Select and Sync Primitives in Go

select statement Like a switch for channels — waits on multiple channel operations and executes the first one that is ready:

go
select {
case msg := <-ch1:
    fmt.Println("ch1:", msg)
case msg := <-ch2:
    fmt.Println("ch2:", msg)
case <-time.After(1 * time.Second):
    fmt.Println("timeout")
}

sync.WaitGroup Coordinates completion of a group of goroutines:

  • wg.Add(n) — declare n goroutines to wait for.
  • wg.Done() — call when a goroutine finishes (use defer).
  • wg.Wait() — block until the counter reaches zero.

sync.Mutex Protects shared state from concurrent access:

go
var mu sync.Mutex
mu.Lock()
// critical section
mu.Unlock()

Use defer mu.Unlock() immediately after Lock() to ensure the lock is always released.

Key points:

  • select with a default case is non-blocking.
  • Prefer channels for communication; use mutexes only to protect shared state.
  • sync.RWMutex allows multiple concurrent readers.

Code Examples

select statementgo
package main

import (
	"fmt"
	"time"
)

func ticker(d time.Duration, ch chan<- string, label string) {
	for i := 0; i < 3; i++ {
		time.Sleep(d)
		ch <- fmt.Sprintf("%s-%d", label, i+1)
	}
	close(ch)
}

func main() {
	ch1 := make(chan string, 3)
	ch2 := make(chan string, 3)

	go ticker(10*time.Millisecond, ch1, "fast")
	go ticker(20*time.Millisecond, ch2, "slow")

	time.Sleep(70 * time.Millisecond)
	for {
		select {
		case v, ok := <-ch1:
			if ok { fmt.Println(v) }
		case v, ok := <-ch2:
			if ok { fmt.Println(v) }
		default:
			fmt.Println("done")
			return
		}
	}
}

select picks a ready channel at random when multiple are ready. A default case makes it non-blocking.

sync.WaitGroup and sync.Mutexgo
package main

import (
	"fmt"
	"sync"
)

type SafeCounter struct {
	mu    sync.Mutex
	count int
}

func (c *SafeCounter) Increment() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.count++
}

func (c *SafeCounter) Value() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.count
}

func main() {
	var wg sync.WaitGroup
	counter := &SafeCounter{}

	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			counter.Increment()
		}()
	}
	wg.Wait()
	fmt.Println("Final count:", counter.Value())
}

Without the mutex, concurrent increments would produce a race condition. WaitGroup ensures we wait for all 100 goroutines.

Quick Quiz

1. What does a `default` case in a `select` statement do?

2. Why should you call `defer mu.Unlock()` right after `mu.Lock()`?

Was this lesson helpful?

PreviousNext