Goroutines & Channels
This is the feature Go is famous for: running many things at once without the heavyweight threads or complicated locking most languages force on you. A goroutine is a function that runs concurrently with the rest of your program, and a channel is how goroutines safely pass data to each other.
Starting a goroutine
The go keyword in front of a function call runs it as a goroutine — the call returns immediately, and the function runs concurrently in the background:
package main import ( "fmt" "time" ) func sayHello() { fmt.Println("Hello from a goroutine!") } func main() { go sayHello() time.Sleep(100 * time.Millisecond) fmt.Println("Done") }
Hello from a goroutine! Done
time.Sleep call is a crude teaching device, not something real code should do. main exits the instant it finishes, and when it does, every goroutine still running gets cut off mid-flight — without giving sayHello time to run first, "Hello from a goroutine!" might never print at all. Real Go code uses proper synchronization, like sync.WaitGroup, instead of sleeping and hoping.Channels: passing data between goroutines
A channel is a typed pipe — one goroutine sends a value into it, another receives it, and the receive blocks until a value actually arrives:
package main import "fmt" func square(n int, results chan int) { results <- n * n } func main() { results := make(chan int) go square(7, results) value := <-results fmt.Println("Result:", value) }
Result: 49
results <- n * n sends a value into the channel; <-results receives one. make(chan int) creates an unbuffered channel, meaning the send blocks until something is ready to receive it — that blocking is exactly what makes this safer than time.Sleep: value := <-results genuinely waits for the goroutine to finish, instead of guessing how long that takes.
Waiting for several goroutines with sync.WaitGroup
package main import ( "fmt" "sync" ) func worker(id int, wg *sync.WaitGroup) { defer wg.Done() fmt.Println("Worker", id, "done") } func main() { var wg sync.WaitGroup for i := 1; i <= 3; i++ { wg.Add(1) go worker(i, &wg) } wg.Wait() fmt.Println("All workers finished") }
Worker 3 done Worker 1 done Worker 2 done All workers finished
wg.Add(1) before each goroutine increments a counter, and defer wg.Done() inside worker decrements it when that goroutine finishes. wg.Wait() blocks main until the counter hits zero — until every worker has called Done. Notice the workers print in 3, 1, 2 order here, not 1, 2, 3: goroutines run concurrently, so their relative finishing order isn't guaranteed.
worker(i, &wg) does above) in plenty of existing Go code, and it's still a perfectly good habit.error-checking idiom, arrays and slices, maps, structs and methods, pointers, interfaces and Go's implicit style of polymorphism, and finally goroutines and channels. From here, the natural next steps are building a real command-line tool or web server with Go's standard library, and exploring generics, added to the language in Go 1.18.