Back to blog
Golang 2025-02-03

Go Concurrency: Goroutines and Channels Explained

One of the first things that makes developers fall in love with Go is its concurrency model. Where other languages bolt concurrency on top as an afterthought, Go was designed from day one to make writing concurrent code as natural as writing sequential code.

What is a Goroutine?

A goroutine is a lightweight, independently executing function. Think of it as a thread that costs almost nothing – you can run thousands (or millions) of them without breaking a sweat.

package main

import (
    "fmt"
    "time"
)

func fetchTokenPrice(token string) {
    // imagine a real HTTP call here
    time.Sleep(100 * time.Millisecond)
    fmt.Printf("Price for %s fetched\n", token)
}

func main() {
    tokens := []string{"HBAR", "ETH", "BTC", "SOL"}

    for _, token := range tokens {
        go fetchTokenPrice(token) // starts each fetch concurrently
    }

    // wait a bit so goroutines can finish
    time.Sleep(200 * time.Millisecond)
}

Starting a goroutine is simply go functionCall(). That is the entire syntax.

Channels: Safe Communication Between Goroutines

Goroutines communicate through channels. A channel is a typed pipe you can send values into and receive values from.

ch := make(chan string) // unbuffered channel

// sender goroutine
go func() {
    ch <- "hello from goroutine"
}()

// receiver (blocks until a value arrives)
msg := <-ch
fmt.Println(msg) // hello from goroutine

The blocking behaviour is the key insight: channels synchronise goroutines without mutexes.

A Real-World Pattern: Fan-Out / Fan-In

When I (MrBns) build blockchain data pipelines I use the fan-out / fan-in pattern constantly. Fetch N things concurrently, collect them all.

package main

import (
    "fmt"
    "sync"
)

type TokenInfo struct {
    Symbol string
    Price  float64
}

func fetchToken(symbol string, wg *sync.WaitGroup, results chan<- TokenInfo) {
    defer wg.Done()
    // simulate work
    results <- TokenInfo{Symbol: symbol, Price: 1.23}
}

func main() {
    symbols := []string{"HBAR", "ETH", "BTC"}
    results := make(chan TokenInfo, len(symbols))

    var wg sync.WaitGroup
    for _, s := range symbols {
        wg.Add(1)
        go fetchToken(s, &wg, results)
    }

    // close the channel once all goroutines finish
    go func() {
        wg.Wait()
        close(results)
    }()

    // drain the channel
    for info := range results {
        fmt.Printf("%s: $%.2f\n", info.Symbol, info.Price)
    }
}

Select: Wait on Multiple Channels

select is like a switch for channels. Use it to implement timeouts and non-blocking receives:

import "time"

func withTimeout(ch chan string) {
    select {
    case msg := <-ch:
        fmt.Println("received:", msg)
    case <-time.After(2 * time.Second):
        fmt.Println("timed out – no data received")
    }
}

This pattern is essential when you are waiting on external APIs (like Hedera mirror nodes) and cannot afford to block indefinitely.

Context: The Idiomatic Cancellation Signal

For long-running operations, use context.Context to propagate cancellation:

import (
    "context"
    "fmt"
    "time"
)

func streamEvents(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("stopped:", ctx.Err())
            return
        default:
            fmt.Println("processing event...")
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    streamEvents(ctx)
}

Common Mistakes to Avoid

  1. Goroutine leaks – always ensure every goroutine has a way to exit.
  2. Closing a channel from the receiver – only the sender should close a channel.
  3. Sharing memory without synchronisation – use channels or sync.Mutex, never both sloppily.
  4. Ignoring the race detector – always run go test -race ./... before shipping.

Summary

Go’s concurrency primitives – goroutines, channels, select, and context – give you an incredibly expressive toolkit for building concurrent systems. As a Go developer and Blockchain Full Stack Developer, I use these patterns every day in event-driven blockchain services, real-time price feeds, and high-throughput API gateways.

Next up: Go error handling patterns that keep your blockchain services robust.

Go Golang concurrency goroutines channels MrBns