Hacker News new | past | comments | ask | show | jobs | submit login

> You're not supposed to return your custom error-like struct pointers from functions - you're supposed to return an "error" interface

Per https://github.com/golang/go/wiki/CodeReviewComments#interfa...:

    Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values. The implementing package should return concrete (usually pointer or struct) types

There clearly is this one exception ONLY for the `error` interface. This advice plus the weird runtime check conventions like `Is` / `As` are nowhere to be seen for other interfaces in the ecosystem.

Go makes a lot of effort so that errors are special. There is an exceptional tuple-type that exists solely for returning errors, and the error-return-interface convention breaks interface-return conventions. If they implemented sum-types, then we would have the typical `Result<T, Error>`, and then go programs would have much more invariants and be much more composable. But presumably compilation- and/or run-time would be worse.

Another alternative would have been adding syntax sugar like rust's `?` for `if err != nil { return ..., err }`, and `match err { case CustomError: ... case error: ... case nil ... }`. And some helpers for composing/piping functions.




There would be two major problems with sum types (especially for results) in Go 1.x:

- There are genuine use cases where returning both a value and an error makes complete sense. My favourite example is doing I/O: some bytes were copied, but there was a problem with the rest.

- It has significant overlap in scope & functionality with interface types. You could have "type Color enum { Named(string); RGB(int, int, int) }", or you could have "type Color interface { RGB() (int, int, int) }; type NamedColor string;" etc and it's not very clear which style you'd be supposed to use, and in what case.

The first problem could be somewhat alleviated by having very special result types that can have both a return value and an error, but these would have to be different from regular "variant" results, so you'd end up either with two ways to do a very similar thing, or yet another layer of abstraction.

The second problem is fundamental to the design of the language, and I would absolutely *hate* to see Go go through the same unholy mess as Python: "old-style" vs "new-style" classes, dataclasses (with third-party "attrs" as a stepping stone), protocols/generic containers, "match"/destructuring, all of these were clumsily grafted on and code that mixes all of these styles can be found all over the place. You do need to make ADTs and pattern matching a first-class citizen in 1.0, otherwise the place you end up in is not pretty at all.

Go? Yeah, we have a lot of SortIntegers([]int) and SortStrings([]string) and SortByName([]Person) in older code, but that's about its biggest sin. As of 1.21, slices.Sort & slices.SortFunc are in std and fixing this older code is pretty much a mechanical task. It's not perfect, but at least it's not worse by trying too hard to be perfect.


> There are genuine use cases where returning both a value and an error makes complete sense.

I don't feel anyone is saying that the addition of sum types would have to replace the existing (Result, error) pattern. Its just a tool in the toolbox. In fact, I think having both is really interesting from a function signature communication perspective; if a function returns (Result, error) in a post-sum-type world, that hints to me that the Result might still be useful even if an error is returned.

> I would absolutely hate to see Go go through the same unholy mess as Python: "old-style" vs "new-style" classes,

Anytime someone brings up how horrible the Py2 to Py3 transition was, I will remind them that Python is the most popular programming language in the world. Clearly; the transition wasn't actually bad enough to negatively impact its popularity; yet its the token example of "don't break existing programs, you don't want to be like Python".

That take is just counter-intuitively wrong. Its like the SDLC paradigm of "releasing often reduces bugs"; it feels wrong, but its actually right. I'm sure saying "we'll never break existing programs" imparts a nice warm feeling in your heart, "we're mature, unlike those other dumb languages". But there is extremely little evidence that backward-incompatible language changes hurt the adoption of programming languages. Actually; there's substantially more evidence that languages which can't adapt and evolve to change will eventually die.

I'm not asserting that will happen to Go; I think they're good about bringing forward enhancements to the language in ways that don't break existing programs (like generics). But I also strongly believe they need to rethink their "we won't break existing programs" rule. I am begging the Go team to break my programs. Its not a big deal. I just can't imagine still writing Go programs in 2050 like we are today; it leaves so much value on the table, and we can do better.


> My favourite example is doing I/O: some bytes were copied, but there was a problem with the rest.

How do you reconcile that with the general rule or convention that `if err != nil` the value is unsafe to use?

Apart from this, I don't think sum-types should be added this late either. I should have written "if they had implemented". "old vs new style" does more damage than good. Today, it is what it is.

What I do think is feasible is adding some more syntax sugar around errors.


> How do you reconcile that with the general rule or convention that `if err != nil` the value is unsafe to use?

By reading the documentation: https://pkg.go.dev/io#Reader

Conventions are not laws, and should not be followed blindly.

> What I do think is feasible is adding some more syntax sugar around errors.

The most common example I hear is something like Rust's `?` operator for propagating errors. In Go, errors should almost always be expanded with additional context, and programmers should think hard about errors and how to handle them. Syntax sugar would just make it easier for people to ignore errors. And you can't ignore errors forever - they always come up, most often in the place you least expect them.


> In Go, errors should almost always be expanded with additional context

I'm convinced this is just a very poor way to cope with the lack of stacktraces.


Actually, stack traces are a very poor way to provide context.

I can't count how many times I've worked with some library in ${LANGUAGE_WITH_EXCEPTIONS}, and had an exception print out a giant stack trace, only for me to realize that the stack trace is useless because the place where exception is raised isn't the place where the error actually happened, but is actually some wrapper/worker collector/other kind of indirection mechanism. In other words, stack traces are directly bound to call stacks, and call stacks don't necessarily contain all relevant context of an error - they only do so in case of simple, single-threaded programs.

Golang-style error-as-values actually provide real, human-curated context that is relevant to the operation at hand. They can be passed between goroutines, and are completely independent of any call stacks. That, in my opinion, makes them vastly superior to stack traces.


It's true that stacktraces don't work out so well in many languages. But ironically in go they do seem to work fine in my experience. Adding a stacktrace to custom go errors do show me correct and useful traces across goroutines. I guess it's the fact that async is built into the language as a primitive or something.

Regarding wrapping errors, if you don't provide a stacktrace or at least file:line, how do you actually map the error to source code? Do you just grep and pray that the message is unique enough?


> Do you just grep and pray that the message is unique enough?

If you're new to the codebase, yes, grepping can get you a long way.

In my experience, just following the error messages from the top (usually errors are printed only in the main function) is enough. Hypothetical example, if you have a file sync program, and you get an error "file sync failed: device Foo unreachable: connect error: quic://123.45.67.89/ i/o timeout", you can already mentally map where exactly the error has happened. The exact file and line should be easy to locate.

Of course, if a codebase doesn't write descriptive enough errors, or just propagates them without context, error diagnosis is going to be difficult. But that's why Go encourages people to think about errors and not ignore them.




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

Search: