DEV Community

Cover image for Finally: GO channels explained
Ayush Gupta
Ayush Gupta

Posted on

Finally: GO channels explained

Learning Go was going just fine until I met you—channel. You pipe-looking snitch. This guy can't hold a secret! Always ready to gossip.

Go channels sit right at the center of Go multi-threading. They make data transfer between goroutines as simple as bread and butter.

Now let's get serious... like really serious about Go channels and how they function. But before we start exploring them, I have to promise you something: By the end of this blog post, you will have a basic understanding—even much more than that—about channels. And on this unshakeable foundation, you will be able to build your building of further, more advanced topics. This blog is not written "bookishly"—loosely translated as not being ordered in such a way that it feels boring by going too much into theory. Let's get our hands dirty from the start!

Creating our first channel

package main

import (
    "fmt"
    "time"
)

func PrintValue(channel chan string) {
    fmt.Println(<-channel)
}

func main() {
    channel := make(chan string)
    go PrintValue(channel)
    channel <- "hello"
    time.Sleep(time.Second * 2)
}
Enter fullscreen mode Exit fullscreen mode

Here is our channel! It sends a string from the main thread to a goroutine. The goroutine, on receiving data from the main thread, prints the value.

Why did I use time.Sleep() here?

It is just for the purpose of demonstration. In real-world scenarios, we are better off using WaitGroups. time.Sleep() pauses the main thread from exiting and ending the program before the other goroutine finishes execution.

By default, channels are unbuffered, meaning the sender and the receiver must be ready at the same time in order to transfer data.

What does "being ready at the same time" mean?

Unbuffered channels are blocking, which means they pause execution until the receiver end is ready to receive the data.

What do you think will happen here?

func main() {
    channel := make(chan string)
    channel <- "hello"
    fmt.Println(<-channel)
}
Enter fullscreen mode Exit fullscreen mode

It looks like it will work, but let me break it to you—it didn't. The reason? We are:

  1. Sending data into the channel in the main thread
  2. The channel is blocked until the receiver end is ready
  3. The receiver end is also in the main thread
  4. Deadlock!

Let's move on to our next topic: buffered channels. These types of channels have some buffer area. They have a certain amount of storage they can fill before blocking.

Declaring a buffered channel is quite similar to an unbuffered channel but with a capacity:

channel := make(chan string, 3)
Enter fullscreen mode Exit fullscreen mode

Here we have a channel of type string with a capacity of 3.

That means we can send up to 3 complete strings before it blocks the execution.

channel <- "Hello"
channel <- "Hi"
channel <- "Namaste"
channel <- "Salut" // this will not work until and unless the
                   // receiver receives previous values and frees the channel
Enter fullscreen mode Exit fullscreen mode

Now let us fix our earlier deadlock using a buffered channel:

func main() {
    channel := make(chan string, 1)
    channel <- "hello"
    fmt.Println(<-channel)
}
Enter fullscreen mode Exit fullscreen mode

Now this will work! The channel has a capacity of 1, which means it can store one string before blocking the execution. But does this also mean the following will work?

func main() {
    channel := make(chan string, 1)
    fmt.Println(<-channel)
    channel <- "hello"
}
Enter fullscreen mode Exit fullscreen mode

No matter how much capacity you increase, there will always be a deadlock.

The reason is simple:the receiver end is always blocking. If some part of the program is trying to receive from a channel, it will block execution. Hence, deadlock.

Moving on, now that we have a rudimentary understanding of the types of channels, we can start exploring certain use cases without introducing much complexity.

Sending data from main thread to worker thread

Here we create 10 worker threads using a for-loop and send some integer data to them using an unbuffered channel.

package main

import (
    "fmt"
    "time"
)

func worker(channel chan int) {
    time.Sleep(time.Second / 2)
    fmt.Println("Completed Task:", <-channel)
}

func main() {
    channel := make(chan int)
    for i := 0; i < 10; i++ {
        go worker(channel)
        channel <- i + 1
    }

    time.Sleep(time.Second * 6)
}
Enter fullscreen mode Exit fullscreen mode

Here is how the execution happens:

  1. First, the main thread creates a worker thread and passes the channel to it.
  2. After creation, it sends data to the channel and waits for the other end to receive while pausing further execution.
  3. The worker thread does some work (simulated by 0.5 seconds of delay) and receives the data.
  4. The main thread continues execution.

Note: This gives us one advantage. All goroutines are supposed to run simultaneously, but here they are pausing.

Fix using buffered channel

package main

import (
    "fmt"
    "time"
)

func worker(channel chan int) {
    time.Sleep(time.Second / 2)
    fmt.Println("Completed Task:", <-channel)
}

func main() {
    channel := make(chan int, 10)
    for i := 0; i < 10; i++ {
        go worker(channel)
        channel <- i + 1
    }

    time.Sleep(time.Second * 2)
}
Enter fullscreen mode Exit fullscreen mode

Here is how the execution happens:

  1. First, the main thread creates worker threads and passes the channel to them.
  2. After creation, it sends data to the channel and continues execution without pausing because the channel has capacity for more data.
  3. The worker thread does some work (simulated by 0.5 seconds of delay) and receives the data.

Just by adding storage to our channel, we make our program 10x faster!

Minimum time (just considering sleep time) before buffered channel = 0.5 * 10 = 5 seconds

Minimum time after buffered channel = 0.5 seconds (every thread running simultaneously)

Note: It is not guaranteed how the threads will be processed. You might see output that is not always ordered the way you expect (1 to 10).

Directional Channels

Let us talk about our final topic for this blog, and that is Directional Channels.

Directional channels let you specify what end of the channel the function will be using. They are restricted to only sending or only receiving data. This prevents a function from accidentally performing the wrong operation on a channel.

Send-only channel: This informs the function that the only thing it is allowed to do with the channel is send data.

func sendData(ch chan<- int) {
    ch <- 10
}
Enter fullscreen mode Exit fullscreen mode

Receive-only channel: This informs the function that it will just be receiving on this channel and nothing more.

func receiveData(ch <-chan int) {
    fmt.Println(<-ch)
}
Enter fullscreen mode Exit fullscreen mode

Here is a very simple example to illustrate directions:

package main

import "fmt"

func sendData(ch chan<- string) {
    ch <- "Hello from sender!"
}

func receiveData(ch <-chan string) {
    fmt.Println(<-ch)
}

func main() {
    ch := make(chan string)
    go sendData(ch)
    receiveData(ch)
}
Enter fullscreen mode Exit fullscreen mode

In this example, sendData can only send values into the channel, and receiveData can only read from it. This restriction enforces clear communication direction—one function writes, the other reads—preventing accidental misuse of the channel (like trying to read in a sender function or vice versa). It's a clean, safe way to structure concurrent communication in Go.

There's a lot to cover, but I think this article explained the core problems that someone might deal with when picking up channels. Strong foundation.

There are more things that I want to cover, but I will give you some time to digest:

  • ranging over channels
  • WaitGroups
  • select statement
  • and many more...

That's it for channels Part 1! Practice these examples, and you'll never fear the pipe-looking snitch again. Next time: range, select, and real-world patterns.

Have a nice day!😊

Top comments (0)