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.
There are several ways to make a mess inside a loop which you need to be aware of.
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 := []int{1, 2, 3}
var out []*int
for _, v := range in {
out = append(out, &v)
}
fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])
The result will be:
Values: 3 3 3
Addresses: 0xc000014188 0xc000014188 0xc000014188
As you can see all elements in
out
slice are 3. It is actually easy to explain why this did happen: in every iteration we append the address of v
to the out
slice. As mentioned before v
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.The simple fix is to copy the loop iterator variable into a new variable:
in := []int{1, 2, 3}
var out []*int
for _, v := range in {
v := v
out = append(out, &v)
}
fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[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 := []int{1, 2, 3}
for _, v := range list {
go func() {
fmt.Printf("%d ", v)
}()
}
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.
This mistake can be made using a shared variable of type
WaitGroup
, as shown in the below code the Wait()
at line 7 can only be unblocked, when Done()
at line 5 is invoked len(tasks)
times, since it is used as parameter to call Add()
at line2. However, the Wait()
Wait()
out from the loop.var wg sync.WaitGroup
wg.Add(len(tasks))
for _, t := range tasks {
go func(t *task) {
defer group.Done()
}(t)
// group.Wait()
}
group.Wait()
defer
does not execute until the function returns. You shouldn’t use defer
in a loop unless you’re sure what you are doing.var mutex sync.Mutex
type Person struct {
Age int
}
persons := make([]Person, 10)
for _, p := range persons {
mutex.Lock()
// defer mutex.Unlock()
p.Age = 13
mutex.Unlock()
}
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.
var mutex sync.Mutex
type Person struct {
Age int
}
persons := make([]Person, 10)
for _, p := range persons {
func() {
mutex.Lock()
defer mutex.Unlock()
p.Age = 13
}()
}
But, sometimes using
defer
in loop may become handy. So you really need to know what you are doing.Go suffers no fools
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.
func doReq(timeout time.Duration) obj {
// ch :=make(chan obj)
ch := make(chan obj, 1)
go func() {
obj := do()
ch <- result
} ()
select {
case result = <- ch :
return result
case<- time.After(timeout):
return nil
}
}
Let’s check the above code. The
doReq
function creates a child Goroutine at line 4 to handle a request which is a common practice in Go server programs. The child Goroutine executes
do
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 select
until either when the child sends the result to ch
(line 9) or when a timeout happens (line 11). If timeout happens earlier, the parent will return from
doReq
func at line 12, and no one else can receive the result from ch
any more, this results in the child being blocked forever. The fix is to change ch
from an unbuffered channel to a buffered one, so that the child Goroutine can always send the result even when the parent has exit. Another fix can be using a
select
statement with empty default case at line 6 so if no Goroutine receiving the ch
, default will happen. Although this solution might not work always....
select {
case ch <- result:
default:
}
...
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,
io.Reader
and io.Writer
might be the most beloved ones.type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
These interfaces can be very powerful. Let’s assume you are going to write an object into a file, so you defined a
Save
method:func (o *obj) Save(file os.File) error
What if you need to write into a
http.ResponseWriter
next day? you don’t want to define a new method. Do you? So use 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
io.ReadWriteCloser
can work as well but it’s not a best practice when the only method you are going to use is Write
. The bigger the interface, the weaker the abstraction.So most of the time you better stay with behaviors instead of concrete type.
This mistake can not cause any error as well but it can cause more memory usage.
type BadOrderedPerson struct {
Veteran bool // 1 byte
Name string // 16 byte
Age int32 // 4 byte
}
type OrderedPerson struct {
Name string
Age int32
Veteran bool
}
It seems like both types have the same size of 21 bytes, but the result
shows something totally different . Compiling code using
GOARCH=amd64
, the BadOrderedPerson
type allocates 32 bytes while OrderedPerson
type does 24 bytes. Why? Well, the reason is Data structure alignment. In 64 bit architecture memory allocates consecutive packet of 8 bytes. Padding need to be added can be calculate by:padding = (align - (offset mod align)) mod align
aligned = offset + padding
= offset + ((align - (offset mod align)) mod align)
type BadOrderedPerson struct {
Veteran bool // 1 byte
_ [7]byte // 7 byte: padding for alignment
Name string // 16 byte
Age 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 OrderedPerson struct {
Name string
Age int32
Veteran 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 maligned you can easily check your code for this issue.
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
-race
flag.$ go test -race pkg # to test the package
$ go run -race pkg.go # to run the source file
$ go build -race # to build the package
$ go install -race pkg # to install the package
When race detector enabled, compiler will record when and how the memory was accessed within the code, while the
runtime
watches for unsynchronized accesses to shared variables.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
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