Go - Channels and everything you need to know about it!

go dev.to

In this article, we are going to explore and take a deep dive into one of the most important topics in the Go language: channels, and how we can use them with goroutines.

If you don’t understand the concept yet, don’t worry—just keep reading. We’ll break everything down step by step for you:

1 - We’ll talk about concurrency
2 - We'll navigating in Channel concept, with a lot of practice of course
3 - We’ll explore the most common example: the producer-consumer pattern, more dirty hands with code! Yay!

Concurrency

Well, what is concurrency? Think about a person sitting at a desk, typing on a keyboard and drinking a cup of coffee (perhaps that person is you right now!). Let’s say that person is typing using both hands. Can that person drink coffee and type at the exact same time? No.

Concurrency is about dealing with multiple tasks; however, this does not mean that things will be processed at the same time — that would be parallelism. Concurrency and parallelism are not the same thing, remember that.

Concurrency is dealing with multiple tasks by switching between them
Paralelism is executing multiple tasks at the exact same time

That's kind of my way to try explain but a really like Rob Pike quote, i also left here to you my deer reader absorb for a moment ;)

Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once

Okay, now you think "i came here to see the fun part, talk it's cheep show me that code!" so let's jump to the second part!

Channels

How we can define channels? I like to think that channels is basely a link that allow us programming communication between goroutines.

There's two ways to declare channels in golang, first is without a buffer

ch := make(chan int) 
Enter fullscreen mode Exit fullscreen mode

With an unbuffered channel, every send must have a corresponding receive. If a goroutine tries to send a value and no one is ready to receive it, the program will pause at that point.

You can think of it as a direct handoff — one goroutine gives, the other takes, at the exact same moment.

Now let’s look at a buffered channel:

ch := make(chan int, 100)
Enter fullscreen mode Exit fullscreen mode

The second parameter in make function define the channel with has a capacity of a hundred. This means it can hold up to 100 values internally before blocking.

So the sender can “get ahead” of the receiver, pushing values into the channel without waiting immediately. Only when the buffer is full does the sender block.

This introduces another important concept buffered channels act like a queue, while unbuffered channels act like a synchronization point.

Example

ch := make(chan int)

go func() {
    ch <- 1
}()

value := <-ch
println(value)
Enter fullscreen mode Exit fullscreen mode

In this case, the send (ch <- 1) will block until the main goroutine is ready to receive.

Very small example, yes. But it already shows us one of the most important ideas behind channels: communication and synchronization walking together. Also, get us ready to next final part! Are you ready?

Produce-Consumer Pattern

Now let’s talk about one of the most classic use cases when working with channels: the producer-consumer pattern.

The idea here is beautifully simple. One part of the program is responsible for producing data, and another part is responsible for consuming that data.

Instead of making both sides depend directly on each other, we place a communication layer in the middle. In Go, that layer is usually a channel(incredible, right?).

The producer sends values into the channel, and the consumer reads values from it.

What makes this pattern so interesting is that both sides can work independently. The producer does not need to know how the consumer will process the data, and the consumer does not need to know where that data came from. They only need to agree on the channel they use to communicate.

If we bring this into a real-life analogy, think about a restaurant kitchen. A waiter writes down the orders and places them in a queue. The cooks do not need to talk directly to the customers, they just receive the next order and start preparing it. In this case, the waiter is the producer, the cooks are the consumers, and the queue is the channel. You better give a good tip!

Now, enough talking, let’s get our hands dirty with code:

package main

import "fmt"

func producer(ch chan int) {
    for i := 1; i <= 5; i++ {
        fmt.Println("producing:", i)
        ch <- i
    }
    close(ch)
}

func consumer(ch chan int) {
    for value := range ch {
        fmt.Println("consuming:", value)
    }
}

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

    go producer(ch)
    consumer(ch)
}
Enter fullscreen mode Exit fullscreen mode

The producer function sends numbers from 1 to 5 into the channel and then closes it, signaling that no more data will come.

On the other side, the consumer reads values using range ch, which is one of the most idiomatic ways to consume data from a channel in Go, since it keeps reading until the channel is closed.

Then, in main, we create the channel, start the producer in its own goroutine, and let the consumer read from that shared flow of data.

What I really like about this example is how clear the responsibilities are. One function produces, the other consumes, and the channel becomes the bridge between them.

Of course, this is just the beginning.

In real-world applications, things can get a little more interesting. What if we have multiple consumers? What if the producer needs to stop early? What if a task takes too long and we need to cancel it gracefully?

That is where more advanced Go concepts start, especially things like select, worker pools, and the context package.

So if this example made you think, “okay... this can get much more powerful,” you are absolutely right. And that is exactly where Go starts to get even more fun. Stay tuned, more good stuff is coming soon in future articles!

Source: dev.to

arrow_back Back to Tutorials