Concurrency with Goroutines and Channels

Introduction to Goroutines and Channels for achieving concurrency in Go programs


Working with Channels: Introduction

Channels are a fundamental concurrency primitive in Go, allowing goroutines to communicate and synchronize with each other. They provide a safe and elegant way to pass data between concurrently executing functions, preventing race conditions and other common concurrency problems. This document provides an introduction to Go channels, covering their purpose, declaration, initialization, and basic send/receive operations.

Introduction to Channels

Purpose of Channels

Channels serve several key purposes in concurrent Go programs:

  • Communication: They enable goroutines to exchange data with each other.
  • Synchronization: They allow goroutines to wait for specific events or data from other goroutines.
  • Data Sharing: They provide a controlled way to share data between goroutines, preventing data corruption and race conditions.

Declaring and Initializing Channels

Channels are declared using the chan keyword, followed by the type of data that the channel will carry. They must be initialized using the make function before they can be used.

Declaration

 var ch chan int  // Declares a channel that can carry integers
var strChan chan string // Declares a channel that can carry strings 

Initialization

 ch := make(chan int) // Creates an unbuffered channel of integers
strChan := make(chan string) // Creates an unbuffered channel of strings

//Buffered channels
bufferedChan := make(chan int, 10)  //creates a buffered channel of int with a capacity of 10 

Unbuffered Channels: Unbuffered channels (created with make(chan T)) require both a sender and a receiver to be ready at the same time for the communication to happen. The send operation blocks until a receiver is ready, and the receive operation blocks until a sender is ready. This is synchronous communication. Buffered Channels: Buffered channels (created with make(chan T, capacity)) can hold a certain number of values without requiring an immediate receiver. The sender can send values to the channel as long as there is space available in the buffer. Once the buffer is full, the sender blocks until a receiver retrieves a value. This provides some degree of asynchronicity.

Basic Send/Receive Operations

Channels use the <- operator for sending and receiving data.

Send Operation

 ch <- 10  // Sends the integer value 10 to the channel ch
strChan <- "Hello" //Sends the string "Hello" to channel strChan 

The send operation ch <- value blocks until another goroutine is ready to receive the value from the channel, unless the channel is buffered and has available space.

Receive Operation

 value := <-ch // Receives a value from the channel ch and assigns it to the variable value
message := <-strChan //Receives a value from the channel strChan and assigns it to the variable message 

The receive operation <-ch blocks until another goroutine sends a value to the channel, unless the channel is closed.

Send and Receive Example

 package main

import "fmt"

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

    go func() {
        ch <- 10 // Send 10 to the channel
        fmt.Println("Sent 10 to channel")
    }()

    value := <-ch // Receive from the channel
    fmt.Println("Received:", value)
} 

In this example, a goroutine is launched that sends the integer 10 to the channel ch. The main goroutine then receives the value from the channel and prints it to the console. Because the channel is unbuffered, the send operation in the goroutine blocks until the main goroutine is ready to receive.

Closing Channels

Channels can be closed using the close function. Closing a channel signals that no more values will be sent on it.

 close(ch) 

It is important to note that only the sender should close a channel, never the receiver. Receiving from a closed channel returns the zero value of the channel's type without blocking, along with a second boolean value indicating whether the channel is open or closed.

 value, ok := <-ch
if !ok {
    // Channel is closed
    fmt.Println("Channel is closed")
} else {
    fmt.Println("Received:", value)
} 

Closing channels is particularly useful when using range to iterate over a channel. The loop terminates automatically when the channel is closed.

 package main

import "fmt"

func main() {
    ch := make(chan int, 5) // Buffered channel

    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch) // Crucial: signal that no more values will be sent

    for value := range ch {
        fmt.Println("Received:", value)
    }

    fmt.Println("Done")
}