Don’t Make These 5 Golang Mistakes | Hacker Noon

@josieAli Josie

Yet Another Software Engineer

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.

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 := []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:

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

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()

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

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()

1.3 Using defer in loop

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

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.

read original article here