Concurrency ใน Go ตอนที่ 2: Channel

Published: (April 30, 2026 at 09:31 PM EDT)
3 min read
Source: Dev.to

Source: Dev.to

Channel

“Do not communicate by sharing memory; instead, share memory by communicating.”
In Go, goroutines usually exchange data through channels rather than mutating shared variables.

  • Sending a value: c <- v
  • Receiving a value: v := <-c

c is a typed channel; its element type is fixed when the channel is created.

func multiply(v1, v2 int, c chan int) {
    c <- v1 * v2
}

func main() {
    c := make(chan int)
    go multiply(1, 2, c)
    go multiply(3, 4, c)

    a := <-c
    b := <-c
    fmt.Println(a * b) // prints 24
}

The channel’s built‑in blocking semantics make an explicit WaitGroup unnecessary: a send blocks until a receiver is ready, and a receive blocks until a value is available.

Using a channel for simple synchronization

A channel can act as a one‑off signal, similar to a WaitGroup.

ready := make(chan bool)

go func() {
    // do some work...
    ready <- true // signal completion
}()

<-ready // wait until the goroutine signals

The received value is often ignored; the program merely waits for the send to occur.

Receiving a stream with for range

When a producer closes a channel, the receiver can iterate until the channel is closed.

func producer(c chan int) {
    for i := 1; i <= 5; i++ {
        fmt.Println("Sending", i)
        c <- i
        time.Sleep(500 * time.Millisecond)
    }
    close(c)
}

func main() {
    c := make(chan int)
    go producer(c)

    for v := range c {
        fmt.Print("Receiving ", v)
    }
    fmt.Println("\nDone")
}

Important: The sender should close the channel when no more values will be sent. Closing from the receiver side can cause a panic if the sender is still active.

Buffered vs Unbuffered Channels

By default a channel has capacity 0 (unbuffered) and synchronizes each send with a corresponding receive.

A buffered channel allows a limited number of values to be queued without blocking the sender:

c := make(chan int, 3) // capacity 3
  • The first three sends succeed immediately.
  • The fourth send blocks until a receiver reads a value.
  • Receives block only when the buffer is empty.

Buffered channels are useful when producers and consumers run at different speeds, smoothing temporary mismatches.

Directional Channels

When a channel is passed as a function parameter, its direction can be restricted:

  • Send‑only: func producer(c chan<- int)
  • Receive‑only: func consumer(c <-chan int)

This makes the intended use explicit and helps the compiler catch misuse.

Select Statement

select works like a switch for channel operations. It blocks until one of its cases can proceed.

Basic usage

func main() {
    c1 := make(chan string)
    c2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        c1 <- "A"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        c2 <- "B"
    }()

    select {
    case res := <-c1:
        fmt.Println("Received from c1:", res)
    case res := <-c2:
        fmt.Println("Received from c2:", res)
    }
}

The program prints the value from the channel that becomes ready first (c1 in this example).

Selecting between send and receive

select {
case a := <-inChan:
    fmt.Println("Received:", a)
case outChan <- b:
    fmt.Println("Sent:", b)
}

Common patterns

Timeout

select {
case res := <-c1:
    fmt.Println("Received:", res)
case <-time.After(5 * time.Second):
    fmt.Println("Timeout")
}

Non‑blocking default

select {
case res := <-c1:
    fmt.Println("Received:", res)
default:
    fmt.Println("No message")
}

Abort signal

select {
case res := <-c1:
    fmt.Println("Received:", res)
case <-abortChan:
    fmt.Println("Abort")
    return
}
0 views
Back to Blog

Related posts

Read more »

Go do zero: var vs :=

Go tem duas formas de declarar variáveis: var e :=. Elas existem por motivos diferentes e têm regras distintas. Saber quando usar cada uma evita erros bobos e c...