This is a compact collection of examples where goroutines are actually unnecessary.
Background — just add "go"
Go makes it easy to parallelize tasks like saving statistics, tracking online status, or similar background work — just add the go keyword.
This simplicity helps you focus on business logic, but sometimes leads to mindlessly adding go before tasks that can be parallelized but don't need to be.
Example 1: launching a goroutine at the end of a goroutine
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func endLongtimeAction(now int64) {
time.Sleep(time.Microsecond)
// some logic
log.Printf("endLongtimeAction %d", now)
}
func longtimeAction(now int64) {
time.Sleep(time.Microsecond)
// some logic
log.Printf("longtimeAction %d", now)
// useless start goroutine
go endLongtimeAction(now)
}
func hello(w http.ResponseWriter, r *http.Request) {
// useful start goroutine
go longtimeAction(time.Now().UnixNano())
fmt.Fprintf(w, "hello\n")
}
func main() {
http.HandleFunc("/hello", hello)
log.Printf("start server on port 8090")
http.ListenAndServe(":8090", nil)
}
The goroutine before calling endLongtimeAction is unnecessary — it's the last thing longtimeAction does anyway.
Example 2: using a goroutine for a simple action
Here's a similar hello handler where AddView is launched in a goroutine:
func hello(w http.ResponseWriter, r *http.Request) {
go AddView(fontThemeID(), country(r))
fmt.Fprintf(w, "hello\n")
}
AddView is just a simple buffer write:
import "sync"
type FontThemeKey struct {
FontThemeID uint8
Country string
}
type FontThemeValue struct {
ViewCount uint32
RegisterCount uint32
}
var (
fontThemeBuffer = make(map[FontThemeKey]FontThemeValue)
fontThemeMutex sync.Mutex
)
func AddView(fontThemeID uint8, country string) {
var key = FontThemeKey{
FontThemeID: fontThemeID,
Country: country,
}
fontThemeMutex.Lock()
defer fontThemeMutex.Unlock()
var value = fontThemeBuffer[key]
value.ViewCount += 1
fontThemeBuffer[key] = value
}
func Flush() error {
return store(swap())
}
func swap() map[FontThemeKey]FontThemeValue {
fontThemeMutex.Lock()
defer fontThemeMutex.Unlock()
var result = fontThemeBuffer
fontThemeBuffer = make(map[FontThemeKey]FontThemeValue, len(result))
return result
}
func store(map[FontThemeKey]FontThemeValue) error {
// store to stats
return nil
}
Let's write a simple benchmark to check whether go actually speeds things up here:
import (
"sync/atomic"
"testing"
)
const (
fontThemeCount = 15
)
var (
fixtureCountries = []string{"C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8"}
fixtureCountryLength = uint32(len(fixtureCountries))
)
func reset() {
fontThemeBuffer = make(map[FontThemeKey]FontThemeValue)
}
func BenchmarkAddView(b *testing.B) {
reset()
var index uint32
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
var i = atomic.AddUint32(&index, 1)
AddView(uint8(i%fontThemeCount), fixtureCountries[i%fixtureCountryLength])
}
})
}
func BenchmarkAddViewExtraGoroutine(b *testing.B) {
reset()
var index uint32
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
var i = atomic.AddUint32(&index, 1)
// useless goroutine
go AddView(uint8(i%fontThemeCount), fixtureCountries[i%fixtureCountryLength])
}
})
}
Benchmark results:
| Test name | Iterations | Avg time per iteration | Memory allocated |
|---|---|---|---|
| AddView | 6413756 | 185 ns/op | 0 B/op |
| AddViewExtraGoroutine | 2628880 | 403 ns/op | 58 B/op |
The extra goroutine only slowed things down.
Example 3: goroutine by template
On a product page you need to render two blocks: "recommendations / similar items" and "bought together". Fetching them in parallel makes sense:
func ExtraGoodCodes(goodCode uint32) ([]uint32, []uint32) {
var wg = new(sync.WaitGroup)
var (
recommendations []uint32
supplies []uint32
)
wg.Add(1)
go func() {
recommendations = getRecommendationGoods(goodCode)
wg.Done()
}()
wg.Add(1)
go func() {
supplies = getSupplyGoods(goodCode)
wg.Done()
}()
wg.Wait()
return recommendations, supplies
}
The logic is correct, but one goroutine can be removed — the main goroutine can do one of the two calls directly:
func ExtraGoodCodes(goodCode uint32) ([]uint32, []uint32) {
var wg = new(sync.WaitGroup)
var (
recommendations []uint32
supplies []uint32
)
wg.Add(1)
go func() {
recommendations = getRecommendationGoods(goodCode)
wg.Done()
}()
supplies = getSupplyGoods(goodCode)
wg.Wait()
return recommendations, supplies
}
Epilogue
All of these examples came from real code I encountered during development. This article was originally written in Ukrainian five years ago — published on DOU — but the patterns are still just as common today.
If you're working with Go in production and want to see which product companies use it, check out ReadyToTouch — Go companies.
I'd be curious in the comments: which of these have you seen in the wild?
Top comments (0)