Hacker News new | ask | show | jobs
by petree 1903 days ago
Having expicitly optional/nullable types is great in languages which are religious about it, since they remove a lot of useless branching and complexity from the code. If it's just tacked on, then it just tends to look ugly, without solving any problems.
1 comments

Syntactic sugar makes a big difference as well, e.g. Rust's `?` operator.
Simplicity in Go is one of it's most loved features. For the most part, there are fewer ways to do the same thing and I love that.
It's actually only superficial simplicity. There have been good comments on HN about this issue. Just because the language is "simple", it doesn't hide the complexity of reality, and written code ends up being harder and more verbose and more difficult to manage compared to powerful languages.
With all due respect those comments you’re referencing generally don’t know what they’re talking about. There is a lot hatred on this website for Go, which appears to have more to do with “I don’t understand why it’s designed this way, and I think all good languages should look like lisp/Haskell/rust” than “this design is net negative for developers”.

Practical simplicity is all about hiding complexity. Unless you’re building a race car, you don’t need to know the differences between file handling in Linux Mac and windows. It just never comes up. And when it does, it’s possible to peak under the hood.

A lot of the criticism of go mistakes “difficult to write” or “not trendy” for “bad design”, and again I assert this is because the critics don’t actually understand what Go is designed for, period.

With all due respect, I do understand why go was designed the way it was, and I vehemently disagree with those decisions. I used go for years, and with every passing day I grew more and more disenfranchised with the language.

GP is right. Eschewing abstractions in a programming language forces users of that language to deal with it themselves on a recurring basis. Millions of lines of

    if res, err := fn(...); err != nil {
        return nil, fmt.Errorf("...")
    }
don't help anyone, and only detract from readability which is bar none the most important part of a code base. Sadly, this is one of many symptoms in the language where problems that could have been solved in the language have instead been pushed down to its users to deal with over and over and over.
You claim to understand the decisions, so I’ll push you on that.

Why is go error handling designed the way it is? What are the intended benefits? What are the actual benefits?

A follow up, on your abstraction point: why does go eschew abstraction? Intended upside? Actual upside?

It’s very clear people in these threads can perceive downsides of some of Go’s decisions, but what about upsides? And further, can you recognize how Go copes with the downsides it’s choices produce?

I disagree. I've seen and wrote a lot of golang code, and it's a mess once the domain becomes complex. Those comments are saying the right thing.

Golang was designed without any regard to language developments since the 70s, and it shows. It still has null, and for no good reason. No proper enums, let alone pattern matching. These are mainstream features. The only reason golang became popular was because of branding. Its predecessor didn't go anywhere. I admit that concurrency is somewhat ok, but it lacks the expressiveness to make it much more useful. Java is implementing green threads, and is much better equipped to tackle this area (proper concurrent types, immutable types via records, better profiling, hierarchy management, etc.).

> Unless you’re building a race car, you don’t need to know the differences between file handling in Linux Mac and windows.

And golang does a terrible job at that abstraction: https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-...

> “I don’t understand why it’s designed this way, and I think all good languages should look like lisp/Haskell/rust”

False dichotomy. It's possible for languages to be better designed than golang, yet not be lisp/haskell/rust. Java has been making great strides in this area.

In the spirit of keeping this specific, and to demonstrate your understanding, I’d be curious how you’d answer these questions:

1. What is good about go’s file abstraction? What are the specific real world consequences (the article, which I was actually referencing in my comment, doesn’t deal with what happens in practice)?

2. What is the downside of increasing expressiveness? What is the downside of supporting sophisticated abstraction and type systems?

I don't think I know of a single thing where there are fewer ways of doing something in Go than there are in Java.

There are multiple ways to declare a variable, to pass a value to a function, to declare a constant, to create something similar to an enum, to return errors, to check for errors, to handle closing, to synchronize parallel threads of execution, to initialize a struct, to create a list of items. I can probably go on.

What are some examples where Go is simpler than Java, other than its current lack of generics which has always been a known-limitation?

There is one way to iterate over things, for any kind of elementwise processing: a `for` loop.

There is one way to format your code ;)

There are still two ways: the C-style for loop and its variants (for initializer; condition; increment) and the range for loop with its variants (iterate by key, by value, or both). There's also the option of writing a recrusive function.

Still less than Java's five (do-while, while, C-style for, range for, recursion), to be fair.

Which is simpler?

    if (ptr == null) {
        return nil, fmt.Errorf("bad");
    }

    val := *ptr
or

    let value = ptr?
If `?` is unacceptable complexity, how on earth do you deal with functions—or, even worse—control flow keywords?
For any complex codebase, people will build their own sugar and that may differ in implementation so it depends whether that is a good idea.

