Channels

By Syncfusion®, Inc. · Last updated

Go channels: create, send and receive, buffered channels and blocking, close, range and select, timeouts, channel directions, worker pools and done signals.

What Are Channels?

Channel = a pipe that goroutines use to send values to each other.

  • One goroutine sends a value into the channel.
  • Another goroutine receives it from the other end.
  • The channel makes them wait for each other at the right moment.

Go's motto: "Do not communicate by sharing memory. Share memory by communicating."

Simple lesson: a channel is a safe pipe for passing values between goroutines.

A Real-World Example

Think of the answer-sheet tray between the exam hall and the marking room.

  • Invigilators drop answer sheets into the tray.
  • Teachers pick sheets out of the tray to mark.
  • If the tray is empty, a teacher waits.
  • If the tray is full, an invigilator waits.
  • At the end, the office puts up a sign: "No more sheets". That is closing the channel.

Now the mapping:

  • The tray = the channel.
  • Dropping a sheet = ch <- sheet (send).
  • Picking a sheet = sheet := <-ch (receive).
  • The tray's size = the channel's buffer capacity.

You met goroutines in Goroutines.

There, each goroutine wrote to its own slot and main used a WaitGroup. Channels let goroutines hand values to each other instead.

What You Will Learn

In this lesson, you will learn:

  1. How to create channels, and send and receive values.
  2. Buffered channels: capacity and blocking.
  3. How to close a channel and loop over it with range.
  4. How select waits on many channels at once.
  5. Patterns for goroutine communication and data sharing.

Channel Fundamentals

Creating Channels

ch := make(chan string)       // unbuffered channel of strings
nums := make(chan int, 3)     // buffered: holds up to 3 ints
  • chan T = a channel that carries values of type T.
  • Channels are made with make. The zero value of a channel is nil.

Sending and Receiving Data

ActionCode
Sendch <- value
Receive into a variablev := <-ch
Receive and throw away<-ch
  • The arrow shows which way the value moves.

Send from a goroutine, receive in main

package main

import "fmt"

func main() {
	results := make(chan string)
	go func() {
		results <- "Asha scored 91"
	}()
	msg := <-results
	fmt.Println(msg)
}
Asha scored 91
  • main waits at <-results until the goroutine sends.
  • No WaitGroup is needed: receiving the value is the waiting.

Unbuffered Channels Hand Over Directly

Unbuffered channel = a channel with no storage. A send waits until a receiver takes the value.

Try sending and receiving. Switch between unbuffered and buffered.

Watch for BLOCKED:

  • Unbuffered: a send waits for a receiver, and a receive waits for a sender.
  • Buffered: a send waits only when the buffer is full.

Simple lesson: ch <- v sends, <-ch receives, and both wait until the other side is ready.

Buffered Channels

Capacity

Buffered channel = a channel with room for some values: make(chan T, n).

A buffer of 3

package main

import "fmt"

func main() {
	tray := make(chan string, 3)
	tray <- "sheet 1"
	tray <- "sheet 2"
	fmt.Println(len(tray), cap(tray))
	fmt.Println(<-tray)
	fmt.Println(<-tray)
	fmt.Println(len(tray))
}
2 3
sheet 1
sheet 2
0
  • cap(ch) = how many values fit.
  • len(ch) = how many are waiting right now.
  • Values come out in the order they went in: first in, first out.

Blocking Behavior

Block = wait at that line until something happens.

ChannelSend blocks whenReceive blocks when
Unbufferedno receiver is readyno sender is ready
Bufferedthe buffer is fullthe buffer is empty

Deadlock

Deadlock = every goroutine is waiting, and none can ever continue.

func main() {
	ch := make(chan int)
	ch <- 1          // waits for a receiver... that never comes
	fmt.Println(<-ch)
}
// fatal error: all goroutines are asleep - deadlock!
  • The send waits forever, because the receive is on the next line of the same goroutine.
  • The real go tool stops with the message above.
  • In this lab, a deadlocked program is stopped after a few seconds instead.

Simple lesson: a buffered channel lets a sender run ahead until the buffer is full.

Channel Operations

close()

close(ch) = say "no more values will be sent".

  • Only the sender should close a channel.
  • Receiving from a closed, empty channel gives the zero value at once.
  • Sending on a closed channel panics.

Receiving with ok

Is the channel closed?

package main

import "fmt"

func main() {
	ch := make(chan int, 2)
	ch <- 91
	close(ch)
	v, ok := <-ch
	fmt.Println(v, ok)
	v, ok = <-ch
	fmt.Println(v, ok)
}
91 true
0 false
  • ok is false when the channel is closed and empty.

range

for v := range ch receives until the channel is closed and empty. See Loops.

Press Play to watch a producer and a consumer.

Produce, close, and range

  • Without close, the range would wait forever after the last sheet: a deadlock.

select

select = wait on several channel operations, and run whichever is ready first.

Whichever answers first

package main

import (
	"fmt"
	"time"
)

