Suppose you are one of the people who came from C or similar programming language and started using Golang. In that case, you might want to pass a pointer to a function parameter in order to avoid the data copy so that the computer can process without allocating extra memory.
Two team members asked me about it in the same week, so I think it would be helpful to provide the information for other people on the Internet.
Why passing a pointer
If we need to store a big size of content, the memory usage of the slice could be MB or GB. Assume that we need to download a big file from a server and do something. If we have to wait for many chunks of byte stream, the slice gets big. Once we receive special content, we want to pass the slice to another function because of readability.
If it’s a string, the value is copied. Therefore, the change made in a function doesn’t affect the original value.
package main
import "fmt"
func main() {
val := "foo"
change(val)
fmt.Println(val) // foo
}
func change(val string) {
val = "hoge"
}
If the string is big, you might want to pass a pointer instead for the performance. If passing a pointer, the value is not copied and thus the performance is a bit improved.
package main
import "fmt"
func main() {
val := "foo"
change(&val)
fmt.Println(val) // hoge
}
func change(val *string) {
*val = "hoge"
}
Likewise, you might want to pass a pointer of a slice. Is it really necessary?
Pass a slice variable instead of a pointer
Let’s try to pass a slice to a function parameter and check the address. If the data is copied, the address should be different from the original slice.
The passed slice has the same address as the original slice object
func passSlice1() {
arr := []byte{1, 2, 3, 4, 5}
fmt.Printf("%p, %p, %p, %p\n", arr, &arr[0], &arr[1], &arr[2])
// 0xc0000b0058, 0xc0000b0058, 0xc0000b0059, 0xc0000b005a
passValue(arr)
}
func passValue(val []byte) {
fmt.Printf("%p, %p, %p, %p\n", val, &val[0], &val[1], &val[2])
// 0xc0000b0058, 0xc0000b0058, 0xc0000b0059, 0xc0000b005a
}
The original slice object has the same addresses as the object used in passValue() function. It looks like the slice is passed to a function parameter as a reference. Then, all changes in the function affect to the original value.
Wait. Let’s check more.
Update of an element affects but appending an element not
Let’s try to make a change in the function.
func passSlice1() {
arr := []byte{1, 2, 3, 4, 5}
fmt.Printf("%p, %p, %p, %p\n", arr, &arr[0], &arr[1], &arr[2])
// 0xc0000b0058, 0xc0000b0058, 0xc0000b0059, 0xc0000b005a
passValue(arr)
fmt.Println(arr) // [1 11 3 4 5]
fmt.Printf("len=%d,cap=%d\n", len(arr), cap(arr)) // len=5,cap=5
}
func passValue(val []byte) {
fmt.Printf("%p, %p, %p, %p\n", val, &val[0], &val[1], &val[2])
// 0xc0000b0058, 0xc0000b0058, 0xc0000b0059, 0xc0000b005a
val[1] = 11
val = append(val, 77, 88, 99) // this doesn't affect the original value
fmt.Printf("len=%d,cap=%d\n", len(val), cap(val))
// len=8,cap=16
}
The second element is updated in passValue()
function and the change affects to the original slice. However, appending some values doesn’t affect it. This is an important thing to know. The append operation is closed in the function because runtime representation of a slice is a struct called SliceHeader.
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
Since the actual data is a pointer in the struct, the change in the function affects the original slice object. However, Len
and Cap
properties are copied from the original object. Therefore, appending values don’t affect the original object.
Pass a pointer of a slice to a function parameter
How about passing a pointer?
func passSlice1() {
arr := []byte{1, 2, 3, 4, 5}
fmt.Printf("%p, %p, %p, %p\n", arr, &arr[0], &arr[1], &arr[2])
// 0xc0000b0058, 0xc0000b0058, 0xc0000b0059, 0xc0000b005a
passPointer(&arr)
// pointer address: 0xc0000b0058
// address of pointer: 0xc0000bc030
// len=8,cap=16
fmt.Println(arr) // [1 22 3 4 5 77 88 99]
fmt.Printf("len=%d,cap=%d\n", len(arr), cap(arr)) // len=8,cap=16
}
func passPointer(val *[]byte) {
fmt.Printf("pointer address: %p\n", *val)
fmt.Printf("address of pointer: %p\n", val)
(*val)[1] = 22
*val = append(*val, 77, 88, 99)
fmt.Printf("len=%d,cap=%d\n", len(*val), cap(*val))
}
*val
in passPointer()
function has the same address as the original object but val
not because it’s an address to store the address of the pointer.
Same as before, the change in the function affects to the original slice object. Furthermore, appending values affects it as well because the caller passes the pointer of the slice object. passPointer()
function uses exactly the same object as caller’s SliceHeader
struct. Therefore, the changes to Len
and Cap
affect the original slice object.
Conclusion
You don’t need to pass a pointer of a slice object unless you need to append values. Passing a slice object is a cheap operation. You can remove nil check by passing a slice object and the code gets a bit more readable.
The changes for the existing elements affect the original slice object but I don’t recommend it. The rule of thumb is as follows.
- Pass a pointer of a slice if the function must update the slice
- Pass a slice object if the function just wants to read
The intention is clear if passing a pointer.
If you want to know the details of the slice implementation in Go, this official article is helpful.
Go Slices: usage and internals
Are you looking for a way to reduce memory size? The following article could help you out.
Comments