Top 10 Go Coding Traps and Tips

  sonic0002        2021-07-03 23:45:51       1,731        0    

Go is currently the most common programming language in cloud development. Though I use it very much in my work, I am still repeating certain mistakes. This article is more a record of these errors, figuring out the causes and solutions so that people who read this article will save themselves time when coming across the same problems.

Let’s cut through to the tips.

Don’t rely on index var in the for loop

The most common mistake we make is that we often create goroutine inside a for loop and rely on the index variable. For example,

package main

import (
  "fmt"
  "sync"
)

func main() {
  var wg sync.WaitGroup
  items := []int{1, 2, 3, 4}
  for index, _ := range items {
    wg.Add(1)
    go func() {
      defer wg.Done()
      fmt.Printf("item:%v\\n", items[index])
    }()
  }
  wg.Wait()
}

//output:
items:4
items:4
items:4
items:4

The code looks just right at first sight, while the output is always 4(the last value in the array). Why?

Because goroutines won’t start before the for loop finishes. And assigned with a new value(0,1,2,3) in each loop, the index is always 3 in the end.

To avoid it, you just stop relying on the index variable. Instead, you can either pass it into the goroutine or copy it to a new variable inside the loop. And the first method is my personal preference.

package main

import (
  "fmt"
  "sync"
)

func main() {
  var wg sync.WaitGroup
  items := []int{1, 2, 3, 4, 5}
  for index, _ := range items {
    wg.Add(1)
    go func(i int) {
      defer wg.Done()
      fmt.Printf("item:%v\\n", items[i])
    }(index)
  }
  wg.Wait()
}

Convert the default JSON number data into int

The code below won’t work.

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	data := []byte(`{"number": 1}`)

	var result map[string]interface{}
	if err := json.Unmarshal(data, &result); err != nil {
		fmt.Println("parse json error:", err)
		return
	}

	n := result["number"].(int) //error
	fmt.Println("number is:", n)
}

The output error is:

panic: interface conversion: interface {} is float64, not int
goroutine 1 [running]:
main.main()
/tmp/sandbox635569672/prog.go:17 +0x252

Obviously, the default type of a number from Unmarshall is not int but float64. We can make it out by continue using float64 and converting it to int later.

n := result["number"].(float64)
in := int(n)
fmt.Println("number is:", in) // output: number is : 1

And I want to introduce a more popular solution, which is wrapping the field into a struct and converting it with json tag.

package main

import (
	"encoding/json"
	"fmt"
)

type Data struct {
   Number int `json:"number"`
}

func main() {
	dataStr := []byte(`{"number": 1}`)
	data := Data{}
	
	if err := json.Unmarshal(dataStr, &data); err != nil {
		fmt.Println("parse json error:", err)
		return
	}

	fmt.Println("number is:", data.Number)
}

Don’t always trust DeepEqual

In Go, we can compare some types of variables without initialization using ==because they have default values.

package main

import (
	"fmt"
)

func main() {
	var n1 int
	var n2 int
	fmt.Println("n1 == n2:", n1 == n2); 
	
	var ch1 <- chan string
	var ch2 <- chan string
	fmt.Println("ch1 == ch2:", ch1 == ch2); 
	
	var i1 interface{}
	var i2 interface{}
	fmt.Println("i1 == i2:", i1 == i2); 
  
  //another supported types: bool, string, float, pointer, array
}

However, there are some we cannot do so even after assigning them with new values. For instance,

package main

import (
	"fmt"
)

func main() {
	var m1 = map[string]int{"a":1, "b": 2}
	var m2 = map[string]int{"b":2, "a":1}
	fmt.Println("m1 == m2:", m1 == m2); 
	
	var b1 = []string{"1"}
	var b2 = []string{"1"}
	fmt.Println("b1 == b2:", b1 == b2); 
	
	var f1 = func() int {return 1}
	var f2 = func() int {return 2}
	fmt.Println("f1 == f2:", f1 == f2); 
}


// output:
./prog.go:10:30: invalid operation: m1 == m2 (map can only be compared to nil)
./prog.go:14:30: invalid operation: b1 == b2 (slice can only be compared to nil)
./prog.go:18:30: invalid operation: f1 == f2 (func can only be compared to nil)

