Goroutines
| The go keyword | |
| Anonymous goroutines | |
| WaitGroups | |
| Never leak a goroutine |
The go keyword
The go keyword starts a function call in a new goroutine, a lightweight thread managed by the runtime. The caller keeps running while the new routine works in parallel. Thousands of goroutines cost little, unlike OS threads. The sleep below only keeps this demo alive long enough to print.
package main
import (
"fmt"
"time"
)
func greet(name string) {
fmt.Println("hello,", name)
}
func main() {
go greet("ada")
go greet("grace")
time.Sleep(100 * time.Millisecond)
}
The two greetings may print in any order, since the routines race each other. Sleeping to wait is a demo trick, not a technique. WaitGroups below do the waiting properly.
Anonymous goroutines
Short background tasks fit naturally in an anonymous function started with go. The literal runs at once in its own routine. Pass loop values as arguments so each routine gets its own copy. Capturing the loop variable directly is a classic bug.
package main
import (
"fmt"
"time"
)
func main() {
for i := 1; i <= 3; i++ {
go func(id int) {
fmt.Println("worker", id)
}(i)
}
time.Sleep(100 * time.Millisecond)
}
Each routine receives its own id at launch time. Output order still varies from run to run. That nondeterminism is normal for concurrent code and fine here.
WaitGroups
sync.WaitGroup waits until a set of routines all finish. Add counts a task, Done marks it complete, and Wait blocks for the rest. Defer Done so panics and early returns still report back. This is the standard way to join background work.
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("job", id)
}(i)
}
wg.Wait()
fmt.Println("all done")
}
Add runs before go starts, so the counter never drops to zero early. Wait returns only after three Done calls. Replace every waiting Sleep with this pattern from now on.
Never leak a goroutine
A leaked goroutine waits forever on a signal that never comes and its memory never returns. Every routine needs an exit path, usually a done channel closed by the owner. Closing broadcasts the stop to all listeners at once. The worker below ticks until that moment arrives.
package main
import (
"fmt"
"time"
)
func worker(done <-chan struct{}) {
for {
select {
case <-done:
fmt.Println("worker stopped")
return
case <-time.After(10 * time.Millisecond):
fmt.Println("tick")
}
}
}
func main() {
done := make(chan struct{})
go worker(done)
time.Sleep(35 * time.Millisecond)
close(done)
time.Sleep(20 * time.Millisecond)
}
The receive-only parameter documents that the worker never closes the channel itself. Only the owner closes, exactly once. Channels and select get full lessons right after this one.
Next: Channels
Article author: Arthur Isaev