Hacker News new | ask | show | jobs
by avl999 1766 days ago
Go is my favorite language to write code in. I was very skeptical when I first started learning it for a new job with error handling that looked archaic in comparison to exceptions but once I got the hang of it it has quickly become my preferred language of choice for any project.

Once generics are finally added the language should basically be a no-brainer for any serious production code.

1 comments

From a distance it looks difficult to write in. For example, two recommended ways to delete from a slice:

    a = append(a[:i], a[i+1:]...)
    // or
    a = a[:i+copy(a[i:], a[i+1:])]
Both seem harder than necessary.

Is that code just idiomatic, and Go programmers recognize it instantly? Or maybe they don't deal with slices that often?

https://github.com/golang/go/wiki/SliceTricks

With the introduction of generics in Go 1.18 and the accompanying "slices" package (https://github.com/golang/go/issues/45955), we might soon write that as:

  a = slices.Delete(a, i, i+1)
That said, in code I write, I rarely need to do an in-place delete from a slice. I think it's rare enough that recognizing the idiom is okay.
It's so weird why they're adding a slices package instead of making them functions on the slice itself. They did the same with strings.

Compare

    strings.ToUpper(strings.Replace(strings.Trim(s), "a", "b")))
Instead of

    s.Trim().Replace("a, "b").ToUpper()
For better or worse, this is by design. See: https://golang.org/doc/faq#methods_on_basics

It's "to avoid complicating questions about the interface (in the Go type sense) of basic types". It also allows separating the builtin functions, of which there are very few (they have to be in the core language spec), from the stdlib functions like strings.ToUpper, which there are many many more of and are added to more quickly than the builtins.

I'm not sure what their answer means. They used a similar hand wavy answer about why the language has null pointers. "Complication" here refers to how complicated their implementation of the compiler is, not the end user complication, which is what they traded off (simpler compiler for more complex user code).

I didn't get the last point. builtin functions are there (for a big part) because the language doesn't have generics. Functions on types can be added without affecting the interface, since strings don't implement any interface (at least not on purpose, another problem with golang). Java constantly enriches standard library types with more useful methods.

Because in-place delete is slow and you want to avoid algorithms with a lot of in-place deletes. So it's ok to have it be awkard, it's like the names of rare elements being longer than the names of common elements, not an issue.
No, you deal with slices all the time. You just recognize the idiom.

You can restructure it if you don't need to preserve order:

    a[i] = a[len(a)-1] 
    a = a[:len(a)-1]
In general* Go does not hide complexity from you. It's a blessing and a curse.

*: because the Internet is a nitpicky place.

This has been my experience as well: I had to fix a bug in a Go codebase recently, and the fix required me to de-duplicate an unordered collection of elements. It turns out that there's (1) no built-in way to do this, and (2) there isn't even a built-in set structure in Go, so you can't do the obvious solution without explicitly using a map with a chaff value. Stack Overflow has dozens of duplicate questions for this, all with ridiculous O(n^2) or buggy (or both) answers.
map[T]struct{} is the set type in Go. (Note that struct{} is represented with 0 bytes of memory, so it's not chaff.)

You might like the slices proposal for utility functions like these: https://github.com/golang/go/issues/45955

Thanks. I wasn't aware that struct{} is zero-sized, so that plugs that hole.

I'll admit freely that I'm not a proficient Go programmer, so it's easier for me to see the things I don't like than the things that Gophers praise. But even this solution leaves me unsatisfied, in contrast to what I'd reach for in Rust or even C++: it requires that I know that struct{} is zero-sized, and I still have to do the manual legwork of writing a CS101 dedupe function.

Zero Size optimisations are a thing in Rust too though, and I don't think an empty struct being zero size is any more surprising than Rust's empty tuple being zero size.

Like, if you tell Rust you want 4 million empty tuples in a vector, it will give you a vector with exactly 4 million empty tuples in it and no heap allocation because those empty tuples don't take space, so, 4 million of them also doesn't take up any space.

The key difference being knowledge: I don't have to know `HashSet` is really just a map with a ZST to use it efficiently. All I need to know is that I don't need a value, and the ecosystem obliges me. This in contrast to Go, where I need to know that `struct{}` is a ZST, that the "right" way to do a set is to use it as the map value, &c.
Fun thing, is Rust employs the same trick. The HashSet<T> is an alias for HashMap<T, ()>. "()" is the unit type. Which is zero-sized.

Because Rust has generics, it is more ergonomic though.

https://doc.rust-lang.org/std/collections/struct.HashSet.htm...

You can find things to complain in any language, including the languages that YOU use.

For every complain like that in Go I'll find a hundred in C++, ten in Java / JavaScript / Python.

As to your question, as a full-time Go programmer: this doesn't come up often.

When it does, I google "slice tricks" and copy & paste the formula.

It'll be fixed by next release because generics will enable writing a function that does that and that function will be in standard library.

Why wasn't this fixed before generics? Because the only way to do it would be via a compiler built-in function and Go team is rightfully reluctant to bloat the language / compiler for minor functionality like that.

Oof, I have no desire to spark a language war. Every language has its warts for sure.

It seems strange to not provide slice.erase as a primitive operation, and instead to recommend implementing it in terms of append. Usually removing from a vector does not require allocation. Still it may be for the best with upcoming generics, fewer builtins is good.

This is an interesting point. On the first glance, it looks like a very obvious missing piece and having to hand-write such expressions feels like unnecessary painful.

On the second glance, one can see why probably it was left out: there are two fundamental different ways to implement the deletion. You can copy the last element into the slot of the deleted element, or you can move all elements after the deleted one place to the left. The latter is what you implemented and preserves the ordering, which often is desirable. The former is way more efficient, if you don't need to preserve the order. A default implementation would probably choose to preserve the order and thus introduce a systematic inefficiency which is easy to be avoided.

Fortunately, with the introduction of generics and especially the new slice package, this discussion becomes moot.

Dealing with any kind of collections will often require you to write more code than you are used to, see the other post in this thread about someone complaining about a lack of Set type. Most of these problems are due to the fact that the language doesn't have generics yet, however generics are coming soon and once they do arrive we will have a robust collection and concurrent collection library to alleviate for most of these problems.

For now you just have to accept these quirks as part of the tradeoff of working with the language. I don't find it that big of a deal esp with the solution on the horizon.

It is not only hard for you, but it is also hard for a computer. And Go expresses this perfectly. Once you hide this behind a convenient `slice.Delete(x)`, you may end up with a codebase that bleeds performance.
With two years of working in Go at two companies, I have never had to write that. I can't remember writing the equivalent in Python over even more years, either. The 99% case is building a list/slice, not removing from one.

Have you ever had to remove elements from a list/slice?

Yes, a sorted vector is a common data structure for frequent queries and infrequent updates.
gopls seems to be integrating some of the "tricks" into editor completion. This one isn't there, but other idioms are. (With gopls > 0.7.0, it's enabled by default. Details: https://github.com/golang/tools/releases/tag/gopls%2Fv0.7.0)
I use slices all the time, but very rarely need to delete out of the middle of them. So it seems like a bit of an edge case to me.