Hacker News new | ask | show | jobs
by rendaw 492 days ago
Appending a slice is also full of gotchas. Sometimes it modifies the slice in place, sometimes it reallocates and copies.
1 comments

Only really a gotcha if you pass a slice into a function and expect to see modifications in that slice after the function completes. It's helpful to remember that Go passes by value, not reference.

That can be addressed by passing the slice as a pointer: https://go.dev/play/p/h9Cg8qL9kNL

> Only really a gotcha if you pass a slice into a function and expect to see modifications in that slice after the function completes. It's helpful to remember that Go passes by value, not reference.

Slices are passed partly by value (the length), partly by reference (the data).

    func takeSlice(s []int) {
      slices.Sort(s)
    }
From your explanation, you would expect that to not mutate the slice passed in, but it does.

This can have other quite confusing gotchas, like:

    func f(s []int) {
      _ = append(s, 1)
    }

    func main() {
        s := []int{1, 2, 3}
        f(s[0:2])
        fmt.Printf("%v\n", s)
    }
I'm sure the output makes perfect intuitive sense https://go.dev/play/p/79gOzSStTp4
Slices are passed only by value. It's just that the value is a struct containing a reference to the data. Once one understands that, the rest makes perfect sense.

I can see why it trips up newcomers, but it feels pretty basic otherwise.

I say this as someone working with go every day.

The fact that I can pass a slice to a func 'by value' and mutate the source slice outside the func is already surprising behavior to most people. The fact that it MIGHT mutate the source slice depending on the slice capacity is the part that really drives it home as bad ergonomics for me.

Overall I enjoy working with go, but there are a few aspects that drive me up the wall, this is one of them.

How would you have designed it? An internal byte array instead of a pointer?
I think the key thing missing from go slices is ownership information, especially around sub-slices.

Make it so you can create copy-on-write slices of a larger slice, and a huge number of bugs go away.

Or do what rust did, except at runtime, and keep track of ownership

    s := []int{1, 2, 3}
    s[0] = 0 // fine, s owns data
    s1 := s[0:2] // ownership transferred to s1, s is now read-only
    s1[0] = 1 // fine, s1 owns data

    s[0] = 1 // panic or compiler error, s1 owns data, not s
With of course functions to allow multiple mutable ownership in cases where that's needed, but it shouldn't be the default
I could have worded it better, but yes, slices have footgun potential but it's simple to work with once you know how they work (and maps fall into this same category).