Most active commenters
  • kace91(3)
  • tuetuopay(3)

←back to thread

Go is still not good

(blog.habets.se)
644 points ustad | 18 comments | | HN request time: 0.408s | source | bottom
Show context
blixt ◴[] No.44983245[source]
I've been using Go more or less in every full-time job I've had since pre-1.0. It's simple for people on the team to pick up the basics, it generally chugs along (I'm rarely worried about updating to latest version of Go), it has most useful things built in, it compiles fast. Concurrency is tricky but if you spend some time with it, it's nice to express data flow in Go. The type system is most of the time very convenient, if sometimes a bit verbose. Just all-around a trusty tool in the belt.

But I can't help but agree with a lot of points in this article. Go was designed by some old-school folks that maybe stuck a bit too hard to their principles, losing sight of the practical conveniences. That said, it's a _feeling_ I have, and maybe Go would be much worse if it had solved all these quirks. To be fair, I see more leniency in fixing quirks in the last few years, like at some point I didn't think we'd ever see generics, or custom iterators, etc.

The points about RAM and portability seem mostly like personal grievances though. If it was better, that would be nice, of course. But the GC in Go is very unlikely to cause issues in most programs even at very large scale, and it's not that hard to debug. And Go runs on most platforms anyone could ever wish to ship their software on.

But yeah the whole error / nil situation still bothers me. I find myself wishing for Result[Ok, Err] and Optional[T] quite often.

replies(18): >>44983384 #>>44983427 #>>44983465 #>>44983479 #>>44983531 #>>44983616 #>>44983802 #>>44983872 #>>44984433 #>>44985251 #>>44985721 #>>44985839 #>>44986166 #>>44987302 #>>44987396 #>>45002271 #>>45002492 #>>45018751 #
1. kace91 ◴[] No.44983531[source]
My feeling is that in terms of developer ergonomics, it nailed the “very opinionated, very standard, one way of doing things” part. It is a joy to work on a large microservices architecture and not have a different style on each repo, or avoiding formatting discussions because it is included.

The issue is that it was a bit outdated in the choice of _which_ things to choose as the one Go way. People expect a map/filter method rather than a loop with off by one risks, a type system with the smartness of typescript (if less featured and more heavily enforced), error handling is annoying, and so on.

I get that it’s tough to implement some of those features without opening the way to a lot of “creativity” in the bad sense. But I feel like go is sometimes a hard sell for this reason, for young devs whose mother language is JavaScript and not C.

replies(3): >>44983704 #>>44986562 #>>44991271 #
2. j1elo ◴[] No.44983704[source]
> People expect a map/filter method

Do they? After too many functional battles I started practicing what I'm jokingly calling "Debugging-Driven Development" and just like TDD keeps the design decisions in mind to allow for testability from the get-go, this makes me write code that will be trivially easy to debug (specially printf-guided debugging and step-by-step execution debugging)

Like, adding a printf in the middle of a for loop, without even needing to understand the logic of the loop. Just make a new line and write a printf. I grew tired of all those tight chains of code that iterate beautifully but later when in a hurry at 3am on a Sunday are hell to decompose and debug.

replies(5): >>44983856 #>>44984023 #>>44984735 #>>44985816 #>>44986632 #
3. williamdclt ◴[] No.44983856[source]
I'll agree that explicit loops are easier to debug, but that comes at the cost of being harder to write _and_ read (need to keep state in my head) _and_ being more bug-prone (because mutability).

I think it's a bad trade-off, most languages out there are moving away from it

replies(1): >>44985443 #
4. kace91 ◴[] No.44984023[source]
I'm not a hard defender of functional programming in general, mind you.

It's just that a ridiculous amount of steps in real world problems can be summarised as 'reshape this data', 'give me a subset of this set', or 'aggregate this data by this field'.

Loops are, IMO, very bad at expressing those common concepts briefly and clearly. They take a lot of screen space, usually accesory variables, and it isn't immediately clear from just seing a for block what you're about to do - "I'm about to iterate" isn't useful information to me as a reader, are you transforming data, selecting it, aggregating it?.

The consequence is that you usually end up with tons of lines like

userIds = getIdsfromUsers(users);

where the function is just burying a loop. Compare to:

userIds = users.pluck('id')

and you save the buried utility function somewhere else.

5. lenkite ◴[] No.44984735[source]
This depends on the language and IDE. Intellij Java debugger is excellent at stream debugging.
6. nasretdinov ◴[] No.44985443{3}[source]
There's actually one more interesting plus for the for loops that's not quite obvious in the beginning: the for-loops allow to do perform a single memory pass instead of multiple. If you're processing a large enough list it does make a significant difference because memory accesses are relatively expensive (the difference is not insignificant, the loop can be made e.g. 10x more performant by optimising memory accesses alone).

So for a large loop the code like

