Loading...
Loading...

Go Channels Tutorial

Channels are the communication pipes that connect concurrent goroutines in Go. Unlike shared memory models that require locks, channels enable goroutines to synchronize and exchange data safely through communication.

1. Channel Fundamentals

What Are Channels?

Channels are typed conduits that allow you to send and receive values between goroutines. They enforce synchronization by design - a send operation blocks until there's a receiver ready, and vice versa.

// Basic channel example
messageChan := make(chan string) // Create unbuffered string channel

go func() {
    messageChan <- "Hello" // Send blocks until receiver is ready
}()

msg := <-messageChan // Receive blocks until sender is ready
fmt.Println(msg) // Prints: Hello

Channel Characteristics

  • Thread-safe: No additional locking needed
  • Synchronized: Send/receive operations coordinate goroutines
  • Typed: Only declared data types can pass through
  • First-in-first-out: Maintains order of sent values

2. Channel Types

Unbuffered Channels

Unbuffered channels (capacity=0) require both sender and receiver to be ready at the same instant. They provide strong synchronization guarantees.

func main() {
    done := make(chan bool) // Unbuffered
    
    go func() {
        fmt.Println("Working...")
        time.Sleep(time.Second)
        done <- true // Blocks until main() receives
    }()
    
    <-done // Blocks until goroutine sends
    fmt.Println("Finished")
}

Buffered Channels

Buffered channels (capacity>0) allow sends to complete without blocking until the buffer is full. They're useful when producers and consumers operate at different speeds.

func main() {
    ch := make(chan int, 3) // Buffered (holds 3 values)
    
    ch <- 1 // Doesn't block
    ch <- 2
    ch <- 3
    
    fmt.Println(<-ch) // 1 (FIFO order)
    ch <- 4 // Now allowed since space freed
}

Directional Channels

Channel directions constrain usage to prevent misuse in function signatures:

// Producer can only send to channel
func producer(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}

// Consumer can only receive from channel
func consumer(ch <-chan int) {
    for num := range ch {
        fmt.Println(num)
    }
}

3. Channel Operations

Sending and Receiving

The <- operator handles both sending and receiving. Operations block until the complementary operation is ready.

ch := make(chan int)

// Send operation (blocks if unbuffered and no receiver)
go func() { ch <- 42 }()

// Receive operation (blocks until value available)
value := <-ch
fmt.Println(value)

Closing Channels

Closing indicates no more values will be sent. Receivers can detect closure using the comma-ok idiom.

ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch) // Closes channel

// Safe receive check
if val, ok := <-ch; ok {
    fmt.Println(val)
} else {
    fmt.Println("Channel closed")
}

Range Over Channels

The range loop automatically exits when a channel is closed.

func producer(ch chan int) {
    for i := 0; i < 3; i++ {
        ch <- i
    }
    close(ch)
}

func main() {
    ch := make(chan int)
    go producer(ch)
    
    // Automatically stops at close
    for v := range ch {
        fmt.Println(v) // 0, 1, 2
    }
}

4. Select Statement

Basic Select

The select statement allows a goroutine to wait on multiple channel operations simultaneously.

ch1 := make(chan string)
ch2 := make(chan string)

go func() { ch1 <- "one" }()
go func() { ch2 <- "two" }()

for i := 0; i < 2; i++ {
    select {
    case msg1 := <-ch1:
        fmt.Println(msg1)
    case msg2 := <-ch2:
        fmt.Println(msg2)
    }
}

Default Case

A default case makes select non-blocking.

select {
case msg := <-messages:
    fmt.Println(msg)
default:
    fmt.Println("No message ready")
}

Timeout Pattern

Select can implement timeouts using time.After.

select {
case res := <-longOperation():
    fmt.Println(res)
case <-time.After(1 * time.Second):
    fmt.Println("Operation timed out")
}

5. Channel Patterns

Worker Pool

Distribute work across multiple goroutines using channels.

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        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 jobs
    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)
    
    // Collect results
    for a := 1; a <= 5; a++ {
        <-results
    }
}

Fan-out/Fan-in

Parallelize work across multiple goroutines and consolidate results.

func producer(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 merge(cs ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    out := make(chan int)
    
    // Start output goroutine for each input channel
    for _, c := range cs {
        wg.Add(1)
        go func(c <-chan int) {
            for n := range c {
                out <- n
            }
            wg.Done()
        }(c)
    }
    
    // Close out after all inputs are done
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

6. Common Pitfalls

Deadlocks

Occur when goroutines wait indefinitely for each other.

// Deadlock: No receiver for the send
func main() {
    ch := make(chan int)
    ch <- 42 // Blocks forever
    fmt.Println(<-ch)
}

Premature Closing

Sending to a closed channel causes a panic.

ch := make(chan int)
close(ch)
ch <- 1 // panic: send on closed channel

Leaking Goroutines

Blocked goroutines that never terminate can leak resources.

func leak() {
    ch := make(chan int)
    go func() {
        <-ch // Blocks forever
    }()
    // Returns without sending to ch
}

7. Best Practices

Ownership Principles

  • Channel owners should close channels
  • Never close channels from receivers
  • Document channel ownership clearly

Error Handling

Use dedicated error channels in concurrent pipelines.

func worker(jobs <-chan int, results chan<- int, errs chan<- error) {
    for j := range jobs {
        if j < 0 {
            errs <- fmt.Errorf("invalid job %d", j)
            continue
        }
        results <- j * 2
    }
}

When to Use Channels vs Mutexes

Use Channels When Use Mutexes When
Transferring ownership of data Protecting internal state
Coordinating goroutines Simple shared memory access
Implementing pipelines Performance-critical sections
0 Interaction
0 Views
Views
0 Likes
×
×
×
🍪 CookieConsent@Ptutorials:~

Welcome to Ptutorials

$ Allow cookies on this site ? (y/n)

top-home