When there are concurrency problems using Go's built-in map (which is not designed to be safe for concurrent use) and a fatal error happens, the stack traces for each goroutine will only show the access from the goroutine that crashed, and not for the other.

The reason for this seems to be that the second goroutine will have moved on by the time the stack trace is printed. Furthermore, fixing this in the language would have a performance penalty.

To help debug this issue, Go provides a built-in data race detector, which makes it show more obvious what happened. This has a significant performance cost, so it shouldn't be used in production.

Here's an example of a program that will fail in such a way, taken from this GitHub issue:

package main

import (
    "sync"
)

var x map[int]string = make(map[int]string)

func f(s string, wg *sync.WaitGroup) {
    x[0] = s
    wg.Done()
}

func g(s string, wg *sync.WaitGroup) {
    x[1] = s
    wg.Done()
}

func main() {
    for {
        var wg sync.WaitGroup
        wg.Add(2)
        go f("Hello", &wg)
        go g("Playground", &wg)
        wg.Wait()
    }
}

To run this with the race detector, use the -race flag:

$ go run -race main.go
==================
WARNING: DATA RACE
Write at 0x00c000192000 by goroutine 6:
  runtime.mapassign_fast64()
      /usr/local/go/src/runtime/map_fast64.go:92 +0x0
  main.f()
      /usr/src/myapp/main.go:10 +0x51
  main.main·dwrap·1()
      /usr/src/myapp/main.go:23 +0x58

Previous write at 0x00c000192000 by goroutine 7:
  runtime.mapassign_fast64()
      /usr/local/go/src/runtime/map_fast64.go:92 +0x0
  main.g()
      /usr/src/myapp/main.go:15 +0x54
  main.main·dwrap·2()
      /usr/src/myapp/main.go:24 +0x58

Goroutine 6 (running) created at:
  main.main()
      /usr/src/myapp/main.go:23 +0x11c

Goroutine 7 (finished) created at:
  main.main()
      /usr/src/myapp/main.go:24 +0x2d
==================
fatal error: concurrent map writes

goroutine 34336 [running]:
runtime.throw({0x499779, 0x48aad2})
    /usr/local/go/src/runtime/panic.go:1198 +0x71 fp=0xc0001aff30 sp=0xc0001aff00 pc=0x45e5f1
runtime.mapassign_fast64(0x4919e0, 0xc000192000, 0x0)
    /usr/local/go/src/runtime/map_fast64.go:176 +0x2f4 fp=0xc0001aff68 sp=0xc0001aff30 pc=0x43fe54
main.f({0x497d0b, 0x5}, 0x7fd24fe80008)
    /usr/src/myapp/main.go:10 +0x52 fp=0xc0001affa0 sp=0xc0001aff68 pc=0x48aad2
main.main·dwrap·1()
    /usr/src/myapp/main.go:23 +0x59 fp=0xc0001affe0 sp=0xc0001affa0 pc=0x48af19
runtime.goexit()
    /usr/local/go/src/runtime/asm_amd64.s:1581 +0x1 fp=0xc0001affe8 sp=0xc0001affe0 pc=0x486e21
created by main.main
    /usr/src/myapp/main.go:23 +0x11d

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000311cf4)
    /usr/local/go/src/runtime/sema.go:56 +0x25
sync.(*WaitGroup).Wait(0xc000311cf4)
    /usr/local/go/src/sync/waitgroup.go:130 +0xea
main.main()
    /usr/src/myapp/main.go:25 +0x38
exit status 2
make: *** [Makefile:7: run-race] Error 1

Note that the first section of the output describes the detected data race, including the two goroutines responsible for it, and the rest is the normal fatal error output which, in this case, only shows one of the goroutines (line 10, corresponding to func f).

Previous on Go
Mastodon Mastodon