Concurrency in Golang

Goroutines

Goroutines allow functions to run concurrently and consume significantly less memory compared to traditional threads. Every Go program begins execution with a primary Goroutine, commonly referred to as the main Goroutine. If the main Goroutine exits, all other active Goroutines are terminated immediately.

Syntax:

func functionName() {
    // statements
}

// To execute as a Goroutine
go functionName()

Example:

package main

import "fmt"

func showMessage(msg string) {
    for i := 0; i < 3; i++ {
        fmt.Println(msg)
    }
}

func main() {
    go showMessage("Hello, Concurrent World!") // Executes concurrently
    showMessage("Hello from Main!")
}
Creating a Goroutine

To initiate a Goroutine, simply use the go keyword as a prefix when calling a function or method.

Syntax:

func functionName() {
    // statements
}

// Using `go` keyword to execute the function as a Goroutine
go functionName()

Example:

package main
import "fmt"

func printMessage(message string) {
    for i := 0; i < 3; i++ {
        fmt.Println(message)
    }
}

func main() {
    go printMessage("Welcome to Goroutines!") // Executes concurrently
    printMessage("Running in Main!")
}

Output:

Running in Main!
Running in Main!
Running in Main!
Running Goroutines with Delay

Incorporating time.Sleep() allows sufficient time for both the main and additional Goroutines to execute completely.

Example:

package main
import (
    "fmt"
    "time"
)

func printMessage(msg string) {
    for i := 0; i < 3; i++ {
        time.Sleep(300 * time.Millisecond)
        fmt.Println(msg)
    }
}

func main() {
    go printMessage("Executing in Goroutine!")
    printMessage("Executing in Main!")
}

Output:

Executing in Main!
Executing in Goroutine!
Executing in Goroutine!
Executing in Main!
Executing in Goroutine!
Executing in Main!
Anonymous Goroutines

You can also run anonymous functions as Goroutines by appending the go keyword before the function.

Syntax

go func(parameters) {
    // function body
}(arguments)

Example:

package main
import (
    "fmt"
    "time"
)

func main() {
    go func(msg string) {
        for i := 0; i < 3; i++ {
            fmt.Println(msg)
            time.Sleep(400 * time.Millisecond)
        }
    }("Anonymous Goroutine Execution!")

    time.Sleep(1.5 * time.Second) // Wait for Goroutine to complete
    fmt.Println("Main Goroutine Ends.")
}

Output:

Anonymous Goroutine Execution!
Anonymous Goroutine Execution!
Anonymous Goroutine Execution!
Main Goroutine Ends.

Select Statement

In Go, the select statement allows you to wait for multiple channel operations to complete, such as sending or receiving values. Similar to a switch statement, select lets you proceed with the first available case, making it ideal for managing concurrent operations and handling asynchronous tasks effectively.

Example

Imagine you have two tasks that finish at different times. You can use select to receive data from whichever task completes first.

package main

import (
    "fmt"
    "time"
)

func task1(ch chan string) {
    time.Sleep(2 * time.Second)
    ch <- "Task 1 finished"
}

func task2(ch chan string) {
    time.Sleep(4 * time.Second)
    ch <- "Task 2 finished"
}

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go task1(ch1)
    go task2(ch2)

    select {
    case msg1 := <-ch1:
        fmt.Println(msg1)
    case msg2 := <-ch2:
        fmt.Println(msg2)
    }
}

Output:

Task 1 finished

In this example, “Task 1 finished” will be printed after 2 seconds, as task1 finishes before task2. If task2 had completed first, the output would have been “Task 2 finished.”

Syntax

The select statement in Go listens to multiple channel operations and proceeds with the first ready case.

select {
    case value := <-channel1:
        // Executes if channel1 is ready to send/receive
    case channel2 <- value:
        // Executes if channel2 is ready to send/receive
    default:
        // Executes if no other case is ready
}

Key Points:

  • The select statement waits until at least one channel operation is ready.
  • If multiple channels are ready, one is selected at random.
  • The default case is executed if no other case is ready, preventing the program from blocking.
Select Statement Variations

 Basic Blocking Behavior: In this variation, we modify the example to remove the select statement and see the blocking behavior when no channels are ready.

package main

import (
    "fmt"
)

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

    select {
    case msg := <-ch:
        fmt.Println(msg)
    default:
        fmt.Println("No channels are ready")
    }
}

Output:

No channels are ready

Handling Multiple Cases: If multiple tasks are ready at the same time, select chooses one case randomly. This can occur if tasks have nearly the same completion times.

Example:

package main

import (
    "fmt"
    "time"
)

func portal1(channel1 chan string) {
    time.Sleep(3 * time.Second)
    channel1 <- "Welcome from portal 1"
}

func portal2(channel2 chan string) {
    time.Sleep(9 * time.Second)
    channel2 <- "Welcome from portal 2"
}

func main() {
    R1 := make(chan string)
    R2 := make(chan string)

    go portal1(R1)
    go portal2(R2)

    select {
    case op1 := <-R1:
        fmt.Println(op1)
    case op2 := <-R2:
        fmt.Println(op2)
    }
}

Output:

Welcome from portal 1

Using Select with Default Case to Avoid Blocking: The default case can be used to avoid blocking when no cases are ready. Here’s an example of modifying the structure to include the default case.

package main

import (
    "fmt"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    select {
    case msg1 := <-ch1:
        fmt.Println(msg1)
    case msg2 := <-ch2:
        fmt.Println(msg2)
    default:
        fmt.Println("No tasks are ready yet")
    }
}

Output:

No tasks are ready yet

Infinite Blocking without Cases: A select statement with no cases will block indefinitely. This is commonly used when you need an infinite wait.

