These are mistakes that I’ve made writing Go. Although these might not cause any sort of error but they can potentially affect the software. 1. Inside Loop There are several ways to make a mess inside a loop which you need to be aware of. 1.1 Using reference to loop iterator variable Due to efficiency, the loop iterator variable is a single variable that takes different values in each loop iteration. It might lead to unwitting behavior. in := [] { , , } out []* _, v := in { out = (out, &v) } fmt.Println( , *out[ ], *out[ ], *out[ ]) fmt.Println( , out[ ], out[ ], out[ ]) int 1 2 3 var int for range append "Values:" 0 1 2 "Addresses:" 0 1 2 The result will be: Values: 3 3 3 Addresses: 0xc000014188 0xc000014188 0xc000014188 As you can see all elements in slice are 3. It is actually easy to explain why this did happen: in every iteration we append the address of to the slice. As mentioned before is a single variable which takes new values in each iteration. So as you can see in second line of the output, addresses are the same and all of them are pointing to the same value. out v out v The simple fix is to copy the loop iterator variable into a new variable: in := [] { , , } out []* _, v := in { v := v out = (out, &v) } fmt.Println( , *out[ ], *out[ ], *out[ ]) fmt.Println( , out[ ], out[ ], out[ ]) int 1 2 3 var int for range append "Values:" 0 1 2 "Addresses:" 0 1 2 The new output: Values: 1 2 3 Addresses: 0xc0000b6010 0xc0000b6018 0xc0000b6020 The same issue can be found in the loop iteration variable being used in a Goroutine. list := [] { , , } _, v := list { { fmt.Printf( , v) }() } int 1 2 3 for range go func () "%d " Output will be: 3 3 3 It can be fixed using the very same solution mentioned above. Note that without running the function with Goroutine, the code runs as expected. 1.2 Calling WaitGroup.Wait in loop This mistake can be made using a shared variable of type , as shown in the below code the at line 7 can only be unblocked, when at line 5 is invoked times, since it is used as parameter to call at line2. However, the is called inside the loop, so that it blocks Goroutine creation at line 4 in the next iterations. The simple solution is to move the invocation of out from the loop. WaitGroup Wait() Done() len(tasks) Add() Wait() Wait() wg sync.WaitGroup wg.Add( (tasks)) _, t := tasks { { group.Done() }(t) } group.Wait() var len for range go func (t *task) defer // group.Wait() 1.3 Using defer in loop does not execute until the returns. You shouldn’t use in a loop unless you’re sure what you are doing. defer function defer mutex sync.Mutex Person { Age } persons := ([]Person, ) _, p := persons { mutex.Lock() p.Age = mutex.Unlock() } var type struct int make 10 for range // defer mutex.Unlock() 13 In the above example, if you use line 8 instead of line 10, next iteration can not hold mutex lock because the lock is already in use and the loop blocks forever. If you really need to use defer inside loop you might wanna entrust another function to do the work. mutex sync.Mutex Person { Age } persons := ([]Person, ) _, p := persons { { mutex.Lock() mutex.Unlock() p.Age = }() } var type struct int make 10 for range func () defer 13 But, sometimes using in loop may become handy. So you really need to know what you are doing. defer Go suffers no fools 2. Sending into an unguaranteed channel You can send values into channels from one Goroutine and receive those values into another Goroutine. By default, sends and receives block until the other side is ready. This allows Goroutines to synchronize without explicit locks or condition variables. { ch := ( obj, ) { obj := do() ch <- result } () { result = <- ch : result <- time.After(timeout): } } func doReq (timeout time.Duration) obj // ch :=make(chan obj) make chan 1 go func () select case return case return nil Let’s check the above code. The function creates a child Goroutine at line 4 to handle a request which is a common practice in Go server programs. doReq The child Goroutine executes function and sends the result back to the parent through channel ch at line 6. The child will block at line 6 until the parent receives the result from ch at line 9. Meanwhile, the parent will block at until either when the child sends the result to (line 9) or when a timeout happens (line 11). do select ch If timeout happens earlier, the parent will return from func at line 12, and no one else can receive the result from any more, this results in the child being blocked forever. The fix is to change from an unbuffered channel to a buffered one, so that the child Goroutine can always send the result even when the parent has exit. doReq ch ch Another fix can be using a statement with empty default case at line 6 so if no Goroutine receiving the , default will happen. Although this solution might not work always. select ch ... { ch <- result: : } ... select case default 3. Not using interfaces Interfaces can make code more flexible. It is a way to bring polymorphism in code. Interface allows you to ask for a set of behaviors instead of a particular type. Not using interfaces might not cause any error but it can lead to a less simple, less flexible, and less extensible code. Among many interfaces, and might be the most beloved ones. io.Reader io.Writer Reader { Read(p [] ) (n , err error) } Writer { Write(p [] ) (n , err error) } type interface byte int type interface byte int These interfaces can be very powerful. Let’s assume you are going to write an object into a file, so you defined a method: Save func (o *obj) Save (file os.File) error What if you need to write into a next day? you don’t want to define a new method. Do you? So use . http.ResponseWriter io.Writer func (o *obj) Save (w io.Writer) error Also, an important note you should know is that always ask for behaviors you are going to use. In the above example, asking for an can work as well but it’s not a best practice when the only method you are going to use is . The bigger the interface, the weaker the abstraction. io.ReadWriteCloser Write So most of the time you better stay with behaviors instead of concrete type. 4. Bad ordered struct This mistake can not cause any error as well but it can cause more memory usage. BadOrderedPerson { Veteran Name Age } OrderedPerson { Name Age Veteran } type struct bool // 1 byte string // 16 byte int32 // 4 byte type struct string int32 bool It seems like both types have the same size of 21 bytes, but the result shows something totally different . Compiling code using , the type allocates 32 bytes while type does 24 bytes. Why? Well, the reason is . In 64 bit architecture memory allocates consecutive packet of 8 bytes. Padding need to be added can be calculate by: GOARCH=amd64 BadOrderedPerson OrderedPerson Data structure alignment padding = (align - (offset mod align)) mod align aligned = offset + padding = offset + ((align - (offset mod align)) mod align) BadOrderedPerson { Veteran _ [ ] Name Age _ {} } OrderedPerson { Name Age Veteran _ {} } type struct bool // 1 byte 7 byte // 7 byte: padding for alignment string // 16 byte int32 // 4 byte struct // to prevent unkeyed literals // zero sized values, like struct{} and [0]byte occurring at // the end of a structure are assumed to have a size of one byte. // so padding also will be addedd here as well. type struct string int32 bool struct It can lead to a performance issue when you have a big frequently used type. But don’t worry, you don’t have to take care of all your structs manually. Using you can easily check your code for this issue. maligned 5. Not using race detector in test Data races cause mysterious failures, often long after the code has been deployed to production. Because of that those are among the most common and hardest to debug types of bugs in concurrent systems. To help distinguish those kind of bugs, Go 1.1 introduced a built-in data race detector. It can be used simply adding flag. -race $ go -race pkg $ go run -race pkg.go $ go build -race $ go install -race pkg test # to test the package # to run the source file # to build the package # to install the package When race detector enabled, compiler will record when and how the memory was accessed within the code, while the watches for unsynchronized accesses to shared variables. runtime When a data race has been found, race detector prints a report which contains stack traces for conflicting accesses. Here is an example: WARNING: DATA RACE Read by goroutine 185: net.(*pollServer).AddFD() src/net/fd_unix.go:89 +0x398 net.(*pollServer).WaitWrite() src/net/fd_unix.go:247 +0x45 net.(*netFD).Write() src/net/fd_unix.go:540 +0x4d4 net.(*conn).Write() src/net/net.go:129 +0x101 net.func·060() src/net/timeout_test.go:603 +0xaf Previous write by goroutine 184: net.setWriteDeadline() src/net/sockopt_posix.go:135 +0xdf net.setDeadline() src/net/sockopt_posix.go:144 +0x9c net.(*conn).SetDeadline() src/net/net.go:161 +0xe3 net.func·061() src/net/timeout_test.go:616 +0x3ed Goroutine 185 (running) created at: net.func·061() src/net/timeout_test.go:609 +0x288 Goroutine 184 (running) created at: net.TestProlongTimeout() src/net/timeout_test.go:618 +0x298 testing.tRunner() src/testing/testing.go:301 +0xe8 Last words The only real mistake is one from which we learn nothing. Also published behind a paywall on: https://medium.com/swlh/5-mistakes-ive-made-in-go-75fb64b943b8