← Back to list

Goroutines in Go — Simple, Powerful, Subtle

Do not communicate by sharing memory; instead, share memory by communicating.  — Rob Pike

Arash Mousavi · 2025-07-19 10:41 · 9 claps · 2.9 min read
#golang #goroutines #concurrency #backend-development
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Goroutines in Go — Simple, Powerful, Subtle

Do not communicate by sharing memory; instead, share memory by communicating.Rob Pike

Introducing

Go is famous for its built-in concurrency support. The core of this power is the goroutine.

A goroutine is like a thread, but much lighter. Unlike OS threads, goroutines are fully managed by the Go runtime. This allows you to run thousands or even millions of them on just a few CPU cores.

Starting one is simple:

go myFunction()

This simplicity is one of Go’s biggest strengths — and exactly where many developers run into trouble.

If you don’t manage your goroutines properly, you can run into:

  • Goroutine leaks
  • Deadlocks
  • Race conditions

These issues can silently break your production systems.

Goroutine Basics — From Serial to Concurrent

Functions that run with go are called goroutines. The Go runtime juggles these goroutines across OS threads and CPU cores.

Compared to system threads, goroutines are so lightweight that you can easily spin up thousands.

No Goroutines

Let’s start with a simple function that prints each word in a phrase, adding random pauses:

func say(phrase string) {
    for _, word := range strings.Fields(phrase) {
        fmt.Printf("says: %s…\n", word)
        dur := time.Duration(rand.Intn(100)) * time.Millisecond
        time.Sleep(dur)
    }
}

call it:

func main() {
    say("Hello, World!")
}

Output:

says: Hello,…
says: World!…

Two Serial Calls

Now let’s run two talkers, one after the other:

func say(id int, phrase string) {
    for _, word := range strings.Fields(phrase) {
        fmt.Printf("Worker #%d says: %s...\n", id, word)
        dur := time.Duration(rand.Intn(100)) * time.Millisecond
        time.Sleep(dur)
    }
}

func main() {
    say(1, "Hello, World!")
    say(2, "Go is awesome")
}

Output:

Worker #1 says: Hello...
Worker #1 says: World...
Worker #2 says: Go...
Worker #2 says: is...
Worker #2 says: awesome...

It works — but the two functions block each other.

Running in Parallel with Goroutines

func main() {
    go say(1, "Go is awesome")
    go say(2, "Cats are cute")
    time.Sleep(500 * time.Millisecond)
}
Worker #1 says: Go...
Worker #2 says: Cats...
Worker #1 says: is...
Worker #2 says: are...
Worker #2 says: cute...
Worker #1 says: awesome...

Now the workers really compete for attention. Each runs independently.

When we write go f(), the function f() runs independently of the others.

This simple go keyword unlocks real concurrency — but don’t confuse it with async/await in JavaScript or other languages. Go takes a very different approach. Look at it with fresh eyes.

Dependent and Independent Goroutines

When you call go say(), the function runs on its own. main does not wait for it.

For example:

func main() {
    go say(1, "go is awesome")
    go say(2, "cats are cute")
}

This prints nothing — because main finishes before the goroutines start.

main is a goroutine too

main is actually just another goroutine. Once main ends, the whole program stops—no matter how many other goroutines are running.

Why time.Sleep is not enough

Using time.Sleep() to wait for goroutines is fragile. You don’t know exactly how long the work will take. It’s a guess—and in real systems, guessing leads to bugs.

The Proper Way: sync.WaitGroup

func main() {
    var wg sync.WaitGroup

    wg.Add(1)
    go say(&wg, 1, "go is awesome")

    wg.Add(1)
    go say(&wg, 2, "cats are cute")

    wg.Wait()
}

func say(wg *sync.WaitGroup, id int, phrase string) {
    for _, word := range strings.Fields(phrase) {
        fmt.Printf("Worker #%d says: %s...\n", id, word)
        dur := time.Duration(rand.Intn(100)) * time.Millisecond
        time.Sleep(dur)
    }
    wg.Done()
}

Output:

Worker #2 says: cats...
Worker #2 says: are...
Worker #1 says: go...
Worker #2 says: cute...
Worker #1 says: is...
Worker #1 says: awesome..

How it works:

  • wg.Add(1) increases the counter
  • wg.Done() decreases it
  • wg.Wait() blocks main until the counter reaches zero

Keep Logic Clean

There’s a catch: Now say knows about sync.WaitGroup. This makes it harder to reuse say in other contexts.

The Idiomatic Way: Use Anonymous Functions

Separate your business logic from concurrency logic:

func main() {
    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()
        say(1, "go is awesome")
    }()

    go func() {
        defer wg.Done()
        say(2, "cats are cute")
    }()

    wg.Wait()
}

Now say is clean. It has no idea about concurrency — it just does its job.

Final Thoughts

  • Goroutines are simple to start but subtle to get right.
  • Use sync.WaitGroup or channels—not Sleep—to wait for goroutines.
  • Keep business logic and concurrency logic separate.
  • Test your concurrent code carefully. Use go run -race to detect data races early.

메타데이터
post_id
9d89dd6b00a8
slug
goroutines-in-go-simple-powerful-subtle-9d89dd6b00a8
url
https://medium.com/@a.mousavi/goroutines-in-go-simple-powerful-subtle-9d89dd6b00a8
canonical_url
https://medium.com/@a.mousavi/goroutines-in-go-simple-powerful-subtle-9d89dd6b00a8
author_url
https://medium.com/@a.mousavi
status
ok
fetched_at
2026-07-18 21:04:36