Goroutines in Go — Simple, Powerful, Subtle
Do not communicate by sharing memory; instead, share memory by communicating. — Rob Pike
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 counterwg.Done()decreases itwg.Wait()blocksmainuntil 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.WaitGroupor channels—notSleep—to wait for goroutines. - Keep business logic and concurrency logic separate.
- Test your concurrent code carefully.
Use
go run -raceto 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