Starting Goroutines in Go
What a goroutine is, how the go keyword starts one, why main can exit before your goroutines finish, and how goroutines differ from threads.
A goroutine is a function that runs at the same time as the rest of your program. You start one by writing go in front of a function call. That one keyword is all the syntax there is, which is why Go programs use goroutines so freely, for example one per incoming web request.
Your First Goroutine
package main
import (
"fmt"
"time"
)
func say(word string) {
for i := 0; i < 3; i++ {
fmt.Println(word, i)
time.Sleep(10 * time.Millisecond)
}
}
func main() {
go say("background")
say("main")
}
go say("background") starts say in a new goroutine and returns immediately, without waiting. Then main calls say("main") normally. Both loops run at the same time, so the lines are mixed together, and the exact order can change from run to run:
main 0
background 0
background 1
main 1
main 2
background 2
Concurrency means several tasks are in progress during the same period. Whether they also run at the exact same instant on different CPU cores is up to the Go runtime, the part of Go that is built into every program and schedules goroutines.
main Does Not Wait for Goroutines
When main returns, the program ends, even if other goroutines are still running. They are stopped without warning.
package main
import "fmt"
func main() {
go fmt.Println("you will probably never see this")
fmt.Println("main is done")
}
This almost always prints only main is done. The goroutine was started, but the program exited before it got a chance to run.
Do not fix this with time.Sleep:
Adding time.Sleep(time.Second) at the end of main seems to work, but it is a guess. On a slow machine, or with more work, the goroutine may still be running when the sleep ends. Wait properly with sync.WaitGroup or a channel, which are built for exactly this.
Goroutines With Anonymous Functions
You often start a goroutine with a function literal, a function written inline without a name:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("job", i)
}()
}
wg.Wait()
}
This prints job 1, job 2 and job 3 in some order. Note the () after the closing brace: it calls the function. Without it, the compiler says expression in go must be function call.
sync.WaitGroup makes main wait until all three goroutines have called Done. It is explained fully on its own page.
Loop variables before Go 1.22:
Since Go 1.22, each loop iteration has its own i, so the code above prints 1, 2 and 3. In older versions all goroutines shared one i and often printed job 4 three times. If you work on a module whose go.mod says go 1.21 or lower, pass the value as an argument: go func(i int) { ... }(i).
Getting a Result Back From a Goroutine
A goroutine cannot return a value to the code that started it. x := go f() does not compile. To get a result, the goroutine sends it on a channel, which is a typed pipe between goroutines:
package main
import "fmt"
func sum(nums []int, out chan<- int) {
total := 0
for _, n := range nums {
total += n
}
out <- total
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6}
results := make(chan int)
go sum(nums[:3], results)
go sum(nums[3:], results)
a, b := <-results, <-results
fmt.Println("total:", a+b)
}
Each goroutine adds up half of the slice and sends its answer. main receives two values, which also makes it wait for both goroutines. The output is always total: 21. Channels have their own section with much more detail.
Goroutines Compared to Threads
Operating system threads are relatively heavy: each one reserves a large stack, and creating thousands of them strains the system. A goroutine starts with a small stack that grows when needed, and the Go runtime runs many goroutines on a few threads. Running tens of thousands of goroutines is normal in Go servers.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
var mu sync.Mutex
count := 0
for i := 0; i < 10000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
count++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println("finished goroutines:", count)
}
This starts ten thousand goroutines and prints finished goroutines: 10000 almost instantly. The sync.Mutex keeps the shared counter correct, which is the topic of the shared state section.
Cheap is not free:
Each goroutine still uses memory, and one that never finishes is a leak that grows forever. Every goroutine you start should have a clear way to end: its work runs out, its input channel is closed, or it is told to stop.
You have the basics:
If you can start a goroutine with go, explain why main must wait for it, and get a result back through a channel, you know everything needed to read most concurrent Go code.
Summary
go f() runs f in a new goroutine and returns right away. The program ends when main returns, so you must wait for goroutines with a WaitGroup or channel, never with time.Sleep. Goroutines cannot return values directly, so results come back over channels. They are much lighter than threads, but each one still needs a clear way to finish.