Go Channels

Summary: in this tutorial, you will learn how to use Go channels to allow goroutines to communicate with each other.

Introduction to Go channels

In Go, channels are used to pass data between goroutines. In other words, goroutines share data by communicating with each other via channels.

Creating channels

To create a channel, you use the var keyword, followed by the channel name, the chan keyword, and the data type of the channel:

var channelName chan typeCode language: Go (go)

For example:

var ch chan stringCode language: Go (go)

In this example, we define a channel ch with the type string. The ch channel allows goroutines to send and receive strings only.

Alternatively, you can use the built-in make() function to create a channel:

ch := make(chan string)Code language: Go (go)

Sending data to channels

To send data to a channel, you use the <- operator:

channel <- dataCode language: Go (go)

For example, the following shows how to send the message "Hi" to the channel ch:

ch <- "Hi"Code language: Go (go)

If you attempt to send a value that is not a string to the ch channel, you’ll encounter a compilation error.

Receiving data from channels

To receive data from a channel, you use the <- operator:

<- channelCode language: Go (go)

The following example shows how to receive a string message from a channel:

mesage := <- channelCode language: Go (go)

Receiving data from a channel will block the goroutine until the message is received.

Additionally, you can use a for range loop to read data from a channel:

for data := range channel {
    // process the data
}Code language: Go (go)

Putting it all together

Here’s the complete program that demonstrates how to use a channel to exchange data between goroutines:

package main

import "fmt"

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

    go func() {
        ch <- "Hi"
    }()

    message := <-ch
    fmt.Println(message)
}Code language: Go (go)

How it works.

First, create a channel ch with the type of string:

ch := make(chan string)Code language: Go (go)

Second, spawn a new goroutine that sends the message "Hi" to the ch channel:

go func() {
    ch <- "Hi"
}()Code language: Go (go)

Third, receive the message from the channel ch in the main goroutine:

message := <-chCode language: Go (go)

Note that the main goroutine will not complete until it receives the message from the ch channel.

Finally, display the message on the screen:

fmt.Println(message)Code language: Go (go)

In the following modified version of the program, the goroutine sends data to the channel after 3 seconds. Therefore, the main goroutine will wait for the same amount of time before receiving the data:

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("Starting the program...")
    ch := make(chan string)

    go func() {
        time.Sleep(3 * time.Second)
        ch <- "Hi"
    }()

    message := <-ch
    fmt.Println(message)
}Code language: Go (go)

Output:

Starting the program...
HiCode language: Go (go)

The message Hi appears after 3 seconds after the program starts.

Deadlocks

The following program causes a deadlock that you may encounter when start working with channels:

package main

import "fmt"

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

    go func() {
        ch <- "Hi"
    }()

    message := <-ch
    fmt.Println(message)

    // Error: deadlock
    message = <-ch
    fmt.Println(message)
}Code language: Go (go)

Output:

Hi
fatal error: all goroutines are asleep - deadlock!Code language: Go (go)

In this example:

  • First, a goroutine sends the message "Hi" to the channel.
  • Second, the main goroutine receives and prints the message.
  • Third, the main goroutine attempts to receive more data from the channel. However, no more data is sent to the channel, and the second receiving operation blocks indefinitely, causing a deadlock.

To prevent deadlocks, you should ensure every send has a matching receipt. In other words, there should be a corresponding receipt for every message you send, and vice versa.

A practical example of Go channels

The following example shows how to check the status of a list of URLs concurrently:

package main

import (
    "fmt"
    "net/http"
)

func check(url string, ch chan string) {
    _, err := http.Get(url)
    if err != nil {
        ch <- fmt.Sprintf("The %s is down.", url)
    } else {
        ch <- fmt.Sprintf("The %s is up.", url)
    }
}

func main() {
    urls := []string{
        "https://google.com",
        "https://go.dev",
        "https://abcdef.test/",
    }
    ch := make(chan string)
    for _, url := range urls {
        go check(url, ch)
    }

    for i := 0; i < len(urls); i++ {
        fmt.Println(<-ch)
    }
}Code language: Go (go)

How it works.

First, define a function that accepts a URL to check and a channel to send data to once the checking completes:

func check(url string, ch chan string) {
    _, err := http.Get(url)
    if err != nil {
        ch <- fmt.Sprintf("The %s is down.", url)
    } else {
        ch <- fmt.Sprintf("The %s is up.", url)
    }
}Code language: Go (go)

Next, declare and initialize a list of URLs to check:

urls := []string{
    "https://google.com",
    "https://go.dev",
    "https://abcdef.test/",
}Code language: Go (go)

Then, create a channel to exchange the messages:

ch := make(chan string)Code language: Go (go)

After that, spawn goroutines to check the URLs concurrently:

for _, url := range urls {
    go check(url, ch)
}Code language: Go (go)

Finally, receive the URL status from the ch channel in the main goroutine:

for i := 0; i < len(urls); i++ {
    fmt.Println(<-ch)
}Code language: Go (go)

Summary

  • Use channels to send and receive data between goroutines.
  • Use the channel <- data syntax to send data to a channel.
  • Use the data := <- channel to receive data from a channel.
  • Ensure the number of sends and receives are matched to avoid deadlocks.
Was this tutorial helpful?