This rule also stands when these different types are assembled into a struct. We will fail to compare the two struct objects directly once one of the struct fields is not comparable.

We overcome it by using the reflect.DeepEqual function. Let’s do the above example.

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var m1 = map[string]int{"a":1, "b": 2}
	var m2 = map[string]int{"b":2, "a":1}
	fmt.Println("m1 == m2:", reflect.DeepEqual(m1, m2)); 
	
	var b1 = []string{"1"}
	var b2 = []string{"1"}
	fmt.Println("b1 == b2:", reflect.DeepEqual(b1, b2)); 
	
	var f1 = func() int {return 1}
	var f2 = func() int {return 2}
	fmt.Println("f1 == f2:", reflect.DeepEqual(f1, f2)); 
}
// output:
m1 == m2: true
b1 == b2: true
f1 == f2: false

Keep in mind that DeepEqual may not work as expected sometimes, especially for array and slice types. See below,

package main

import (
	"fmt"
	"reflect"
	"bytes"
)

func main() {

	var b1 []byte = nil
	b2 := []byte{}
	fmt.Println("b1 == b2:", reflect.DeepEqual(b1, b2)) //prints: b1 == b2: false
	fmt.Println("b1 == b2:", bytes.Equal(b1, b2))       //prints: b1 == b2: true

	v1 := []string{"one", "two"}
	v2 := []interface{}{"one", "two"}
	fmt.Println("v1 == v2:", reflect.DeepEqual(v1, v2))  // prints: v1 == v2: false
}

PS: When comparing two []byte arrays, always use bytes.Equal or bytes.Compare

For slice, it is harder. I cannot find a common library that fixes the error in the above example, thus converting it and comparing them one by one is my only approach.

Don’t modify value reference variables

Updating the pass-by-value reference variables will not change the variables’ original value. Those pass-by-value references include:

  • Function parameters, such as func abc( a int, b string).
  • Struct’s object reference, such as
    type Data struct {
      num int
    }
    func (d Data) setNum() {
      d.num = 8
    }​
  • Value reference in slice for-range loop.
    numbers := []int{1,2,3}
    for _,v := range numbers {
      v = v * 10 // will not change the original slice
    }
    fmt.Println(numbers) // prints: [1 2 3]​

Always use pointers instead of value references if you prefer in-place updates.

Watch out for slice contamination

As one of the most widely used Go data types, there are various interception and extension operations on an original slice, so unexpected results may occur due to the underlying implementation. Let me give you some examples for a better understanding.

package main

import (
	"bytes"
	"fmt"
)

func main() {
	// -------------- No.1 example
	s1 := []int{1, 2, 3}
	fmt.Println(len(s1), cap(s1), s1) //prints 3 3 [1 2 3]

	s2 := s1[1:]                      // s2 still points to the s1 address
	fmt.Println(len(s2), cap(s2), s2) //prints 2 2 [2 3]

	for i := range s2 {
		s2[i] += 20
	}

	fmt.Println(s1) //prints [1 22 23]
	fmt.Println(s2) //prints [22 23]
	// -------------- No.2 example
	name := []byte("name:abc")
	index := bytes.IndexByte(name, ':')
	key := name[:index]
	value := name[index+1:]
	fmt.Println("key:", string(key))     //prints: key: name
	fmt.Println("value:", string(value)) //prints: value: abc

	key = append(key, "_str"...) // key and value are still share part of the memory
	name = bytes.Join([][]byte{key, value}, []byte{':'})

	fmt.Println("key:", string(key))     //prints: key: name_str
	fmt.Println("value:", string(value)) //prints: value: str (not ok)

}

The solution is very simple and always remember to copy a new slice.

package main

import (
	"bytes"
	"fmt"
)

