Before you go, check out these stories!

0
Hackernoon logoDon't Make These 5 Golang Mistakes by@josie

Don't Make These 5 Golang Mistakes

Author profile picture

@josieAli Josie

Software Engineer at Harlem Next

These are mistakes that I鈥檝e 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 := []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.

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鈥檛 use
defer
in a loop unless you鈥檙e 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

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.

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鈥檚 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:
}
...

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,

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鈥檚 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鈥檛 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鈥檚 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.

4. Bad ordered struct

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鈥檛 worry, you don鈥檛 have to take care of all your structs manually. Using maligned you can easily check your code for this issue.

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

-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

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

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.