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

It is interesting to see them add things like the "clear" function for maps and slices after suggesting to simply loop and delete each key one at a time for so long. Is this a result of the generics work that makes implementation easier vs. the extra work of making a new "magic" function (like "make", etc.)?


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.


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.


> Technically, their only solid common point is that they address contiguous spaces in memory.

That’s a very major thing to have in common. Definitely not a minor detail, for sure.


When you're comparing two cars, the fact that both have four wheels and drive on a road is not that compelling.


In this scenario, we are comparing two cars, a bicycle, a jet ski, and three types of airplane. Yes, the cars are similar, within that context. Many languages, like Python and Java, do not have an array slice type. And the similarities between C++, Rust, and Go are relevant—the length is a property of the slice itself, and since the slice is passed by value, it is not modified by a function that accepts a slice as an argument, even if the objects the slice point to are modified by that function.

If you see a different context, then you misinterpreted what I wrote.

It is easy—trivial, even—to imagine scenarios where a particular “X is like Y” does not make sense. What you should do, as a reader, is try and understand what the writer means, rather than try to figure out some way to interpret a comment so that it is wrong, in your view.

The easy way out—saying “X is not like Y because of difference Z”—does not meaningfully contribute to the discussion.


> 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.


Sure, there may be a right way to say this.

I have a particular axe to grind when it comes to the word “ownership” of objects in programming. In C++ and Rust there is a very natural sense of ownership in that the owner of an object is who may deallocate the object, and that ownership may be shared with std::shared_ptr<T> in C++ or Rc<T> / Arc<T> in Rust. Ownership is such a useful concept in these languages because it is generally true that somebody must deallocate the object, and it must happen safely.

As a very natural consequence, people who spend long hours working in C++, Rust, C, or other similar languages start to associate, very closely, the notions of ownership and correctness. And indeed, ownership is broadly useful outside C++, Rust, and C. Even in a garbage-collected language like Java or Go, it is generally useful to have clear ownership. You don't modify objects that you don't own, or use objects outside their scope.

But occasionally, you come across a piece of code where ownership gets in the way. Perhaps some garbage-collected algorithm that transforms data with pointers going all over the place. It probably sounds like a mess, but that is not necessarily true either—it can be perfectly good, correct, readable code.

So while ownership is a useful concept for talking about specific pieces of Go code, or specific pieces of Java code, it is not applicable to all Go or Java code, and that’s fine. It’s kind of like talking about code in terms of functions—nearly every language on the planet makes heavy use of functions (or some equivalent), but it’s also true that code does not have to be organized in functions, and you will occasionally see code that does not use functions.


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?


Maybe generics have fixed this, I threw in the towel on golang before they released them.

But as an example, if you wanted to have any sort of higher-level management of goroutines (for example, a bounded number of background workers) you get to rewrite or copy-paste that code every place you want to accomplish that. A library couldn't exist to abstract away the idea of a pool of background workers because it can't know in advance what types you want to send over your channels.

Again, I wouldn't be surprised if post-generics there's a library now to do this for you. But for years if you wanted anything higher level than raw channels, you're basically on your own.


Just saw your reply now. Thank you.


> 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


From the spec [1], it was because the loop doesn't work to clear a map with a NaN key.

[1] https://github.com/golang/go/issues/56351


> It is interesting to see them add things like the "clear" function for maps and slices after suggesting to simply loop and delete each key one at a time for so long.

Slowly walking back dogmatic positions is just how the Go team works.

I say this as a person that wrote Go full time for a handful of years.


No, it's because a use case was discovered that the for loop approach can't handle: NaN keys.


In my experience, that's exactly how this plays out every single time.

    Dev: Can we have a function to clear a map?
    
    Go: No, it's easy enough to write the 5 lines of code to just do it yourself every time.
    
    Dev: Okay, I don't see why I should have to write those 5 lines every time but fine. Isn't looping over everything going to be slower than just… having a function that can empty the internals?

    Go: We've implemented a compiler optimization to detect this and rewrite it to the faster code it would have been if it we were to implement it.

    Dev: Isn't that… way harder than just writing the method? Anyway, I noticed this solution doesn't actually always work because of this edge case.
    
    Go: Just handle the edge case every time then.

    Dev: That's the point. I can't.
And around and around we go.


You're distorting the real story to fit your bias. Once the NaNn edge case was discovered, work to deal with that edge case was started.


Can you link to any unit of work that solves this edge case? Because all I can find are bugs and issues created 2015/2016/2017 that were closed and unresolved.


This isn't remotely close to the first or even the tenth time I've seen this exact pattern play out. Finally there's some straw that forces the golang team to backpedal on a dogmatic position, but along the way there's dozens of comical defenses of the current state of things.


So you say, with no actual reference. Maybe what you refer to happened in your head, and not in the real world.


1. Generics

2. Clear

Are the two already covered not enough?


I would like to see some content from the go team on generics or clear that fits your claim. Yes, there are many in the community who speak the way you suggest, but you don’t seem to know much about the Go teams pov.


https://news.ycombinator.com/item?id=23033183

A pretty apt write up.

But I accept there’s probably not an amount of evidence to change your belief on Go’s dogmatism. And that’s okay! You like a language. That’s great!


From what I can tell the issue here is that Rob Pike thinks the label "generics" is inaccurate? Seems like a far cry from what you have accused them of. I think theres not only a lack of evidence to convince me, I think your claim is just straight up unsubstantiated. I think an unbiased, responsible observer would have to conclude similarly to me.


> I think an unbiased, responsible observer would have to conclude similarly to me.

Unsurprising conclusion. Enjoy the day!


I used to really like Go. Now that I don't work with it, I find that the further I go on without it, and with using other tools, the less and less I'd want to go back.


I would argue Go's inability to manage NaN keys is irrelevant to the desire for "clear", in that I would argue that the NaN keys issue should be fixed _regardless_ of clear.


One aspect of this is that it was formerly impossible to delete NaNs from a map[float64]T, unless you had the nan already.


Even with the NaN, the NaN wasn't equal to itself, so it still wouldn't delete. Really, they just should have forbidden float64 key'd maps, but too late for that, I guess.


It might also be that they’ve worked their way down the priority list and are getting to these features that are largely just to tidy up code.


Clearing a container is usually a much simpler and faster operation than looping through all and removing them individually. That's not a question of tidying something up.


There were compiler optimizations for clearing by iterating. I haven’t looked at the code, but I suspect this won’t be much more efficient than iterating was with the optimizations.


I expect both will result in the same code; the only difference is that the clear built-in can handle maps with NaN keys.


> after suggesting to simply loop and delete each key one at a time for so long

Those were always bad alternatives to a real design problem, they just didn't have a good alternative to offer at the time.




Consider applying for YC's Summer 2026 batch! Applications are open till May 4

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

Search: