Wiki
Go Common Pitfalls
Table of Contents
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.