Go Goroutines & Concurrency
Launching Goroutines
go keyword — lightweight concurrent execution
func main() {
go doWork("task-1")
go doWork("task-2")
time.Sleep(time.Second) // crude wait — use WaitGroup in real code
}
func doWork(name string) {
fmt.Printf("%s: started\n", name)
time.Sleep(500 * time.Millisecond)
fmt.Printf("%s: done\n", name)
}
go f() launches f on a new goroutine — a lightweight, runtime-managed thread. Goroutines cost about 2KB of stack (grows as needed). You can run thousands without issue.
WaitGroup
sync.WaitGroup coordinates goroutine lifecycle
var wg sync.WaitGroup
urls := []string{
"https://example.com",
"https://httpbin.org/get",
}
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resp, err := http.Get(u)
if err != nil {
fmt.Printf("FAIL %s: %v\n", u, err)
return
}
resp.Body.Close()
fmt.Printf("OK %s: %d\n", u, resp.StatusCode)
}(url)
}
wg.Wait() // blocks until all goroutines call Done()
Add(1) before launching, defer Done() inside the goroutine, Wait() in the caller.
Mutex
sync.Mutex prevents data races
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.v[key]++
}
func (c *SafeCounter) Get(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.v[key]
}
defer c.mu.Unlock() ensures the lock is released even on panic. sync.RWMutex allows concurrent reads with RLock()/RUnlock().
Worker Pool
Bounded concurrency via goroutines and channels
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("worker %d processing job %d\n", id, j)
time.Sleep(time.Second)
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// Start 3 workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send 9 jobs
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
// Collect results
for r := 1; r <= 9; r++ {
fmt.Println(<-results)
}
}
The worker pool bounds concurrency to N goroutines. Workers read from a shared jobs channel. close(jobs) signals no more work.
Context
context.Context for cancellation and timeouts
func fetchWithTimeout(url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err // includes context.DeadlineExceeded
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
context.WithTimeout creates a context that cancels after a deadline. defer cancel() releases resources immediately when the function returns. Pass context as the first parameter to every function that does I/O.