func main() {
	library := make(chan string)
	canteen := make(chan string)
	go func() {
		time.Sleep(30 * time.Millisecond)
		library <- "book is ready"
	}()
	go func() {
		time.Sleep(10 * time.Millisecond)
		canteen <- "lunch is ready"
	}()
	for i := 0; i < 2; i++ {
		select {
		case m := <-library:
			fmt.Println("library:", m)
		case m := <-canteen:
			fmt.Println("canteen:", m)
		}
	}
}
canteen: lunch is ready
library: book is ready
  • Each select waits until one case can go ahead, then runs it.
  • The canteen answers first, so its case runs first.

Timeouts with select

Do not wait forever

package main

import (
	"fmt"
	"time"
)

func main() {
	reply := make(chan string)
	go func() {
		time.Sleep(200 * time.Millisecond)
		reply <- "too late"
	}()
	select {
	case r := <-reply:
		fmt.Println(r)
	case <-time.After(50 * time.Millisecond):
		fmt.Println("timed out after 50ms")
	}
}
timed out after 50ms
  • time.After(d) gives a channel that receives a value after d.
  • A default: case runs at once if no other case is ready, so select never waits.

Channel operations at a glance

Simple lesson: close ends a stream, range reads it to the end, and select waits on many channels at once.

Channel Communication Patterns

Goroutine Communication

Worker pool = a few goroutines that take jobs from one channel and put results on another.

Three workers mark ten papers

package main

import (
	"fmt"
	"sort"
)

type result struct {
	roll  int
	marks int
}

func worker(jobs <-chan int, results chan<- result) {
	for roll := range jobs {
		results <- result{roll, 50 + roll*4}
	}
}

func main() {
	jobs := make(chan int, 10)
	results := make(chan result, 10)

	for w := 1; w <= 3; w++ {
		go worker(jobs, results)
	}
	for roll := 1; roll <= 10; roll++ {
		jobs <- roll
	}
	close(jobs)

	var all []result
	for i := 0; i < 10; i++ {
		all = append(all, <-results)
	}
	sort.Slice(all, func(i, j int) bool { return all[i].roll < all[j].roll })
	fmt.Println(all[0], all[9], len(all))
}
{1 54} {10 90} 10
  • <-chan int = a receive-only channel. chan<- result = a send-only channel.
  • These direction types stop a worker from using a channel the wrong way.
  • Results arrive in any order, so they are sorted before printing.

Data Sharing

  • Pass ownership: when a value is sent, the sender should stop using it. The receiver now owns it.
  • One writer: let one goroutine own a piece of data, and let others send it requests through a channel.
  • Signal "done": a chan struct{} with close(done) tells many goroutines to stop at once.

A done channel

package main

import "fmt"

func main() {
	done := make(chan struct{})
	finished := make(chan string)
	go func() {
		<-done
		finished <- "cleaner stopped"
	}()
	close(done)
	fmt.Println(<-finished)
}
cleaner stopped
  • struct{} carries no data. The signal is the close itself.
  • Every goroutine waiting on <-done wakes up when it is closed.

Common channel traps:

  • Sending on a closed channel panics.
  • Closing a channel twice panics.
  • Receiving from a nil channel waits forever.

Easy way to remember

  • ch <- v = put in. <-ch = take out.
  • Unbuffered = hand to hand. Buffered = a tray of size n.
  • The sender closes. The receiver ranges.

Simple lesson: channels connect goroutines: jobs go in, results come out, and closing a channel signals the end.

TL;DR

  1. Channel = make(chan T) or make(chan T, n). A pipe between goroutines.
  2. Send = ch <- v. Receive = v := <-ch.
  3. Unbuffered = sender and receiver meet. Buffered = room for n values.

    • Blocking = waiting until the other side is ready.
    • Deadlock = everyone waits forever.
  4. close(ch) = no more values. v, ok := <-ch → ok is false when closed and empty.
  5. range = for v := range ch until closed.
  6. select = wait on many channels. time.After for timeouts.
  7. Directions = chan<- T send-only, <-chan T receive-only.
  8. Patterns = worker pool, pass ownership, done channel.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Send and receive in the same goroutine on an unbuffered channelIt hands itself the valueAnother goroutine must receiveDeadlock.
Forgetting close before rangerange stops by itselfIt waits until the channel is closedDeadlock at the end.
The receiver closes the channelAnyone can closeOnly the sender closessend on closed channel panic.
Closing twiceIt is harmlessIt panicsclose of closed channel
select with no timeout on a slow channelIt always answersIt may wait foreverA stuck program. Add time.After.
Expecting worker results in orderThey come in send orderAny orderSort or index the results.

Check Yourself

Exercises

What to Remember

  • A channel passes values between goroutines: ch <- v, v := <-ch.
  • Unbuffered channels hand over directly. Buffered ones hold up to n values.
  • The sender closes. range reads until closed.
  • select waits on many channels, and time.After adds a timeout.
  • Worker pools: jobs in, results out, and sort results if order matters.

Next: Synchronization. You will learn how to protect shared data with WaitGroups, mutexes and atomic operations.