gopointersslice

Updating slice after passing to function in Go


I want to use defer in Go while also updating the list I am passing in the function itself.

package main

import (
    "fmt"
    "time"
)

func record(start time.Time, l []int) {
    fmt.Printf("Record: Time: %v, List: %v\n", time.Since(start), l)
}

func record2(start time.Time, l ...int) {
    fmt.Printf("Record2: Time: %v, List: %v\n", time.Since(start), l)
}

func record3(start time.Time, l *[]int) {
    fmt.Printf("Record3: Time: %v, List: %v\n", time.Since(start), *l)
}

func main() {
    l := []int{1, 2, 3}

    defer func(t0 time.Time) {
        record(t0, l)
    }(time.Now()) // list updated

    defer record2(time.Now(), l...) // list not updated

    defer record3(time.Now(), &l) // list updated

    l = append(l, 4)
    time.Sleep(1 * time.Second)
    return
}

This returns

Record3: Time: 1.001027459s, List: [1 2 3 4]
Record2: Time: 1.001204583s, List: [1 2 3]
Record: Time: 1.001209792s, List: [1 2 3 4]

However in the "Effective Go" here it is mentioned that method 3 is not idiomatic.

Are there any other alternatives?


Solution

  • They mean using arrays is not idiomatic, it's not about pointers to slices:

    func Sum(a *[3]float64) (sum float64) { ...

    But even this style isn't idiomatic Go. Use slices instead.

    In fact, slice is a structure that contains 3 fields: pointer to an underlying array, length and capacity. Passing a slice to a function copies this structure. If you later change the underlying array by adding an element, the length and capacity fields on the copied structure will not reflect that. Because of that from within the function the slice will look like it was not changed.

    Look at the example bellow (or in the Go Playground):

    package main
    
    import (
        "fmt"
        "time"
    )
    
    func byval(s []int) {
        time.Sleep(1)
        fmt.Printf("But in byval it appears the same: %v\n", s)
    }
    
    func byptr(s *[]int) {
        time.Sleep(2)
        fmt.Printf("While it. is actually changed, which is obvious in byptr: %v\n", s)
    }
    
    func main() {
        s := []int{1, 2, 3}
        go byval(s)
        go byptr(&s)
        s = append(s, 1)
        fmt.Printf("Slice was changed in main: %v\n", s)
        time.Sleep(3)
    }
    

    It prints the following:

    Slice was changed in main: [1 2 3 1]
    But in byval it appears the same: [1 2 3]
    While it is actually changed, which is obvious in byptr: &[1 2 3 1]
    

    Thus, pointers to slices allow to reflect changes of their length and capacity.

    But more often you will want to pass slice to function and modify it there. In such cases an idiomatic way will be to pass slice pointer as receiver as opposed to a parameter:

    func (s *[]int) modify() {
        ...
    }
    

    Again, slice pointer will allow to reflect changes, but in this case in call site of modify.