func main() {
	// -------------- No.1 example
	s1 := []int{1, 2, 3}
	fmt.Println(len(s1), cap(s1), s1) //prints 3 3 [1 2 3]

	s2 := make([]int, 2) // s2 is totally new
	copy(s2, s1[1:])
	fmt.Println(len(s2), cap(s2), s2) //prints 2 2 [2 3]

	for i := range s2 {
		s2[i] += 20
	}

	fmt.Println(s1) //prints [1 2 3]
	fmt.Println(s2) //prints [22 23]
	// -------------- No.2 example
	name := []byte("name:abc")
	index := bytes.IndexByte(name, ':')
	key := name[:index:index] //Very efficient way to create a new slice
	value := name[index+1:]
	fmt.Println("key:", string(key))     //prints: key: name
	fmt.Println("value:", string(value)) //prints: value: abc

	key = append(key, "_str"...) // key and value are still share part of the memory
	name = bytes.Join([][]byte{key, value}, []byte{':'})

	fmt.Println("key:", string(key))     //prints: key: name_str
	fmt.Println("value:", string(value)) //prints: value: abc
}

This blog about Go slice is worth your reading.

Close waitGroup in defer

You can find WaitGroup in most of Go’s open-source libraries because it is super helpful.

But we mistreat it sometimes, which results in serious issues as below.

func main() {
  items := []int{1, 2, 3, 4, 5}
  var wg sync.WaitGroup
  for index, _ := range items {
    wg.Add(1)
    go func(i int) {
      _, err := Do(i) // some logic returns an error
      if err != nil {
        log.Infof("err message:%v\\n", err)
        return
      }
      wg.Done()
    }(i)
  }
  wg.Wait()
}

Have you ever noticed where this code goes wrong?

If the Do function returns an error, the goroutine will end directly without calling wg.Done(), and eventually blocks the waitGroup.

To prevent such mistakes, always call defer wg.Done() at the beginning of your goroutine code if you are using waitGroup.

func main() {
  items := []int{1, 2, 3, 4, 5}
  var wg sync.WaitGroup
  for index, _ := range items {
    wg.Add(1)
    go func(i int) {
      defer wg.Done()
      _, err := Do(i) // some logic returns an error
      if err != nil {
        log.Infof("err message:%v\\n", err)
        return
      }
    }(i)
  }
  wg.Wait()
}

Recover from goroutine

To save ourselves from missing the error info and details, we always use recover. Otherwise, you may have a tough time debugging your goroutine code when it goes wrong.

Nevertheless, it can also go wrong sometimes. Looking at the code below, you will find that this code fails since the recover is called outside of goroutine.

package main

import (
	"fmt"
	"time"
)

func main() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Printf("%v\n", err)
    }
  }()
  go func() {
    panic("error")
  }()

  time.Sleep(2 * time.Second)
}

The output is

panic: error
goroutine 34 [running]:
main.main.func2()
/tmp/sandbox430672375/prog.go:15 +0x39
created by main.main
/tmp/sandbox430672375/prog.go:14 +0x57

The correct usage is like this.

package main

import (
	"fmt"
	"time"
)

func main() {
 
  go func() {
   defer func() {
    if err := recover(); err != nil {
      fmt.Printf("%v\n", err)
    }
   }()
    panic("error")
  }()

  time.Sleep(2 * time.Second)
}

But we can not add recover to all the goroutine in this way, which seems “stupid.”😂 Then how does Kubernetes source handle the below situation?

var PanicHandlers = []func(interface{}){logPanic}
func HandleCrash(additionalHandlers ...func(interface{})) {deads2k, 4 years ago: • start the apimachinery repo
	if r := recover(); r != nil {
		for _, fn := range PanicHandlers {
			fn(r)
		}
		for _, fn := range additionalHandlers {
			fn(r)
		}
		if ReallyCrash {
			// Actually proceed to panic.
			panic(r)
		}
	}
}

// logPanic logs the caller tree when a panic occurs (except in the special case of http.ErrAbortHandler).
func logPanic(r interface{}) {
	if r == http.ErrAbortHandler {
		return
	}

	const size = 64 << 10
	stacktrace := make([]byte, size)
	stacktrace = stacktrace[:runtime.Stack(stacktrace, false)]
	if _, ok := r.(string); ok {
		klog.Errorf("Observed a panic: %s\n%s", r, stacktrace)
	} else {
		klog.Errorf("Observed a panic: %#v (%v)\n%s", r, r, stacktrace)
	}
}

It even includes the stack trace messages.

