Table of contents

  1. Channels

I. Channels

Channels are typed conduits through which you can send and receive values using the channel operator ← .

1.1. Creating and Using Channels

Basic Channel Operations

package main

import "fmt"

func main() {
    // Create a channel of strings
    messages := make(chan string)

    // Send a value in a goroutine
    go func() {
        messages <- "ping"
    }()

    // Receive the value in main
    msg := <-messages
    fmt.Println(msg) // "ping"
}

Key Points:

  • Channels must be created before use with make(chan Type)
  • Sends and receives block by default
  • Blocking ensures synchronization between goroutines

1.2. Channel Direction

Channels can be derection when used as function parameters:

// Send only channel
func send(ch chan<- string) {
    ch <- "hello"
}

// Receive only channel
func receive(ch <-chan string){
    msg := <-ch
    fmt.Println(msg)
}

1.3. Buffered Channels

By default, channels are unbuffered - they only accept sends when there’s a corresponding receive ready. Buffered channels accept a limited number of values without a receiver:

func main() {
    // Create a buffered channel with capacity 2
    ch := make(chan int, 2)

    // These sends won't block
    ch <- 1
    ch <- 2

    // This would block: ch <- 3

    // Receive values
    fmt.Println(<-ch) // 1
    fmt.Println(<-ch) // 2
}
AspectUnbufferedBuffered
Creationmake(chan T)make(chan T, n)
Send blocksUntil receiver readyWhen buffer full
Receive blocksUntil sender readyWhen buffer empty
SynchronizationGuaranteedLooser coupling
Use caseSynchronizationPerformance, decoupling

1.4. Rob Pike’s Channel Patterns

1.4.1. The Generator Pattern

From Rob Pike’s talks, a generator is a function that returns a channel:

func boring(msg string) <-chan string {
    c := make(chan string)
    go func() {
        for i := 0; ; i++ {
            c <- fmt.Sprintf("%s %d", msg, i)
            time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
        }
    }()
    return c
}

func main() {
    c := boring("boring!")
    for i := 0; i < 5; i++ {
        fmt.Printf("You say: %q\n", <-c)
    }
    fmt.Println("You're boring; I'm leaving.")
}

1.4.2. Channel as a Service

type Request struct {
    args        []int
    resultChan  chan int
}

func sum(a []int) int {
    s := 0
    for _, v := range a {
        s += v
    }
    return s
}

func server(requests chan *Request) {
    for req := range requests {
        go func(req *Request) {
            req.resultChan <- sum(req.args)
        }(req)
    }
}

func main() {
    requests := make(chan *Request)
    go server(requests)

    req := &Request {
        args: []int{1,2,3},
        resultChan: make(chan int),
    }
    requests <- req
    result := <- req.resultChan
    fmt.Println("Sum: ", result)
}

This demonstrates how channels can represent as a service, where:

  • Request struct: Represents as a service request with:

    • args []int: Input data for service
    • resultChan chan int: Private channel for receiving the result
  • server() function:

    • Listen on a requests channel for incoming request
    • Spawns a goroutine for each request
    • Send result back through each request’s private result channel

Some key benefits:

  • Service implement is hidden behind channel interface
  • Concurrently process multiple requests
  • Client gets result exactly when it ready
  • Data of each request is isolated

1.4.3. Closing Channel

A sender can close a channel to indicate no more values will be sent:

func main() {
    jobs := make(chan int, 5)
    done := make(chan bool)

    go func() {
        for {
            j, more := <-jobs
            if !more {
                fmt.Println("received all jobs")
                done <- true
                return
            }
            fmt.Println("received job", j)
        }
    }()

    for j := 1; j <= 3; j++ {
        jobs <- j
        fmt.Println("sent job", j)
    }
    close(jobs)
    fmt.Println("sent all jobs")

    <-done
}

Rules for Closing:

  • Only the sender should close a channel
  • Sending on a closed channel causes panic
  • Receiving from a closed channel returns zero value
  • Use v, ok := <-ch to check if channel is closed

1.4.4. Range Over Channels

You can use range to receive values until a channel is closed:

func main() {
    queue := make(chan string, 2)
    queue <- "one"
    queue <- "two"
    close(queue)

    for elem := range queue {
        fmt.Println(elem)
    }
}

1.5. Common Pitfalls

1.5.1. Deadlock

// Deadlock - all goroutines blocked
func main() {
    ch := make(chan int)
    ch <- 42  // Blocks forever, no receiver
    fmt.Println(<-ch)
}

Explain:

  • ch := make(chan int) create an unbuffered int channel
  • ch <- 42 this code attempt to send value (42), but block because there is no receiver ready
  • fmt.Println(<-ch) This line would receive the value 42, but it is never reached because promgram stuck on the previous line
  • That’s happend because all of the logic (unbuffered channel, send, receive) running on only one goroutine

→ To fix this problem, we can use other goroutine, or buffered channel

1.5.2. Goroutine leak

// Leak - goroutine runs forever
func leak() {
    ch := make(chan int)
    go func() {
        val := <-ch  // Blocks forever if ch never receives
        fmt.Println(val)
    }()
    // Function returns, goroutine still running
}

Explain: Even this function return, the goroutine still running because nothing ever send to the channel.

1.5.3. Race on Channel Variable

// Race condition
var ch chan int

func main() {
    go func() {
        ch = make(chan int)  // Goroutine 1, Race!
    }()
    go func() {
        ch <- 42  // Goroutine 2, Race!
    }()
}

Explain: After initial nil channel ch on main(), 2 concurrent goroutine try to access this channel (goroutine 1 tries to create channel, goroutine 2 tries to send value 42 to channel, which leads to an undefined behavior due ti concurent read/write of the same memory location).