Tutorials Logic, IN info@tutorialslogic.com
Navigation
Home About Us Contact Us Blogs FAQs
Tutorials
All Tutorials
Services
Academic Projects Resume Writing Website Development
Practice
Quiz Challenge Interview Questions Certification Practice
Tools
Online Compiler JSON Formatter Regex Tester CSS Unit Converter Color Picker
Compiler Tools

Golang Goroutines: Lightweight Concurrency with Practical Examples

What Is a Goroutine?

A goroutine is a lightweight concurrent function managed by the Golang runtime. You start one by placing the go keyword before a function call. The function then runs independently while the current function continues.

Goroutines are much cheaper than operating system threads, so servers can run many concurrent tasks such as request handling, background jobs, timers, queue consumers, and streaming work. They are lightweight, but they are not magic. Every goroutine still needs clear ownership, synchronization, and a way to finish.

Start a Goroutine
package main

import (
    "fmt"
    "time"
)

func sendEmail(user string) {
    time.Sleep(500 * time.Millisecond)
    fmt.Println("email sent to", user)
}

func main() {
    go sendEmail("asha@example.com")

    fmt.Println("request finished")
    time.Sleep(1 * time.Second)
}

Waiting with sync.WaitGroup

The main function or caller does not automatically wait for goroutines. If the program exits before a goroutine finishes, that goroutine is stopped with the process. Use sync.WaitGroup when the caller must wait for a known number of tasks.

The pattern is simple: call Add before starting the goroutine, call Done when the goroutine finishes, and call Wait where the caller should block until all work is complete.

MethodPurpose
Add(n)Increase the number of goroutines or tasks to wait for.
Done()Mark one task as finished. It is commonly deferred.
Wait()Block until the counter returns to zero.
Wait for Workers
package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)

        go func(id int) {
            defer wg.Done()
            fmt.Println("worker", id, "finished")
        }(i)
    }

    wg.Wait()
    fmt.Println("all workers complete")
}

Avoid Loop Variable Mistakes

When starting goroutines inside a loop, pass loop values into the goroutine as parameters. This makes each goroutine receive the value intended for that iteration.

Modern Golang versions improved loop variable behavior, but passing values explicitly is still clear, portable, and easy for learners to reason about.

Pass Loop Values Explicitly
for _, user := range users {
    wg.Add(1)

    go func(email string) {
        defer wg.Done()
        sendEmail(email)
    }(user.Email)
}

Race Conditions

A race condition happens when goroutines access the same data at the same time and at least one access writes. Race bugs are hard to reproduce because the result depends on timing.

Protect shared state with channels, mutexes, atomic operations, or by designing ownership so only one goroutine modifies a value. During development, run go test -race to catch many unsafe shared-memory mistakes.

Mutex Protection
package main

import "sync"

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

Stopping Goroutines

Do not start goroutines without knowing how they stop. Long-running services should use context cancellation, closed channels, deadlines, or worker shutdown signals.

A leaked goroutine can hold memory, file handles, network connections, or locks longer than expected. Design the stop path when you design the start path, especially in web servers and background workers.

  • Use context.Context for request-scoped cancellation.
  • Use a closed channel to broadcast a stop signal to multiple workers.
  • Use deadlines and timeouts around network or database work.
  • Avoid goroutines that block forever waiting for a value that may never arrive.
Stop with Context
func worker(ctx context.Context, jobs <-chan Job) {
    for {
        select {
        case <-ctx.Done():
            return
        case job := <-jobs:
            process(job)
        }
    }
}

Practical Goroutine Guidelines

Use goroutines for independent work that can truly run at the same time: handling connections, processing jobs, fetching multiple resources, or watching for events. Do not add goroutines just because a function looks slow; first understand the work, ownership, and cancellation rules.

A clean concurrent design usually has a small number of obvious coordination points. If many goroutines are writing shared state from many places, the design may need a worker, channel, mutex, or queue to make ownership clearer.

Key Takeaways
  • Use go to start a goroutine.
  • Use sync.WaitGroup to wait for a known number of concurrent tasks.
  • Pass loop values explicitly when starting goroutines in loops.
  • Synchronize shared mutable state with channels, mutexes, or clear ownership.
  • Every long-running goroutine should have a clear stop condition.

Ready to Level Up Your Skills?

Explore 500+ free tutorials across 20+ languages and frameworks.