Subtle differences in similar looking code can trip people and increase complexity. Fortunately, go has a good standard library to compensate for some of it.

Relative lack of syntactic sugar is one of Go's best features though.
I dont think thats true at all. Even the sugar they have is strange: see `go` and `make`. Go has plenty of good features and "lack of ergonomic faculties for common programming idioms in Go" is not one of them.
`new` is basically syntactic sugar (and I personally rarely use it), but `make` is the only way to dynamically allocate or to pre-allocate slices and maps.
`?` makes code substantially less coherent, not more.
I would love to see some rationale behind this opinion.
I think the core of it is the belief that error handling is no less important than "happy path" code. In some domains, this isn't true. In mine, distributed systems, it is. So I don't want to relegate errors to some ghetto, I want them to be front and center, equal to everything else.

Another small part is probably how you think about the error values themselves. I almost never want to pass an error to my caller exactly as I receive it, I almost always want to do something to it first, most often decorating it with relevant context and metadata where I receive it. Sometimes, obscuring it, if I don't want to leak implementation details.

But ultimately it's about explicitness, obviousness. `?` is easy to miss, and permits method chaining, the outcome of which is incredibly easy to mispredict. And in imperative code, which is the supermajority of all code, `?` gives no meaningful increase in speed-of-reading -- which is a bogus metric, anyway. So for me, strongly net negative.

> I want them to be front and center, equal to everything else.

That’s fine, and that’s why the function itself will have a return type of `Result<T, E>` for some meaningful return type T and error type E.

Even better, if there’s an error, there is no non-error return value. You can’t accidentally use the zero-valued return half of a tuple (as you can in golang) because it simply isn’t there.

Is the important part of error handling having some copy-pasted stanza repeated everywhere? Or is it enforcing that errors are always handled and semantically-undefined return values are never accidentally passed along in the event of an error?

> But ultimately it's about explicitness, obviousness. `?` is easy to miss, and permits method chaining, the outcome of which is incredibly easy to mispredict.

No, it simply is not. `?` early-aborts the function and returns the result straight away if it’s an error, and unwraps the interior value if not. There is no plausible way for someone to mispredict this behavior, and if there was, it would be no different from golang, since the two constructs are semantically virtually identical. One is simply shorter than the other.

`?` is no less explicit than three lines of copy-pasted code and both its existence and behavior are forced due to the function’s return type.

> And in imperative code, which is the supermajority of all code, `?` gives no meaningful increase in speed-of-reading -- which is a bogus metric, anyway.

Ease of understandability is almost hands-down the most important metric given the ratio of frequency to code being read versus written. And to be completely blunt, it is flatly ridiculous that wrapping every line in nearly-identical error handling code somehow doesn’t impair comprehension. The argument is the same for abstractions like `map`, `select`, `reduce` et al. Intent and behavior of code can be understood at a glance when you remove the minutia of looping, bounds-checking, and indexing and focus on just the operation. And as an added bonus, you remove surface area for potential bugs like off-by-one or fencepost errors.

Having nearly identical error-handling everywhere both in theory and in practice obscures the places where something is different. It is hard to notice small differences in largely-identical blocks of visual information—hence the existence of “spot the difference" games—but it is trivial to spot when those differences are large.

I genuinely struggle to comprehend how people can have ideas like this when they fly in the face of what little hard evidence we do have about syntactic differences in programming.

> Even better, if there’s an error, there is no non-error return value. You can’t accidentally use the zero-valued return half of a tuple (as you can in golang) because it simply isn’t there.

That is better! But it's not as better as I think you think it is. The conventions are adequate, here.

> Is the important part of error handling having some copy-pasted stanza repeated everywhere? Or is it enforcing that errors are always handled and semantically-undefined return values are never accidentally passed along in the event of an error?

Neither, really: it's about having the error code path visually equivalent to the non-error code path.

> No, it simply is not. `?` early-aborts the function and returns the result straight away if it’s an error, and unwraps the interior value if not. There is no plausible way for someone to mispredict this behavior, and if there was, it would be no different from golang, since the two constructs are semantically virtually identical. One is simply shorter than the other.

I don't want early abort. Don't know how else to say it. If I have 5 operations, each of which can fail, I want them to be 5 visually distinct stanzas in my source, and I want to be able to manipulate the errors from each independently.

> Ease of understandability is almost hands-down the most important metric given the ratio of frequency to code being read versus written. And to be completely blunt, it is flatly ridiculous that wrapping every line in nearly-identical error handling code somehow doesn’t impair comprehension. The argument is the same for abstractions like `map`, `select`, `reduce` et al. Intent and behavior of code can be understood at a glance when you remove the minutia of looping, bounds-checking, and indexing

I'm sorry, but I just don't agree. You call looping, bounds-checking, index, etc. minutia, but I don't see it that way.