# Goroutines & Concurrency in Go: A Beginner's Guide

go dev.to

By Edward Odero | Apprentice at z01 Kisumu


Introduction

One of the things that makes Go stand out from other programming languages is how it handles concurrency. If you've ever wanted your program to do multiple things at the same time — like fetching data from an API while also writing to a file — Go makes this surprisingly easy with something called goroutines.

In this article, I'll walk you through what goroutines are, how they work, and how to use them alongside channels to build concurrent programs in Go.


What is Concurrency?

Concurrency means doing multiple things at the same time (or at least, making progress on multiple tasks at once). Think of it like a chef cooking multiple dishes simultaneously — they don't finish one dish before starting another; they manage them all in parallel.

In Go, goroutines are the building blocks of concurrency.


What is a Goroutine?

A goroutine is a lightweight thread managed by the Go runtime. Unlike threads in other languages, goroutines are incredibly cheap — you can run thousands (even millions) of them without breaking a sweat.

You start a goroutine simply by using the go keyword before a function call:

package main

import (
    "fmt"
    "time"
)

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

func main() {
    go sayHello() // This runs concurrently
    time.Sleep(1 * time.Second) // Wait for the goroutine to finish
    fmt.Println("Main function done")
}
Enter fullscreen mode Exit fullscreen mode

Output:

Hello from a goroutine!
Main function done
Enter fullscreen mode Exit fullscreen mode

Note: Notice the time.Sleep? Without it, the main function might finish and exit before the goroutine gets a chance to run. We'll see a better way to handle this shortly.


The Problem: Goroutines Don't Wait

When the main function exits, all goroutines are killed — even if they haven't finished. This is why we need a way to synchronize goroutines.

Solution 1: sync.WaitGroup

A WaitGroup lets you wait for a collection of goroutines to finish:

package main

import (
    "fmt"
    "sync"
)

func printMessage(msg string, wg *sync.WaitGroup) {
    defer wg.Done() // Signal that this goroutine is done
    fmt.Println(msg)
}

func main() {
    var wg sync.WaitGroup

    messages := []string{"Hello", "from", "goroutines!"}

    for _, msg := range messages {
        wg.Add(1) // Tell WaitGroup to expect one more goroutine
        go printMessage(msg, &wg)
    }

    wg.Wait() // Block until all goroutines are done
    fmt.Println("All goroutines finished!")
}
Enter fullscreen mode Exit fullscreen mode

Output (order may vary):

from
Hello
goroutines!
All goroutines finished!
Enter fullscreen mode Exit fullscreen mode

Notice the order may vary! That's concurrency in action — goroutines don't always finish in the order they were started.


Channels: Goroutines Talking to Each Other

Goroutines are great, but what if you want them to share data safely? That's where channels come in.

A channel is like a pipe — one goroutine puts data in, another takes it out.

package main

import "fmt"

func sum(nums []int, ch chan int) {
    total := 0
    for _, n := range nums {
        total += n
    }
    ch <- total // Send result to channel
}

func main() {
    nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    ch := make(chan int)

    // Split the work between two goroutines
    go sum(nums[:5], ch)
    go sum(nums[5:], ch)

    part1, part2 := <-ch, <-ch // Receive both results
    fmt.Println("Total:", part1+part2)
}
Enter fullscreen mode Exit fullscreen mode

Output:

Total: 55
Enter fullscreen mode Exit fullscreen mode

Here, two goroutines each sum half the slice, then send their results back through the channel. Clean, safe, and concurrent.


Buffered vs Unbuffered Channels

By default, channels are unbuffered — a sender blocks until a receiver is ready (and vice versa).

You can create a buffered channel that holds a limited number of values without a receiver being ready:

ch := make(chan int, 3) // Buffered channel with capacity 3

ch <- 1 // Doesn't block
ch <- 2 // Doesn't block
ch <- 3 // Doesn't block
// ch <- 4 // This would block! Buffer is full

fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3
Enter fullscreen mode Exit fullscreen mode

The select Statement

The select statement lets a goroutine wait on multiple channels at once — like a switch statement, but for channels:

package main

import (
    "fmt"
    "time"
)

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

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "one"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "two"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println("Received:", msg1)
        case msg2 := <-ch2:
            fmt.Println("Received:", msg2)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

Received: one
Received: two
Enter fullscreen mode Exit fullscreen mode

select picks whichever channel is ready first — perfect for handling timeouts or multiple data sources.


Key Takeaways

Concept What it Does
go func() Starts a goroutine
sync.WaitGroup Waits for goroutines to finish
chan Creates a channel for safe communication
Buffered channels Allow sending without an immediate receiver
select Listens on multiple channels at once

Wrapping Up

Goroutines and channels are at the heart of what makes Go so powerful for building concurrent and scalable programs. The syntax is clean, the mental model is simple, and the performance is excellent.

As a beginner learning Go at z01 Kisumu, this was one of those topics that really made me appreciate the language. The fact that you can spin up thousands of goroutines with just the go keyword is honestly mind-blowing compared to threading in other languages.

If you're just starting with Go, I encourage you to experiment with goroutines — try building a small program that fetches data concurrently or processes a list in parallel. You'll be amazed at how approachable it is!


Feel free to drop a comment if you have questions or want to share your own experience with Go concurrency.

Source: dev.to

arrow_back Back to Tutorials