Concurrency is one of Go’s most powerful features. Go makes it easy to write programs that do multiple things at once through goroutines (lightweight threads) and channels (communication between goroutines).
Concurrency vs Parallelism:
Concurrency is about dealing with multiple things at once (structure)
Parallelism is about doing multiple things at once (execution)
Go provides concurrency primitives. Whether they run in parallel depends on your hardware and runtime.
Starting a goroutine is simple - just use the go keyword:
Basic Goroutine
package mainimport ( "fmt" "time")func say(s string) { for i := 0; i < 3; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) }}func main() { // Start a goroutine go say("world") // Run in main goroutine say("hello")}
Output (order may vary):
helloworldhelloworldhelloworld
When main() returns, all goroutines are terminated, whether they’ve finished or not. Use synchronization to wait for goroutines to complete.
ch := make(chan int)go func() { ch <- 42 // Send value to channel ch <- 17}()
Receiving
ch := make(chan int)go func() { ch <- 42}()value := <-ch // Receive and assignfmt.Println(<-ch) // Receive and use directly
Buffered Channels
// Unbuffered: sender blocks until receiver is readych1 := make(chan int)// Buffered: sender blocks only when buffer is fullch2 := make(chan int, 3)ch2 <- 1 // Doesn't blockch2 <- 2 // Doesn't blockch2 <- 3 // Doesn't block// ch2 <- 4 // Would block - buffer full
Closing Channels
ch := make(chan int, 2)ch <- 1ch <- 2close(ch) // Signal no more values// Can still receive from closed channelfmt.Println(<-ch) // 1fmt.Println(<-ch) // 2fmt.Println(<-ch) // 0 (zero value)// Check if channel is closedv, ok := <-chif !ok { fmt.Println("Channel closed")}
func generate(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out}func square(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * n } close(out) }() return out}func main() { // Pipeline: generate -> square nums := generate(1, 2, 3, 4) squared := square(nums) for result := range squared { fmt.Println(result) // 1, 4, 9, 16 }}
func fanOut(in <-chan int, workers int) []<-chan int { channels := make([]<-chan int, workers) for i := 0; i < workers; i++ { channels[i] = square(in) } return channels}func fanIn(channels ...<-chan int) <-chan int { out := make(chan int) var wg sync.WaitGroup for _, ch := range channels { wg.Add(1) go func(c <-chan int) { defer wg.Done() for n := range c { out <- n } }(ch) } go func() { wg.Wait() close(out) }() return out}func main() { in := generate(1, 2, 3, 4, 5, 6, 7, 8) // Fan-out to 3 workers workers := fanOut(in, 3) // Fan-in results results := fanIn(workers...) for result := range results { fmt.Println(result) }}
func process(jobs <-chan Job) { for job := range jobs { // Range exits when channel is closed handleJob(job) }}// Or check explicitlyfunc process(jobs <-chan Job) { for { job, ok := <-jobs if !ok { return // Channel closed } handleJob(job) }}
Prevent Goroutine Leaks
// Bad: Goroutine may leakfunc search(term string) <-chan Result { ch := make(chan Result) go func() { result := doSearch(term) ch <- result // Blocks forever if receiver gives up }() return ch}// Good: Use buffered channel or contextfunc search(ctx context.Context, term string) <-chan Result { ch := make(chan Result, 1) go func() { result := doSearch(term) select { case ch <- result: case <-ctx.Done(): return // Exit if context cancelled } }() return ch}