Featured image

I recently ran into a -race error message that stumped me for a while, so I’ll retell it here in phases to see if you can catch it.

The code Link to heading

The intent of the code was a service abstraction that ran a goroutine in the background to send runtime stats to Stastd. A small example of the code follows:

type Stats struct {
    prev runtime.MemStats
}

func (p *Stats) stats() {
    ms := runtime.MemStats{}
    runtime.ReadMemStats(&ms)
    mallocCount := ms.Mallocs - p.prev.Mallocs
    fmt.Println("malloc change is", mallocCount)
    p.prev = ms
}

func (p *Stats) Loop(ctx context.Context) {
    runtime.ReadMemStats(&p.prev)
    for {
        select {
        case <-time.After(time.Millisecond * 100):
            p.stats()
        case <-ctx.Done():
            return
        }
    }
}

You can be 100% sure that I never run Loop() for the same Stats struct. And you’re 100% sure I only make a single Stats{} struct in my program. Before reading forward, inspect the above code and see if you can find the race condition I hit while using Stats.

A hint Link to heading

The race condition occurred on line 19 that looks like.

p.prev = ms

Does that help find the race condition?

A second, bigger hint Link to heading

The code that created my service structure looked somewhat like the following.

s := Stats{}
go s.Loop(ctx)
fmt.Println("I started service", s)

Private does not mean undiscoverable Link to heading

Gopher inside glass jar

Break in emergency

When you try to print a structure in Go with the Sprint or Println family of operations it will check to see if it’s a Formatter or Stringer and usually use those methods, otherwise it will use reflection to invent a way to print the structure. You can inspect the conditions for when is uses Formatter or Stringer inside print.go.

This particular code stumped me because I kept looking at Stats{} and thinking t_here’s no way that is a race. I keep_ prev private and never reference it_. However, with Go, you can use reflection to inspect private variables. This inspection created the read operation that caused a race condition to trigger. This is very reasonable, especially since you can trigger a read of private variables even without reflection by taking the value of a structure

s := Stats{}  
q := s // Will read stats to make a copy inside q

Thankfully, we can’t use reflection to set a private variable. See Value.CanSet for a description of when you can and cannot.

package main

import (
    "bytes"
    "fmt"
    "reflect"
)

func main() {
    buf := bytes.Buffer{}

    // Can inspect a private value
    fmt.Println("Current offset ", reflect.ValueOf(buf).FieldByName("off").Int())

    // Setting it will panic.  See Value.CanSet
    reflect.ValueOf(buf).FieldByName("off").Set(reflect.ValueOf(1))
}

The moral of this little trivia is to remember that private variables can cause read -race errors, via copy and reflection, even if looking at the struct itself it appears safe.