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)
}
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
}
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
Let's see what happened here.
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)
...
The output is:
&{1374389592336 0 2} // main
&{1374389592336 1 2} // doSomething
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
} | }
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)
}
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
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)