Golang Is memory size doubled if passing a slice to a parameter?

eye-catch Golang

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.

Sponsored links

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?

Sponsored links

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

Copied title and URL