package main

func main() {
    select {} // This blocks indefinitely because no cases are present
}

Output:

Welcome to Go Programming
Channels in Go Language

A channel in Go is a medium that enables communication between goroutines without using explicit locks. Channels facilitate the exchange of data between goroutines in a synchronized manner, and by default, they are bidirectional. This means the same channel can be used for both sending and receiving data. Below is a detailed explanation and examples of how channels work in Go.

Creating a Channel; In Go, you use the chan keyword to create a channel. A channel can only transport data of a specific type, and you cannot use the same channel to transfer different data types.

Syntax:

var channelName chan Type

Example:

// Go program to demonstrate channel creation
package main

import "fmt"

func main() {
    // Creating a channel using var
    var myChannel chan string
    fmt.Println("Channel value:", myChannel)
    fmt.Printf("Channel type: %T\n", myChannel)

    // Creating a channel using make()
    anotherChannel := make(chan string)
    fmt.Println("Another channel value:", anotherChannel)
    fmt.Printf("Another channel type: %T\n", anotherChannel)
}

Output:

Channel value: <nil>
Channel type: chan string
Another channel value: 0xc00007c060
Another channel type: chan string
Sending and Receiving Data in a Channel

Channels in Go operate through two primary actions: sending and receiving, collectively referred to as communication. These operations use the <- operator to indicate the direction of the data flow.

1. Sending Data: The send operation transfers data from one goroutine to another via a channel. For basic data types like integers, floats, and strings, sending is straightforward and safe. However, when working with pointers or references (like slices or maps), ensure that only one goroutine accesses them at a time.

myChannel <- value

2. Receiving Data: The receive operation fetches the data from a channel that was sent by another goroutin

var channelName chan Type

Example:

// Go program to demonstrate send and receive operations
package main

import "fmt"

func calculateSquare(ch chan int) {
    num := <-ch
    fmt.Println("Square of the number is:", num*num)
}

func main() {
    fmt.Println("Main function starts")

    // Creating a channel
    ch := make(chan int)

    go calculateSquare(ch)

    ch <- 12

    fmt.Println("Main function ends")
}

Output:

Main function starts
Square of the number is: 144
Main function ends

Closing a Channel: You can close a channel using the close() function, which indicates that no more data will be sent to that channel.

Syntax:

close(channelName)

When iterating over a channel using a for range loop, the receiver can determine if the channel is open or closed.

Syntax:

value, ok := <-channelName

If ok is true, the channel is open, and you can read data. If ok is false, the channel is closed.

Example:

// Go program to close a channel using for-range loop
package main

import "fmt"

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

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

    go sendData(ch)

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

Output:

Received value: 1
Received value: 2
Received value: 3
Received value: 4
Received value: 5
Channel closed
Key Points to Remember
  1. Blocking Behavior:

    • Sending data blocks until another goroutine is ready to receive it.

    • Receiving data blocks until another goroutine sends it.

  2. Nil Channels:

    • A zero-value channel is nil, and operations on it will block indefinitely.

  3. Iterating Over Channels:

    • A for range loop can iterate over the values in a channel until it’s closed.

Example:

// Go program to iterate over channel using for-range
package main

import "fmt"

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

    go func() {
        ch <- "Hello"
        ch <- "World"
        ch <- "Go"
        close(ch)
    }()

    for msg := range ch {
        fmt.Println(msg)
    }
}

Output:

Hello
World
Go
Channel Properties

1.Length of a Channel: Use len() to find the number of elements currently in the channel.

Example:

// Go program to find channel length
package main

import "fmt"

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

    ch <- 10
    ch <- 20

    fmt.Println("Channel length:", len(ch))
}

Output:

Channel length: 2

2. Capacity of a Channel: Use cap() to find the total capacity of the channel.

Example:

// Go program to find channel capacity
package main

import "fmt"

func main() {
    ch := make(chan float64, 4)

    ch <- 1.1
    ch <- 2.2

    fmt.Println("Channel capacity:", cap(ch))
}

Output:

Channel capacity: 4

In Golang, channels act as a communication mechanism between concurrently running goroutines, allowing them to transmit and receive data. By default, channels in Go are bidirectional, meaning they support both sending and receiving operations. However, it’s possible to create unidirectional channels, which can either exclusively send or receive data. These unidirectional channels can be constructed using the make() function as demonstrated below:

// For receiving data only
c1 := make(<-chan bool)

// For sending data only
c2 := make(chan<- bool)

Example:

// Go program to demonstrate the concept
// of unidirectional channels
package main

import "fmt"

func main() {
    // Channel restricted to receiving data
    recvOnly := make(<-chan int)

    // Channel restricted to sending data
    sendOnly := make(chan<- int)

    // Display the types of the channels
    fmt.Printf("%T", recvOnly)
    fmt.Printf("\n%T", sendOnly)
}

Output:

<-chan int
chan<- int
Converting Bidirectional Channels into Unidirectional Channels

In Go, you can convert a bidirectional channel into a unidirectional channel, meaning you can restrict it to either sending or receiving data. However, the reverse conversion (from unidirectional back to bidirectional) is not possible. This concept is illustrated in the following example:

Example:

// Go program to illustrate conversion
// of a bidirectional channel into a
// unidirectional channel
package main

import "fmt"

// Function to send data through a send-only channel
func sendData(channel chan<- string) {
    channel <- "Hello from Golang"
}

func main() {
    // Creating a bidirectional channel
    bidiChannel := make(chan string)

    // Passing the bidirectional channel to a function,
    // which restricts it to a send-only channel
    go sendData(bidiChannel)

    // Receiving data from the channel
    fmt.Println(<-bidiChannel)
}

Output:

Hello from Golang