4 ms·
> 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> typ
by wrs 11mo ago
> 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.
- auxiliarymoose 11mo agoWriting "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.
- masklinn 11mo agoIndeed the usual error is b := append(a, …)
- weakfish 11mo agoHow is that an error if b is properly referenced? It’s perhaps a waste of memory but not wrong
- masklinn 11mo agoBecause `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) _ _]
- weakfish 11mo agoThanks for the thorough explanation!
- deleted 11mo ago[deleted]
- dlisboa 11mo ago> 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.
- int_19h 11mo agoIt'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.
- dlisboa 11mo agoOutside 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.
- int_19h 10mo agoWorking with dynamically sized arrays is not one of those things. To my knowledge, Go is the only high-level language that implements them like that; everybody else has a proper abstraction.
- FridgeSeal 11mo ago“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?”
- throwawaymaths 11mo agoironically 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.
- RVuRnvbM2e 11mo ago> There's also no way to shrink the memory underlying a slice. Sorry, that is incorrect: https://pkg.go.dev/slices#Clip 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 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.
- knorker 11mo agoDoes 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?
- wrs 11mo agoYes, my example was garbled. Thanks @masklinn for correcting it for me below!
- skybrian 11mo agoIt 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.