Now, goroutines in Kubernetes source call defer runtime.HandleCrash() in one line.

Treat channel carefully

Channel is a very powerful lightweight message queue in Go, and it is also where bugs usually occur. There are several Don’ts to follow to stay away from bugs.

  • Don’t send data to a closed channel
  • Don’t close a nil channel
  • Don’t close a channel twice

An example for the first point,

package main

import (
  "context"
  "fmt"
  "time"
)

func main() {
  ch := make(chan struct{})
  ctx, _ := context.WithTimeout(context.Background(), time.Second)
  go func() {
    time.Sleep(1 * time.Second)
    ch <- struct{}{}
    fmt.Println("end goroutine")
  }()

  select {
  case <-ch:
    fmt.Println("receive msg")
  case <-ctx.Done():
    fmt.Println("timeout")
  }
  time.Sleep(3 * time.Second)
}

Here the goroutine waits two seconds, so the context ends before the goroutine can send any data to the channel, then the goroutine is blocked. The output is,

./prog.go:11:8: the cancel function returned by context.WithTimeout should be called, not discarded, to avoid a context leak
timeout

To fix it, just simply change the channel’s buffer to 1, ch := make(chan struct{}, 1) . Then the output would be:

timeout
end goroutine

Cannot locate the address of Map’s value

In the previous tip, I mentioned that value reference change would not affect the original variable value. Here I will talk about how manipulating its value leads to compilation errors.

For Map, let’s see some illustrations.

package main

import (
	"fmt"
)

type Data struct {
	Number int
}

func (d *Data) print() {
	fmt.Println("n:", d.Number)
}

type Print interface {
	print()
}

func main() {
	d := Data{}
	d.Number = 1
	m := map[string]Data{"a": d}
	m["a"].Number = 2 // error: cannot assign to struct field m["a"].Number in map

	fmt.Println("m:", m)

	m["a"].print() // error: cannot call pointer method on m["a"]
}

The root is easy to see in the error message as well.

cannot take the address of m[“a”]

Obviously, you cannot update the object without knowing its address.

There are usually two alternatives to solve it.

  • Assign the value to a temp variable and reassign it back after the change
  • Use pointer types
package main

import (
	"fmt"
)

type Data struct {
	Number int
}

func (d *Data) print() {
	fmt.Println("n:", d.Number)
}

type Print interface {
	print()
}

func main() {
	d := Data{}
	d.Number = 1
	m := map[string]Data{"a": d}
	tmp := m["a"] // Use a temp variable
	tmp.Number = 2
	m["a"] = tmp
	fmt.Println("m:", m) // print: map[a:{2}]

	n := map[string]*Data{"a": &d} // use pointer
	n["a"].print()                 // print: n: 1
}

Know the difference between nil interface{} and interface{} is nil

It is common to use interface{}as return types in functions since the default value of an interface{} is nil. But it is very tricky when you assign another nil value object to it.

package main

import (
	"fmt"
)

func assign(s *string) interface{} {
	return s
}

func main() {
	var s *string
	var i interface{}

	fmt.Println(s, s == nil) //prints:  true
	fmt.Println(i, i == nil) //prints:  true

	i = assign(s)
	fmt.Println(i, i == nil) //prints:  false
}

We have to be careful when returning a interface{} type. The right thing is always to return explicitly nil.

package main

import (
	"fmt"
)

func assign(s *string) interface{} {
	if s == nil {
		return nil // explicit
	}
	return s
}

func main() {
	var s *string
	var i interface{}

	fmt.Println(s, s == nil) //prints:  true
	fmt.Println(i, i == nil) //prints:  true

	i = assign(s)
	fmt.Println(i, i == nil) //prints:  true
}

Summary

Go is currently one of the most efficient and engineered languages that facilitate programming. However, we will be in a debugging mess if we make the above mistakes. Keeping a list of these traps and their correspondent solutions in mind and comprehending Go’s underlying implementation will help us more proficient in the Go world.

Thanks for reading!

Note: The post is authorized by original author to republish on our site. Original author is Stefanie Lai who is currently a Spotify engineer and lives in Stockholm, original post is published here.

TIPS  GOLANG  NIL INTERFACE 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

Tip for better Android security