DEV Community

Cover image for Tricky Golang interview questions. Part 1: Slice Header
Harutyun Mardirossian
Harutyun Mardirossian

Posted on

Tricky Golang interview questions. Part 1: Slice Header

This series is for those who are already familiar with Go and want to prepare for challenging and tricky interview questions. We'll dive deep into advanced topics, best practices, and common pitfalls to ensure you're ready to tackle the toughest questions with confidence.

In each article, we will explore hidden flaws and techniques around standard Go practices, uncovering the nuances and subtleties that can make a big difference in your understanding and application of the language. By examining these less obvious aspects of Go, you'll gain a deeper insight into the language's inner workings and be better prepared to impress in your interviews.

Let’s get started and master the intricacies of Go together!

An example I want to discuss is quite simple and requires knowledge about how arguments are passed in Go and how the append function works.

Question: What is the output of the fmt.Println?

package main

import (
    "fmt"
)

func main() {
    s := make([]int, 0, 2)

    doSomething(s)
    fmt.Println(s)
}

func doSomething(a []int) {
    a = append(a, 1)
}
Enter fullscreen mode Exit fullscreen mode

A quick note here:

In Golang, function arguments are passed by value, which means that a copy of the argument value is created and passed to the function.

This quote is straightforward and self-explaining but there's a nuance. The nuance is how slices are constructed under the hood. You see, slices are a more complex data structure. The slice can be represented as the following struct:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}
Enter fullscreen mode Exit fullscreen mode

Slices have headers containing the length, capacity and the pointer to the underlying array. When we pass a slice as a function argument we actually copy the SliceHeader with all its values. Since the SliceHeader.Data contains a pointer copying this address keeps the underlying array the same for both SliceHeaders. So appending to the copied slice will also modify the original slice. Right?

Let's run the program and check the results:

[Running] go run "main.go"
[]
[Done] exited with code=0 in 1.377 seconds
Enter fullscreen mode Exit fullscreen mode

Let's see what happened here.
Slice diagram
This diagram shows that inside the f.doSomething() we created a new copy of the SliceHeader. The difference between the headers is the length fields, it was 0 and now it's 1. The append function calculates the new length after appending the data. If this length exceeds the current capacity, it reallocates a new slice with double the required capacity, copies the old slice into it, and then appends the new data. Finally, it returns the updated slice. Let's print out the slice headers inside both functions using the unsafe package to make sure:

...
sh := (*SliceHeader)(unsafe.Pointer(&s)) // you must define a struct of the header for this to work
fmt.Println(sh)
...
Enter fullscreen mode Exit fullscreen mode

The output is:

&{1374389592336 0 2} // main
&{1374389592336 1 2} // doSomething
Enter fullscreen mode Exit fullscreen mode

Both headers indeed point to the same underlying array, but because they have different lengths, they are two different slices. So since the SliceHeader inside the f.main() function has a len=0 fmt.Println() function prints an empty slice.

// main             |    // doSomething
--------------------+----------------------
&{                  |    &{
    1374389592336   |        1374389592336
    [0    <-- length  -->    1]
    2               |        2
}                   |    }
Enter fullscreen mode Exit fullscreen mode

So the answer to this interview question is simple:
Program will output an empty slice because the slice inside the main function has a length of 0.


The tricky part doesn't end up here. There is a way to print out the slice values. We can slice the slice inside the f.main() function to the first element and have a valid output. Take a look at this:

package main

import (
    "fmt"
)

func main() {
    s := make([]int, 0, 2)

    doSomething(s)
    fmt.Println(s[:1]) // <-- here I sliced the slice from 1st to 2nd element
}

func doSomething(a []int) {
    a = append(a, 1)
}
Enter fullscreen mode Exit fullscreen mode

And voila, we successfully printed the element from the underlying array.

[Running] go run "main.go"
[1]
[Done] exited with code=0 in 1.377 seconds
Enter fullscreen mode Exit fullscreen mode

This happened because we sliced the slice. Slicing is the process of taking a portion of a slice and creating a new slice from it. Once again we copied the SliceHeader with a new length. In this case, we can slice the original slice inside the f.main() because the capacity of the slice is greater than zero: cap(s) > 0 // <-- true.

Top comments (0)