0xDEADBEEF

Home Projects AboutRSS

Common Gotchas in Go

  • go

First thing is first. Happy New Years 🎉🎉

Now that’s out of the way, let’s talk about Go. I recently finished making my first real Go program. It’s called “Fix All Conflicts” or fac for short. It’s an easy-to-use console user interface for fixing git merge conflicts. I made it because I never found a merge tool that was intuitive enough to use.

The process was quite fun and I learned a lot about Go in the process. So, to wrap up my first official foray into Rob Pike’s mystical land of gophers, I decided to write down some of the common “Gotchas!” that any beginning Gopher - me - can run into.

Gophers can be quite aggressive sometimes.

⚠️ #1. Range

The range function is one of the most commonly used functions in Go. Here’s a sample use case of the range function. Note that for some demented reason, we decided to make all the animals in the zoo have 999 legs.

type Animal struct {
	name string
	legs int
}

func main() {
  zoo := []Animal{ Animal{ "Dog", 4 },
                   Animal{ "Chicken", 2 },
                   Animal{ "Snail", 0 },
                 }

  fmt.Printf("-> Before update %v\n", zoo)

  for _, animal := range zoo {
    // 🚨 Oppps! `animal` is a copy of an element 😧
    animal.legs = 999
  }

  fmt.Printf("\n-> After update %v\n", zoo)
}

The above code looks innocent enough. However, you may be surprised to find that the two fmt.Printf() statements yield the same results.

-> Before update [{Dog 4} {Chicken 2} {Snail 0}]
-> After update 🚨🚨🚨 [{Dog 4} {Chicken 2} {Snail 0}]

Lesson

Value property of range (stored here as animal) is a copy of the value from zoo, not a pointer to the value in zoo.

🛠️ The Fix

In order to modify an element within the array, we must change the element via its pointer.

for idx, _ := range zoo {
  zoo[idx].legs = 999
}

This may look quite trivial but you may be surprised to find this as a one of the most common source of bugs; at least for me!

» Go playground #1 for you to play around in

⚠️ #2. The … thingy

You may have used the keyword in the C programming language to create a variadic function; variadic function is a function that takes a variable number or type of arguments.

In C, you have to successively call the va_arg macro in order to access the optional arguments. And if you use the variadic argument in any other way, the compiler will throw an error.

int add_em_up (int count,...) {
  ...
  va_start (ap, count);         /* Initialize the argument list */
  for (i = 0; i < count; i++)
      sum += va_arg(ap, int);   /* Get the next argument value */
  va_end (ap);                  /* Clean up */
  return sum
}

In Go however, things are similar but quite different at the same time. Here is a variadic function myFprint in Go. Notice how the variadic argument a is being used.

func myFprint(format string, a ...interface{}) {
  if len(a) == 0 {
    fmt.Printf(format)
  } else {
    // ⚠️ `a` should be `a...`
    fmt.Printf(format, a)
    // ✅
    fmt.Printf(format, a...)
  }
}

func main() {
    myFprint("%s : line %d\n", "file.txt", 49)
}
[file.txt %!s(int=49)] : line %!d(MISSING)
file.txt : line 49

You’d think that the compiler would throw an error here for using the variadic parameter a in a wrong way. But notice how fmt.Sprintf just used the first argument in a without throwing a fit.

Lesson

In Go, variadic parameters are converted to slices by the compiler

This means that the variadic argument a is in fact, just a slice. Because of this, the code below is completely valid.

// `a` is just a slice!
for _, elem := range a {
    fmt.Println(elem)
}

🛠️ The Fix

Remember to type ALL THREE DOTS whenever using variadic parameters!

» Go playground #2 for you to play around in

⚠️ #3. Slicing

If you have done your fair share of slicing in Python, you may remember that slicing in Python gives you a new list with just the references to the elements copied over. This property allows for code like this in Python.

a = [1, 2, 3]
b = a[:2]			# 👀 a completely new list!
b[0] = 999
>>> a
[1, 2, 3]
>>> b
[999, 2]

However if you try the same thing in Go, you get something else.

func main() {
  data := []int{1,2,3}
  slice := data[:2]
  slice[0] = 999

  fmt.Println(data)
  fmt.Println(slice)
}
[999 2 3]
[999 2]

Lesson

In Go, a slice shares the same backing array and capacity as the original. So if you change an element in the slice, the original contents are modified as well.

🛠️ The Fix

If you want to get an independent slice, you have two options.

// Option #1
// appending elements to a nil slice
// `...` changes slice to arguments for the variadic function `append`
a := append([]int{}, data[:2]...)

// Option #1
// Create slice with length of 2
// copy(dest, src)
a := make([]int, 2)
copy(a, data[:2])

And according to StackOverflow, the append option is slightly faster than the make. + copy option!

» Go playground #3 for you to play around in

Hacker News thread 🤩