DEV Community

Yaroslav Podorvanov
Yaroslav Podorvanov

Posted on

The Goroutine Temptation. Examples of goroutine usage where they are actually unnecessary

#go

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)
}
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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])
        }
    })
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)