visit
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.
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])
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])
Values: 1 2 3
Addresses: 0xc0000b6010 0xc0000b6018 0xc0000b6020
list := []int{1, 2, 3}
for _, v := range list {
go func() {
fmt.Printf("%d ", v)
}()
}
3 3 3
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()
}
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
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:
}
...
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.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 . 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{}
}
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
Also published behind a paywall on: //medium.com/swlh/5-mistakes-ive-made-in-go-75fb64b943b8