Skip to main content

Go Ep 10: Goroutines & Channel Basics

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
go - This article is part of a series.
Part 10: This Article
Do not communicate by sharing memory; instead, share memory by communicating. Goroutines and Channels form the core primitives of Go’s concurrency model.

TL;DR (Quick Summary)
#

  • go Keyword: Spawns a new concurrent Goroutine starting at ~2KB memory.
  • Unbuffered Channels (make(chan T)): Synchronous handoff. Sender blocks until receiver receives.
  • Buffered Channels (make(chan T, N)): Asynchronous buffer. Sender blocks ONLY when buffer is full (len == N).
  • Channel Directions: chan<- T (Send-only), <-chan T (Receive-only). Enforces safety in function signatures.
  • select Statement: Multiplexes multiple channel operations, executing whichever case is ready first.

1. Unbuffered vs Buffered Channel Flow
#


graph LR
    subgraph Unbuffered Channel (Sync Handoff)
        Sender1["Goroutine A: ch <- val"] -->|Blocks until Receiver ready| SyncChan["Unbuffered chan int"]
        SyncChan --> Receiver1["Goroutine B: <-ch"]
    end

    subgraph Buffered Channel (Async Buffer: Cap 2)
        Sender2["Goroutine A: ch <- val"] -->|Fills Buffer instantly| BufChan["'[ Val 1 | Val 2 "]"]
        BufChan --> Receiver2["Goroutine B: <-ch"]
    end

2. Step-by-Step Lab: Channels and select Multiplexing
#

package main

import (
 "fmt"
 "time"
)

// Channel Direction: Send-Only channel parameter (chan<- string)
func fetchAPI1(ch chan<- string) {
 time.Sleep(100 * time.Millisecond)
 ch <- "Response from API 1"
}

// Channel Direction: Send-Only channel parameter
func fetchAPI2(ch chan<- string) {
 time.Sleep(200 * time.Millisecond)
 ch <- "Response from API 2"
}

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

 go fetchAPI1(ch1)
 go fetchAPI2(ch2)

 // Multiplex channel reads using select
 for i := 0; i < 2; i++ {
  select {
  case msg1 := <-ch1:
   fmt.Println("Received:", msg1)
  case msg2 := <-ch2:
   fmt.Println("Received:", msg2)
  case <-time.After(500 * time.Millisecond):
   fmt.Println("Timeout waiting for API response!")
  }
 }
}

3. Comparison: Unbuffered vs Buffered Channels
#

PropertyUnbuffered ChannelBuffered Channel
Declarationmake(chan T)make(chan T, capacity)
Capacity0capacity > 0
Send BlockingBlocks until receiver reads.Blocks ONLY when buffer is full.
SynchronizationGuarantees exact point-in-time handshake.Decouples sender and receiver execution timing.

4. Troubleshooting & Common Errors
#

Error 1: Send on Closed Channel Panic
#

The Cause: Attempting to execute ch <- val on a channel that has already been closed.

close(ch)
ch <- 42 // 💥 panic: send on closed channel

The Fix: Always enforce a single owner pattern: only the sender should close a channel, never the receiver!


Summary & Next Steps
#

In this episode:

  • We spawned lightweight Goroutines with the go keyword.
  • We passed messages between Goroutines using Unbuffered and Buffered channels.
  • We multiplexed channel reads using select.

In Episode 11: Advanced Concurrency & Worker Pools, we will master sync.WaitGroup, sync.Mutex, and building scalable Worker Pools!

go - This article is part of a series.
Part 10: This Article