Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> In Go, a slice is a fat pointer to a contiguous sequence in memory, but a slice can also grow, meaning that it subsumes the functionality of Rust’s Vec<T> type and Zig’s ArrayList.

Well, not exactly. This is actually a great example of the Go philosophy of being "simple" while not being "easy".

A Vec<T> has identity; the memory underlying a Go slice does not. When you call append(), a new slice is returned that may or may not share memory with the old slice. There's also no way to shrink the memory underlying a slice. So slices actually very much do not work like Vec<T>. It's a common newbie mistake to think they do work like that, and write "append(s, ...)" instead of "s = append(s, ...)". It might even randomly work a lot of the time.

Go programmer attitude is "do what I said, and trust that I read the library docs before I said it". Rust programmer attitude is "check that I did what I said I would do, and that what I said aligns with how that library said it should be used".

So (generalizing) Go won't implement a feature that makes mistakes harder, if it makes the language more complicated; Rust will make the language more complicated to eliminate more mistakes.





> There's also no way to shrink the memory underlying a slice.

Sorry, that is incorrect: https://pkg.go.dev/slices#Clip

> It's a common newbie mistake to think they do work like that, and write "append(s, ...)" instead of "s = append(s, ...)". It might even randomly work a lot of the time.

"append(s, ...)" without the assignment doesn't even compile. So your entire post seems like a strawman?

https://go.dev/play/p/icdOMl8A9ja

> So (generalizing) Go won't implement a feature that makes mistakes harder, if it makes the language more complicated

No, I think it is more that the compromise of complicating the language that is always made when adding features is carefully weighed in Go. Less so in other languages.


Does clipping make the rest eligible for GC?

Clipping doesn't seem to automatically move the data, so while it does mean appending will reallocate, it doesn't actually shrink the underlying array, right?


Yes, my example was garbled. Thanks @masklinn for correcting it for me below!

Writing "append(s, ...)" instead of "s = append(s, ...)" results in a compiler error because it is an unused expression. I'm not sure how a newbie could make this mistake since that code doesn't compile.

Indeed the usual error is

    b := append(a, …)

How is that an error if b is properly referenced? It’s perhaps a waste of memory but not wrong

Because `append` works in-place, Go slices are amortised, and the backing buffer is shared between `a` and `b`, so unless you never ever use a again it likely will have strange effects e.g.

    a := make([]int, 0, 5)
    a = append(a, 0, 0)
    b := append(a, 1)
    a = append(a, 0)
    fmt.Println(b)
prints

    [0 0 0]
because the following happens:

    a := make([]int, 0, 5)
    // a = [() _ _ _ _ _]
    // a has length 0 but the backing buffer has capacity 5, between the parens is the section of the buffer that's currently part of a, between brackets is the total buffer
    a = append(a, 0, 0)
    // a = [(0 0) _ _ _]
    // a now has length 2, with the first two locations of the backing buffer zeroed
    b := append(a, 1)
    // b = [(0 0 1) _ _]
    // b has length 3, because while it's a different slice it shares a backing buffer with a, thus while a does not see the 1 it is part of its backing buffer:
    // a  = [(0 0) 1 _ _]
    a = append(a, 0)
    // append works off of the length, so now it expands `a` and writes at the new location in the backing buffer
    // a = [(0 0 0) _ _]
    // since b still shares a backing buffer...
    // b = [(0 0 0) _ _]

Thanks for the thorough explanation!

It seems kind of odd that the Go community doesn't have a commonly-used List[T] type now that generics allow for one. I suppose passing a growable list around isn't that common.

> Go programmer attitude is "do what I said, and trust that I read the library docs before I said it".

I agree and think Go gets unjustly blamed for some things: most of the foot guns people say Go has are clearly laid out in the spec/documentation. Are these surprising behaviors or did you just not read?

Getting a compiler and just typing away is not a great way of going about learning things if that compiler is not as strict.


It's not unjust to blame the tool if it behaves contrary to well established expectation, even if that's documented - it's just poor ergonomics then.

Outside very simple programming techniques there is no such thing as well-established when it comes to PL. If one learns more than a handful of languages they’ll see multiple ways of doing the same thing.

As an example all three of the languages in the article have different error handling techniques, none of which are actually the most popular choice.

Built in data structures in particular, each language does them slightly differently to there’s no escaping learning their peculiarities.


ironically with zig most of the things that violate expectations are keywords. so you run head first into a whole ton of them when you first start (but at least it doesn't compile) and then it you have a very solid mental model of whats going on.

“Clearly it’s your fault for not realising that we embedded razor blades in our hammers! What did you think, that you could safely pick up a tool?”

Another example is the (very recently fixed) documented but unobvious and unenforceable requirements for calling Timer.Reset() [0].

[0] https://pkg.go.dev/time@go1.22.12#Timer.Reset




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: