» Quick Introduction to Go » 3. Advanced » 3.2 Channels

Channels

Do not communicate by sharing memory; instead, share memory by communicating.

Channels are a powerful concurrency primitive that allows communication and synchronization between goroutines.

You can create a channel using the make function. By default channels are unbuffered, meaning that they will only accept sends (chan <-) if there is a corresponding receive (<- chan) ready to receive the sent value.

ch := make(chan int) // Creates an unbuffered channel of type int

ch2 := make(chan int, 3) // Buffered channel with a capacity of 3

Buffering

Buffered channels accept a limited number of values without a corresponding receiver for those values.

package main

import "fmt"

func main() {

    messages := make(chan string, 2)

    messages <- "lite"
    messages <- "rank"

    fmt.Println(<-messages)
    fmt.Println(<-messages)
}

Synchronization

package main

import (
    "fmt"
    "time"
)

func worker(done chan int) {
    fmt.Print("working...")
    time.Sleep(time.Second)
    fmt.Println("done")

    done <- 1
}

func main() {
    done := make(chan int, 1)
    go worker(done)

    <-done
}

The worker function simulates some work and notifies when it's done by sending a value to the done channel.

Directions

Channels can have directions, which means you can specify whether a channel is meant to only send or only receive values.

This is a form of type-checking at the language level to prevent misuse of channels. Channel directions are declared using the <- operator.

Send-only Channel

package main

import "fmt"

func sendData(sendCh chan<- int) {
    sendCh <- 42
}

func main() {
    ch := make(chan int)
    go sendData(ch)

    fmt.Println("Data sent successfully")
}

Receive-only Channel

package main

import "fmt"

func receiveData(receiveCh <-chan int) {
    data := <-receiveCh
    fmt.Println("Received data:", data)
}

func main() {
    ch := make(chan int)
    go receiveData(ch)

    fmt.Println("Data received successfully")
}

Closing

You can close a channel to signal that no more values will be sent on it. Closing a channel is useful for notifying receivers that they should stop waiting for new values.

package main

import (
	"fmt"
	"time"
)

func produceData(dataCh chan<- int) {
	for i := 1; i <= 5; i++ {
		dataCh <- i
	}
	close(dataCh)
}

func consumeData(dataCh <-chan int, done chan<- struct{}) {
	for {
		// The 'ok' value will be false if the channel is closed.
		data, ok := <-dataCh

		if !ok {
			fmt.Println("Channel closed. No more data.")
			close(done) // Signal that consumption is done
			return
		}

		fmt.Println("Received data:", data)
		time.Sleep(time.Second)
	}
}

func main() {
	dataCh := make(chan int)
	done := make(chan struct{})

	go produceData(dataCh)

	go consumeData(dataCh, done)

	// Block until the 'done' channel is closed, indicating that consumption is complete.
	<-done

	fmt.Println("Main goroutine exiting.")
}

Ranging Over

The for ... range loop is commonly used with channels in Go to iterate over values until the channel is closed.

package main

import (
	"fmt"
	"time"
)

func produceData(dataCh chan<- int) {
	for i := 1; i <= 5; i++ {
		dataCh <- i
	}
	close(dataCh)
}

func consumeData(dataCh <-chan int, done chan<- struct{}) {
	for data := range dataCh {
		fmt.Println("Received data:", data)
		time.Sleep(time.Second) // Simulate some processing
	}

	// Channel is closed, signal that consumption is done
	close(done)
}

func main() {
	dataCh := make(chan int)
	done := make(chan struct{})

	go produceData(dataCh)
	go consumeData(dataCh, done)

	// Block until the 'done' channel is closed, indicating that consumption is complete.
	<-done

	fmt.Println("Main goroutine exiting.")
}

Non-Blocking Operations

You can perform non-blocking operations on channels using the select statement. The select statement allows you to wait on multiple communication operations simultaneously and proceed with the first one that can proceed.

package main

import "fmt"

func main() {
	dataCh := make(chan int, 1)

  dataCh <- 58

	// Try sending a value to the channel without blocking.
	select {
	case dataCh <- 42:
		fmt.Println("Data sent successfully.")
	default:
		fmt.Println("Unable to send data. Channel is full.")
	}
    // => Unable to send data. Channel is full.

	// Try receiving a value from the channel without blocking.
	select {
	case data := <-dataCh:
		fmt.Println("Received data:", data)
	default:
		fmt.Println("No data available. Channel is empty.")
	}
    // => Received data: 58

	close(dataCh)
}

Worker Pools

A worker pool is a common concurrency pattern where a fixed number of worker goroutines are used to process tasks from a queue. Channels are often used for communication between the main goroutine (which produces tasks) and the worker goroutines (which consume and process tasks).

package main

import "fmt"

func worker(id int, jobs <-chan int, results chan<- int, done chan<- struct{}) {
	for job := range jobs {
		fmt.Printf("Worker %d processing job %d\n", id, job)
		results <- job * 2
	}

	// Signal that the worker has finished its work
	done <- struct{}{}
}

func main() {
	const numJobs = 10
	const numWorkers = 3

	// Create channels for communication between main and worker goroutines.
	jobs := make(chan int, numJobs)
	results := make(chan int, numJobs)
	done := make(chan struct{}, numWorkers)

	for i := 1; i <= numWorkers; i++ {
		go worker(i, jobs, results, done)
	}

	// Produce jobs and send them to the jobs channel.
	go func() {
		for i := 1; i <= numJobs; i++ {
			jobs <- i
		}
		close(jobs)
	}()

	// Wait for all workers to finish.
	for i := 1; i <= numWorkers; i++ {
		<-done
	}

	close(results)

	for result := range results {
		fmt.Printf("Result: %d\n", result)
	}
}

Code Challenge

Write a Go program that counts the occurrences of words in a string concurrently. The program should use goroutines to process the input string concurrently and channels to communicate the results.

Loading...
> code result goes here
Prev
Next