for i, value := source { result[i] = value * 2 + 1 }

Would be 2x faster than a loop like

for i, value := source { intermediate[i] = value * 2 }

for i, value := intermediate { result[i] = value + 1 }

replies(2): >>44985804 #>>44985910 #
7. tuetuopay ◴[] No.44985804{4}[source]
Depending on your iterator implementation (or, lackthere of), the functional boils down to your first example.

For example, Rust iterators are lazily evaluated with early-exits (when filtering data), thus it's your first form but as optimized as possible. OTOH python's map/filter/etc may very well return a full list each time, like with your intermediate. [EDIT] python returns generators, so it's sane.

I would say that any sane language allowing functional-style data manipulation will have them as fast as manual for-loops. (that's why Rust bugs you with .iter()/.collect())

replies(2): >>44985862 #>>44986036 #
8. tuetuopay ◴[] No.44985816[source]
Rust has `.inspect()` for iterators, which achieves your printf debugging needs. Granted, it's a bit harder for an actual debugger, but support's quite good for now.
9. Capricorn2481 ◴[] No.44985862{5}[source]
Clojure transducers as well.
10. kace91 ◴[] No.44985910{4}[source]
This is a very valid point. Loops also let you play with the iteration itself for performance, deciding to skip n steps if a condition is met for example.

I always encounter these upsides once every few years when preparing leetcode interviews, where this kind of optimization is needed for achieving acceptable results.

In daily life, however, most of these chunks of data to transform fall in one of these categories:

- small size, where readability and maintainability matters much more than performance

- living in a db, and being filtered/reshaped by the query rather than code

- being chunked for atomic processing in a queue or similar (usual when importing a big chunk of data).

- the operation itself is a standard algorithm that you just consume from a standard library that handless the loop internally.

Much like trees and recursion, most of us don’t flex that muscle often. Your mileage might vary depending of domain of course.

replies(1): >>44986511 #
11. maleldil ◴[] No.44986036{5}[source]
Python map/filter/zip/etc. return generators, so they're lazily evaluated.
replies(1): >>44987089 #
12. empath75 ◴[] No.44986511{5}[source]
There's also that rust does a _lot_ of compiler optimizations on map/filter/reduce and it's trivially parallelizable in many cases.
13. dkarl ◴[] No.44986562[source]
> The issue is that it was a bit outdated in the choice of _which_ things to choose as the one Go way

I agree with this. I feel like Go was a very smart choice to create a new language to be easy and practical and have great tooling, and not to be experimental or super ambitious in any particular direction, only trusting established programming patterns. It's just weird that they missed some things that had been pretty well hashed out by 2009.

Map/filter/etc. are a perfect example. I remember around 2000 the average programmer thought map and filter were pointlessly weird and exotic. Why not use a for loop like a normal human? Ten years later the average programmer was like, for loops are hard to read and are perfect hiding places for bugs, I can't believe we used to use them even for simple things like map, filter, and foreach.

By 2010, even Java had decided that it needed to add its "stream API" and lambda functions, because no matter how awful they looked when bolted onto Java, it was still an improvement in clarity and simplicity.

Somehow Go missed this step forward the industry had taken and decided to double down on "for." Go's different flavors of for are a significant improvement over the C/C++/Java for loop, but I think it would have been more in line with the conservative, pragmatic philosophy of Go to adopt the proven solution that the industry was converging on.

replies(1): >>44989361 #
14. const_cast ◴[] No.44986632[source]
Just use a real debugger. You can step into closures and stuff.

I assume, anyway. Maybe the Go debugger is kind of shitty, I don't know. But in PHP with xdebug you just use all the fancy array_* methods and then step through your closures or callables with the debugger.

15. tuetuopay ◴[] No.44987089{6}[source]
Thanks, I was not sure, hence the "may". Comment edited :)
16. throwaway920102 ◴[] No.44989361[source]
Go Generics provides all of this. Prior to generics, you could have filter, map, reduce etc but you needed to implement them yourself once in a library/pkg and do it for each type.

After Go added generics in version 1.18, you can just import someone else's generic implementations of whatever of these functions you want and use them all throughout your code and never think about it. It's no longer a problem.

replies(1): >>44989778 #
17. dkarl ◴[] No.44989778{3}[source]
The language might permit it now, but it isn't designed for it. I think if the Go designers had intended for map, filter, et al to replace most for loops, they would have designed a more concise syntax for anonymous functions. Something more along the lines of:

    colors := items.Filter(_.age > 20).Map(_.color)
Instead of

    colors := items.Filter(func(x Item){ return x.age > 20 }).Map(func(x Item){ return x.color })
which as best as I can tell is how you'd express the same thing in Go if you had a container type with Map and Filter defined.
18. javier2 ◴[] No.44991271[source]
The lack of stack traces in Go is diabolical for all the effort we have to out in by manually passing every error