The hidden risk of passing slice as function parameter

  sonic0002        2020-12-13 06:11:14       8,318        2    

In Go's source code or other open source libraries, there are lots of cases where a slice pointer is passed to function instead of slice itself. This brings up a doubt why not passing slice directly as its internal is backed by an array pointer to point to underlying data?

For example, in log package, the formatHeader function takes a parameter buf as type *[]byte instead of []byte.

func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {}

Let's understand the rationale behind this starting with an example.

func modifySlice(innerSlice []string) {
    innerSlice[0] = "b"
    innerSlice[1] = "b"
    fmt.Println(innerSlice)
}

func main() {
    outerSlice := []string{"a", "a"}
    modifySlice(outerSlice)
    fmt.Print(outerSlice)
}

// output
[b b]
[b b]

The above output is expected as the underlying data pointer is updated which reflects both inside and outside the function.

Now changing the parameter of the function to take a pointer to the slice

func modifySlice(innerSlice *[]string) {
    (*innerSlice)[0] = "b"
    (*innerSlice)[1] = "b"
    fmt.Println(*innerSlice)
}

func main() {
    outerSlice := []string{"a", "a"}
    modifySlice(&outerSlice)
    fmt.Print(outerSlice)
}

// output
[b b]
[b b]

From the output, the result is the same. Hence what's the difference? Take a closer look at the struct of slice.

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

array is the internal array pointer stores the actual data, len is the length of the slice and cap is the capacity of the slice

Modify the above code snippet a bit.

func modifySlice(innerSlice []string) {
    innerSlice = append(innerSlice, "a")
    innerSlice[0] = "b"
    innerSlice[1] = "b"
    fmt.Println(innerSlice)
}

func main() {
    outerSlice := []string{"a", "a"}
    modifySlice(outerSlice)
    fmt.Print(outerSlice)
}

// output
[b b a]
[b b]

Alright, the result is a bit interesting now. The update to the slice inside the function doesn't reflect outside of the function. What happened?

To figure this out, adding more logs to see what is going on.

func modifySlice(innerSlice []string) {
    fmt.Printf("%p %v   %p\n", &innerSlice, innerSlice, &innerSlice[0])
    innerSlice = append(innerSlice, "a")
    innerSlice[0] = "b"
    innerSlice[1] = "b"
    fmt.Printf("%p %v %p\n", &innerSlice, innerSlice, &innerSlice[0])
}

func main() {
    outerSlice := []string{"a", "a"}
    fmt.Printf("%p %v   %p\n", &outerSlice, outerSlice, &outerSlice[0])
    modifySlice(outerSlice)
    fmt.Printf("%p %v   %p\n", &outerSlice, outerSlice, &outerSlice[0])
}

// output
0xc00000c060 [a a]   0xc00000c080
0xc00000c0c0 [a a]   0xc00000c080
0xc00000c0c0 [b b a] 0xc000022080
0xc00000c060 [a a]   0xc00000c080

Parameters passed in Go functions are values. When passing a slice into a function, a copy of the slice struct is passed. Hence the underlying array pointer is copied as well, if the data changed in array pointer, it would reflect on the copied slice struct as well.

However if the underlying array pointer is resized or reallocated, the change to the inside slice would not reflect on the outside slice. The same applies to len and cap.

Below are some diagrams explaining this.

If len and cap is the same, an append operation would trigger resizing,  when resing happens, a new underlying array is created with new size, the data from original array would be copied to the new array. And the new appended item would be in the new array. Now the link between the slice inside the function and the outside function is broken. 

 This explains why some functions would take pointers of slice as parameters.

func modifySlice(innerSlice *[]string) {
    *innerSlice = append(*innerSlice, "a")
    (*innerSlice)[0] = "b"
    (*innerSlice)[1] = "b"
    fmt.Println(*innerSlice)
}

func main() {
    outerSlice := []string{"a", "a"}
    modifySlice(&outerSlice)
    fmt.Print(outerSlice)
}

// output
[b b a]
[b b a]

The lesson takeaway is that if just updating the value of slice without appending or other operation which would cause resizing, can pass slice, otherwise please consider to pass pointer of slice.

Reference: Go 切片传递的隐藏危机

GOLANG  SLICE  SLICE POINTER 

       

  RELATED


  2 COMMENTS


Anonymous [Reply]@ 2020-12-14 21:37:02

image is broken

Ke Pi [Reply]@ 2020-12-15 05:39:19

thx for pointing out. fixed



  RANDOM FUN

Compilation without error in one shot