DEV Community

Cover image for Understanding Goroutines and Channels in Go
Kamal Rhrabla
Kamal Rhrabla

Posted on

Understanding Goroutines and Channels in Go

Concurrency is one of the biggest reasons developers choose Go. While many languages require complex threading models or external libraries, Go provides a simple and powerful way to handle concurrent tasks using goroutines and channels.

In this article, we’ll explore what goroutines and channels are, how they work, and how you can use them to build efficient concurrent programs.

What is a Goroutine?

A goroutine is a lightweight thread managed by the Go runtime. Unlike operating system threads, goroutines are extremely cheap to create and can scale to thousands or even millions running at the same time.

Creating a goroutine is simple: you just add the go keyword before a function call.

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    fmt.Println("Hello from goroutine")
}

func main() {
    go sayHello()
    time.Sleep(time.Second)
    fmt.Println("Main function finished")
}
Enter fullscreen mode Exit fullscreen mode

What happens here?

The go keyword starts sayHello() in a separate goroutine.

The main function continues executing without waiting.

We add time.Sleep so the program doesn’t exit before the goroutine runs.

In real applications, we typically use synchronization mechanisms instead of sleep.

Why Goroutines Are Powerful

Traditional threads can be heavy and expensive. Goroutines solve this problem by being:

  • Lightweight, very small memory footprint
  • Fast to create
  • Managed by the Go scheduler
  • Perfect for I/O-heavy tasks

Examples of where goroutines shine:

  • Web servers handling many requests
  • Background jobs
  • API calls to multiple services
  • Concurrent data processing

The Problem: Sharing Data Between Goroutines

If multiple goroutines run at the same time, how do they communicate safely?

In many languages you would use:

  • Locks
  • Mutexes
  • Shared memory

Go promotes a different philosophy:

“Do not communicate by sharing memory; share memory by communicating.”

This is where channels come in.

What Are Channels?

A channel is a typed conduit that allows goroutines to communicate and synchronize.

You can send values into a channel and receive values from it.

Creating a channel

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

This creates a channel that can transmit integers.

Sending and Receiving Data

package main

import "fmt"

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

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

    value := <-ch
    fmt.Println(value)
}
Enter fullscreen mode Exit fullscreen mode

What’s happening?

We create a channel.

A goroutine sends the value 42 into the channel.

The main goroutine receives the value and prints it.

Output:

42
Enter fullscreen mode Exit fullscreen mode

Channels block by default, meaning:

  • Sending waits until another goroutine receives
  • Receiving waits until another goroutine sends
  • This makes synchronization simple.
  • Buffered Channels Sometimes you don’t want the sender to block immediately. For that, Go provides buffered channels.
ch := make(chan int, 2)
Enter fullscreen mode Exit fullscreen mode

Example:

package main

import "fmt"

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

    ch <- 1
    ch <- 2

    fmt.Println(<-ch)
    fmt.Println(<-ch)
}
Enter fullscreen mode Exit fullscreen mode

Here the channel can store 2 values before blocking.

Using Goroutines with Channels (Real Example)

Let’s run multiple tasks concurrently and collect their results.

package main

import (
    "fmt"
    "time"
)

func worker(id int, ch chan string) {
    time.Sleep(2 * time.Second)
    ch <- fmt.Sprintf("Worker %d finished", id)
}

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

    for i := 1; i <= 3; i++ {
        go worker(i, ch)
    }

    for i := 1; i <= 3; i++ {
        fmt.Println(<-ch)
    }
}
Enter fullscreen mode Exit fullscreen mode

Output (order may vary):

Worker 2 finished
Worker 1 finished
Worker 3 finished
Enter fullscreen mode Exit fullscreen mode

Here we:

  • Start 3 goroutines
  • Each performs work
  • Each sends a result back through the channel
  • The main goroutine collects the results
  • This pattern is very common in Go.
  • Closing Channels

Sometimes we want to signal that no more values will be sent.

close(ch)
Enter fullscreen mode Exit fullscreen mode

Example:

for value := range ch {
    fmt.Println(value)
}
Enter fullscreen mode Exit fullscreen mode

The loop automatically stops when the channel is closed.

When to Use Goroutines and Channels

You should consider them when:

  • Running multiple independent tasks
  • Making parallel API requests
  • Processing jobs in background workers
  • Building high-performance servers
  • Streaming data between components

Common Beginner Mistakes:

1. Forgetting that goroutines are asynchronous

Your program might exit before goroutines finish.

2. Deadlocks

Example:

fatal error: all goroutines are asleep - deadlock!
Enter fullscreen mode Exit fullscreen mode

This happens when goroutines are waiting on each other with no progress.

3. Overusing goroutines

Just because goroutines are cheap doesn’t mean everything should be concurrent.

Final Thoughts

Goroutines and channels are core features that make Go a powerful language for building scalable and concurrent systems.

Instead of complex thread management, Go gives developers a clean and expressive model for concurrency.

Once you master these two concepts, you'll unlock one of Go’s greatest strengths.

If you're learning Go for backend development, mastering goroutines, channels, and concurrency patterns will significantly improve your ability to build fast and scalable services.

Top comments (0)