Hacker News new | ask | show | jobs
by maximilianburke 1098 days ago
That `clear` on a slice sets all values to their type's zero value is going to be extremely confusing especially coming from other languages (Rust, C#, C++, Java, ...) where the same-named function is used on list-ish types to set their length to zero.

Doubly-so when `clear` on a map actually seems to follow the convention of removing all contained elements.

4 comments

Sure, although as a Go user, the behavior described is exactly what I’d expect. These new functions are no different from functions that you could write yourself.
I'm a go user, and think it's dumb that:

    clear(f)
    fmt.Println(len(f))
will have different results if f is a slice and a map.
I guess, but that seems expected to me at this point, and consistent within the semantics of how slices and maps work (and other values).

Maps are kind of like

    type map *struct{ len int; ... }
Slices are kind of like

    type slice struct{ len int; ... }
We get a lot of convenience by having the pointers auto-dereferenced, but the cost is that the semantics are still different and there are no syntactic markers to remind us of the fact.

I don't think any language has really given us something that is completely intuitive here. Python's semantics with the list type are a constant surprise to newcomers. C++’s semantics surprise newcomers. Rust's semantics surprise newcomers. Surprises all around. The best you can hope for is something that is internally consistent.

The slice in Go is more or less equivalent to &[] in Rust or std::span in C++. The whole idea of passing a pointer by value is key to understanding the semantics of most modern programming languages. Like, is Java pass-by-value or pass-by-reference? You can argue the point, but whatever label you decide is appropriate for Java, it’s useful to think of Java as passing pointers by value. Same with Python, Rust, Go, etc. This is not intuitive for people who are new to programming.

> The slice in Go is more or less equivalent to &[] in Rust or std::span in C++.

Not really, because they are mutable, they can mutate the underlying memory, and they can re-allocate. They are a weird mix of &mut []/Vec or std::{span,vector}.

In contrast, a Rust &[] can may the underlying storage (if it's an &mut []), but cannot spin out a new storage on its own and start a new life without a backing structure – and I'm not utterly familiar with std::span, but I would wage the semantics are close.

Go slices can, which is why they are always tricky, especially for beginners. Not only does = not really do what is intuitively expected, not only every beginner will be bitten in the ass by forgetting the `x =` in `x = append(x, y)`, but it is impossible, when calling a function expecting a slice, to know if this function only wants a view on some memory or actually expect to modify it; a capital difference that is very clear in Rust or C++ type systems.

To be honest I kinda hate the reply to “X is like Y” comment when someone says “X is not like Y because of difference Z”. It's just… so pedantic. The whole reason we say “X is like Y” instead of “X is the same as Y” is because X is not the same as Y. I’m just really tired of seeing this response on HN over and over. I was pretty damn explicit when I said “more or less” and you’re here to argue about whether it is legal for me to say “more or less” in this context. I mean, geez, what a drag.

If you talk about how Go slices are tricky for beginners, but you cite C++ as some kind of gold standard against which Go should be compared, then I think you’ve lost the plot—C++’s type system is a complete and utter trash fire for people who are new to programming. Rust, as well, is very difficult for people to get into. Even the Python semantics for lists get people tripped up all the time.

    a = [[]] * 5
    b = [[] for _ in range(5)]
I bring this up because there is no language that gets things right for beginners and still provides the tools which professional programmers expect to have. And if you want to pick an example of a language that is particularly bad for beginners, C++ is it. C++ is shit for beginners. Complete shit. I bring up the Python example because it’s something I’m always explaining to people who are learning Python—Python is ok, but slicing in Python creates new arrays containing a copy of the slice's contents.

The nuances of how references and values work is something that you have to work through, and then you have to come to terms with the conventions for the particular language you are using. IMO, Go’s slices are fine… you really just have to be careful about aliasing a slice you don’t own, but then again, that’s true for languages like C++, Python, Java, and C# as well. Rust is the only one that’s really different here.

> The whole reason we say “X is like Y” instead of “X is the same as Y” is because X is not the same as Y

Being able to change the underlying data is a pretty big difference. Technically, their only solid common point is that they address contiguous spaces in memory.

> you cite C++ as some kind of gold standard

I never did; I highlighted the difference between immutable views vs. whatever Go slices are.

> I think you’ve lost the plot

No need for the snark there.

> The slice in Go is more or less equivalent to &[] in Rust or std::span in C++.

My understanding is, to use the Rust/C++ term, slices in Go are owned, but they are not in Rust or C++. That is, they're a pointer + length in the latter two, but a pointer, length, and capacity in Go.

The type in Go does not carry ownership information. It is not a useful distinction to say that slices are "owned" in Go.
Types in C++ don’t carry ownership information inherently either, but they’re still thought of in these terms. I know Go doesn’t often use these terms, which is why I clarified.

I think the distinction is useful specifically because it explains why Go slices work differently than in at least those two languages.

Every language has sharp edges, but go's whole MO is to avoid rabid footguns at the expense of verbosity (IMO). The for-shadow issue thats fixed this release is a great example of go deciding to do the intuitive thing rather than the "correct" thing because that's how people work.

I don't think the implementation details matter to a user of a map or a slice (or an array for that matter) - they're language builtins (as opposed to span, vector and map in c++ which are library types).

In my experience, go has tons of footguns that come because of the verbosity. Rather than having clear abstractions that handle edge cases for you, you get to reimplement these things yourself every single time.

Case in point, clear. Or "typed nils". Or accidentally swallowing errors because you had to handle them manually. Or reimplementing higher-level job control on top of channels every single time.

>Or reimplementing higher-level job control on top of channels every single time.

Can you please explain this?

> Python's semantics with the list type are a constant surprise to newcomers.

Care to elaborate?

The builtin clear() will handle cases like deleting NaN from a map.
Go slices are passed by value so there's no way for clear() to resize the underlying array without reassignment.

I suppose it could have been x = clear(x) or clear(&x), but certainly if you understand Go semantics then seeing any function call do Foo(slice) already signals that the call can't modify the length since there's no return value.

This is a great example of why I dislike Go. It is not obvious that a slice is passed by value while a map is not or why. Therefore every action on it feels a bit weird because of that, and now you have functions like "clear" that take a very non-obvious action. Personally, I'd rather have pass-by-value return an error and only allow pass-by-reference (better: they should have had maps and slices be pointers). I'm not sure I'd ever use a function that set every value to its zero type.
I agree the semantics seem weird, I've occasionally wanted the equivalent of x = clear(x) but I can't think of a time when I've wanted to set all the values to the zero value.

The bug doesn't seem to discuss use cases for it either. The most I could find is: https://github.com/golang/go/issues/56351#issuecomment-13326...

Which boils down to "doing what clear(slice) does cannot be implemented efficiently today" but I'm not sure how having an efficient way to do something folks don't want is useful?

There's already a memory clearing optimization in the compiler: https://github.com/golang/go/issues/19266

So yeah I'm not sure under what situations folks will use clear(slice).

You often do it in Go to avoid pointing at something needlessly which would delay or even keep that something from being garbage collected.
That's actually a great explanation of why it's not easy to implement the clear function the way it makes sense for slices. However, this is a built-in, not a normal function, so they could make it do whatever they like, including doing the intuitive and desired thing, no? It seems to me that they've just created another "loop variable gotcha" type situation...
C#’s array/span.Clear() does exactly the same - it zeroes them out.
It sounds like they're inheriting the naming from the calloc command. Allocates then 0's the memory. It lines up with the go devs' backgrounds