Wiki

Go Common Pitfalls

Memory Model

Go is not sequentially consistent.

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
    var s string
    var done bool

    go func() {
        s = "hello"
        done = true
    }()

    for !done {}
    // Behavior undefined, possible to print "".
    fmt.Println(s)
}

If you need ordering constraint, establish happens-before relationship as one of the following:

  • If p imports q, q’s init happens before p’s.
  • Package main’s init happens before main.main
  • The go statement happens before the execution of the created goroutine
  • A send (or close) on a channel happens before the receive
  • Unlock happens before subsequent Lock

Deferred Calls

log.Fatal or os.Exit does not respect deferred calls whereas t.Fatal does, where t is a testing.T.

In-line deferred statement is evaluated at the time of calling defer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
    s := "old"
    defer fmt.Println("defer inline", s)
    defer func() {
        fmt.Println("defer func", s)
    }()
    s = "new"
    fmt.Println(s)
}
/*
OUTPUT:
new
defer func new
defer inline old
*/

Deferred calls are executed after return

1
2
3
4
5
6
7
8
9
10
func str() (s string) {
    defer func() {
        s = "prefix-" + s
    }()
    return "hello"
}

func main() {
    fmt.Println(str()) // prints "prefix-hello"
}

Defer within loops hold resources for too long

1
2
3
4
5
6
7
8
9
10
func main() {
    var someChannel chan string
    for filepath := range someChannel {
        f, err := os.Open(filepath)
        if err != nil {
            log.Fatal(err)
        }
        defer f.Close()
    }
}

Do this instead.

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
    var someChannel chan string
    for filepath := range someChannel {
        func(filepath string) {
            f, err := os.Open(filepath)
            if err != nil {
                log.Fatal(err)
            }
            defer f.Close()
        }(filepath)
    }
}

Defer cleanup as soon as possible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func CopyFile(dstName, srcName string) error {
    src, err := os.Open(srcName)
    if err != nil {
        return err
    }

    dst, err := os.Create(dstName)
    if err != nil {
        return err
    }

    _, err = io.Copy(dst, src)
    dst.Close()
    src.Close()
    return err
}

The above code will not close src if os.Create(dstName) failed. Instead of adding src.Close() before every return, use defer to cleanup as soon as you can. Similarly, defer unlock as soon as you can.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func CopyFile(dstName, srcName string) error {
    src, err := os.Open(srcName)
    if err != nil {
        return
    }
    defer src.Close()

    dst, err := os.Create(dstName)
    if err != nil {
        return
    }
    defer dst.Close()

    _, err = io.Copy(dst, src)
    return err
}

Array and Slice

Array is a value type. Slice is a reference type. Be careful when you pass them to other functions. Internally, slice is defined as the following

1
2
3
4
5
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

To duplicate a slice

1
2
3
4
5
6
s := []string{"hello", "world"}
s2 := make([]string, len(s))
copy(s2, s)

// or this
s2 = append([]string{}, s...)

Be careful with make

1
2
3
4
5
6
7
8
func main() {
    // func make([]T, len, cap) []T, where cap is optional.
    s := make([]int, 3)
    s = append(s, 1)
    s = append(s, 2)
    s = append(s, 3)
    fmt.Println(s) // [0 0 0 1 2 3]
}

Slicing retains the entire underlying array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
    prefixCache := make(map[string][]byte)

    for filepath := range someChannel {
        bytes, err := ioutil.ReadFile(filepath)
        if err != nil {
            log.Fatal(err)
        }
        // This will not release the underlying array of `bytes`.
        prefixCache[filepath] = bytes[:8]
        // Instead, let's create a smaller copy.
        prefixCache[filepath] = append([]byte{}, bytes[:8]...)
    }
}

Interface Holding Nil Is Not Nil

1
2
3
4
5
6
7
func main() {
    var a, b interface{}
    fmt.Println(a == nil) // true
    var p *int = nil
    b = p
    fmt.Println(b == nil) // false
}

Internally, Go interface is represented as

1
2
3
4
5
6
7
type iface struct {
    // tab holds the type of the interface and
    // the type of the `data`.
    tab  *itab
    // data points to the value held by the interface.
    data unsafe.Pointer
}

In Go, an interface is nil only if both its type and value are nil. In the example above, b = (*int)(nil) means b’s type is not nil.

This behavior surprises people the most in error, which is an interface. See Why is my nil error value not equal to nil?.

1
2
3
4
5
6
7
func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = ErrBad
    }
    return p // Always return a non-nil error.
}

Return an explicit nil instead.

1
2
3
4
5
6
func returnsError() error {
	if bad() {
		return ErrBad
	}
	return nil
}

Goroutine

Wait group

When main goroutine exits, everything dies.

1
2
3
4
func main() {
    go println("hello")
}
// May not print hello at all.

Instead, use WaitGroup as barrier.

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
    var wg sync.WaitGroup

    // Important that Add happens before starting goroutine.
    wg.Add(1)

    go func() {
        defer wg.Done()
        println("hello")
    }()

    wg.Wait()
}

Scheduling and preemption

Go version 1.14 introduced asynchronous preemption, so that loops without function calls no longer potentially deadlock the scheduler or significantly delay garbage collection.

Previously, goroutines are only context switched when

  • blocked on syscalls, channels, locks, or sleep
  • the goroutine has to grow its stack
  • calling runtime.Gosched() directly

Hence, a goroutine calling for {} will occupy the processor forever.