Golang Grammar Quiz vol.1: Slice

I am Douglas Takeuchi, a Tokyo-based software engineer with a background in fintech and a particular expertise in developing prepaid card systems using Golang. I also have substantial experience in Python and Javascript, making me a versatile and skilled programmer.
Born in Tokyo, Japan, in 1998, I now reside in the bustling Tokyo metropolitan area. I hold a bachelor's degree in social science, which has provided me with a well-rounded perspective in my career.
Beyond my professional work, I'm a dedicated musician and language enthusiast. I've been an active musician since my university days, and my music has resonated with audiences worldwide, amassing thousands of plays.
One of my notable projects is the creation of a web application designed for university students to seek advice and guidance on various aspects of university life and their future career paths, similar to Yahoo Answers.
In the world of coding, I feel most comfortable in Golang. I place a strong emphasis on achieving goals as a team, often coming up with collaborative solutions for success. This might involve organizing workshops to delve deeper into new technologies and collectively improve our skills.
As a software engineer, I bring creativity, problem-solving skills, and a determination to excel. My commitment to my craft and the "Fake it till you make it" mentality drive my continuous growth and success.
"Fake it till you make it" is my guiding principle, encouraging me to step out of my comfort zone, take on challenges, and learn from every experience, ultimately propelling me toward success.
Yesterday, I came across a recording from the 2018 GopherCon (GopherCon 2018: Jon Bodner - Go Says WAT). In this presentation, Jon Bodner delves into some of the brain-twisting behaviors of Golang.
While the content was intriguing, I must admit that I struggled to comprehend a significant portion of his explanations because he speaks very fast in the video.
In this post, I aim to break down his specific explanation about slices. If you, too, found it challenging to grasp the content from the video, perhaps this post can offer you a clearer understanding.
Even if you haven't watched the video yet, that's perfectly fine – let's explore the fascinating behaviors of Golang together.
Question 1
In Golang, a slice is a reference type, which is like a pointer, and can be modified by the called function, as shown below:
package main
import "fmt"
func main() {
s := []int{1, 3, 5}
multiplyBy3(s)
fmt.Println(s)
}
func multiplyBy3(s []int) {
for i, v := range s {
s[i] = v * 3
}
}
// Output:
// [3, 9, 15]
But what happens if we append some numbers to the slice in the called function?
package main
import "fmt"
func main() {
a := []int{1, 3, 5}
appendNums(a)
fmt.Println(a)
}
func appendNums(s []int) {
s = append(s, 2, 4)
}
What do you think the output of the above code is?
A. Does not compile
B. Panics
C. Prints [1, 3, 5]
D. Prints [1, 3, 5, 2, 4]
Answer and Explanation
The answer is C. Prints [1, 3, 5].
We can see what's going on by printing the address of the slice and its elements.
package main
import "fmt"
func main() {
s := []int{1, 3, 5}
fmt.Printf("before appendNums: \taddress:%p\tcap:%d\tvalue:%v\n", s, cap(s), s)
appendNums(s)
fmt.Printf("after appendNums: \taddress:%p\tcap:%d\tvalue:%v\n", s, cap(s), s)
}
func appendNums(s []int) {
fmt.Printf("start of appendNums: \taddress:%p\tcap:%d\tvalue:%v\n", s, cap(s), s)
s = append(s, 2, 4)
fmt.Printf("end of appendNums: \taddress:%p\tcap:%d\tvalue:%v\n", s, cap(s), s)
}
// Output:
// before appendNums: address:0xc000112000 cap:3 value:[1 3 5]
// start of appendNums: address:0xc000112000 cap:3 value:[1 3 5]
// end of appendNums: address:0xc000126000 cap:6 value:[1 3 5 2 4]
// after appendNums: address:0xc000112000 cap:3 value:[1 3 5]
Evidnetly, at the end of the appendNums function, the address of the variable s differs from the one at the beginning of the function.
This is because the built-in append function creates a new slice with double the capacity of the original slice and copies the elements of the original slice to the new slice. Consequently, the original slice remains unmodified.
Question 2
We've just learned that a new slice gets created when we append elements to a slice that doesn't have enough capacity.
But what if we pass a slice with enough capacity to the called function?
package main
func main() {
s := make([]int, 0, 10) // create a slice with length 0, capacity 10
s = append(s, 1, 2, 3)
appendNums(s)
fmt.Println(s)
}
func appendNums(s []int) {
s = append(s, 7, 8)
}
Which of the following options will be the output of the above code?
A. Does not compile
B. Panics
C. Prints [1, 2, 3]
D. Prints [1, 2, 3, 7, 8]
Answer and Explanation
The answer is C. Prints [1, 2, 3].
To understand why, let's print the address of the slice and its elements again.
package main
import "fmt"
func main() {
s := make([]int, 0, 10) // create a slice with length 0, capacity 10
s = append(s, 1, 2, 3)
fmt.Printf("before appendNums: \taddress:%p\tcap:%d\tvalue:%v\n", s, cap(s), s)
appendNums(s)
fmt.Printf("after appendNums: \taddress:%p\tcap:%d\tvalue:%v\n", s, cap(s), s)
}
func appendNums(s []int) {
fmt.Printf("start of appendNums: \taddress:%p\tcap:%d\tvalue:%v\n", s, cap(s), s)
s = append(s, 7, 8)
fmt.Printf("end of appendNums: \taddress:%p\tcap:%d\tvalue:%v\n", s, cap(s), s)
}
// Output:
// before appendNums: address:0xc000018050 cap:10 value:[1 2 3]
// start of appendNums: address:0xc000018050 cap:10 value:[1 2 3]
// end of appendNums: address:0xc000018050 cap:10 value:[1 2 3 7 8]
// after appendNums: address:0xc000018050 cap:10 value:[1 2 3]
It might be confusing to see they share the same address, yet the slice is not modified after appendNums, isn't it?
I've mentioned that the slice object can be treated like a pointer, but it's not exactly the same as a pointer.
In essence, what appears to be the pointer of the slice is not the pointer of the slice itself, but the pointer of the underlying array.
Let me explain a bit more.
First, let's examine the definition of the slice object.
type slice struct {
array unsafe.Pointer
len int
cap int
}
A slice is composed of three fields: array, len, and cap.
It's clear that len and cap hold the length and capacity of the slice, while array is the unsafe pointer to the underlying array. Using the unsafe.Pointer type for array allows slices to hold any type of elements.
Golang is a call-by-value language (not call-by-reference), meaning it always copies the value of the argument to the parameter of the called function.
Consequently, it is possible to modify the value from the original slice because the address of the underlying array is shared between the original slice and the slice in the called function.
However, when we add elements to the slice in the called function, the addresses of the added elements are no longer shared with the original slice simply because the original slice does not have a reference to the added elements.
Let's examine one more piece of code to understand this more clearly.
package main
import "fmt"
func main() {
s := make([]int, 0, 10)
s = append(s, 1, 2, 3)
appendNums(s)
fmt.Println(s)
}
func appendNums(s []int) {
s = append(s, 7, 8)
s[1] = 100 // added line
}
// Output:
// [1 100 3]
As you can see, the value of the second element of the original slice is modified from 2 to 100 because the address of the second element is shared between the original slice and the slice in the called function. However, the original slice does not have references to the added elements(7 and 8), so 7 and 8 are not printed at the end.
If you want to both modify and lengthen the original slice, you can pass the pointer of the slice to the called function.
package main
import "fmt"
func main() {
s := make([]int, 0, 10)
s = append(s, 1, 2, 3)
appendNums(&s) // pass the pointer of the slice
fmt.Println(s)
}
func appendNums(s *[]int) { // receive the pointer of the slice
*s = append(*s, 7, 8) // dereference the pointer to modify the original slice
}
// Output:
// [1 2 3 7 8]
Wrap Up
In Jon Bodner's presentation, he explores various other interesting behaviors of Golang. I look forward to breaking them down in future posts.
If you haven't watched the video yet, I encourage you to check it out. It's a bit mind-bending for me, but I'm sure you'll find it enjoyable as well.
Happy coding!




