Using product types instead of sum types to represent "success or error" is definitely something wrong with it. Product types should be used for "and", and sums should be used for "or".
Because when you return `foo, err` , your caller has to remember not to use the foo.
And when you're constructing your return values, often if you have an error, you won't have a real `foo` to give back, which raises the temptation to return a pointer-to-foo instead so you can give nil in that case.
But then your caller has to check for the nonsense case where there's no error but also a nil foo.
Usually what is meant is "I'll return a foo _or_ an error" but what go's type system encodes is "I'll give you a foo and maybe an error" or "I'll give you maybe a foo and maybe an error" depending on what choice you make.
Because the typical situation with error handling is that there are two possible cases: (a) a function succeeds with some result, or (b) it fails with an error. A sum type represents those two possibilities exactly. In contrast, a product type says "I have both a result and an error", which makes no sense. However, if things can be nil, which is the norm in Go, then you can represent both (a) and (b) with a product type, but you also have two additional cases to worry about: (c) both the result and the error are present, and (d) both are nil (and what are you supposed to do in that situation?).
When using static types, it is best to make as few nonsensical/illegal/invalid states representable as possible, so that you can rely on the compiler enforcing that they won't happen. Functional programmers have been saying this for decades, and these days Rust programmers are generally onboard with this too.
Sum types are the categorical dual of product types, so it always seems unnatural to me when a language only has the latter and not the former. I think part of the problem is we don't teach category theory to programmers, so they never stop to consider the duals of things.
I'd say the problem is many programmers don't learn enough languages to know what they're missing, or if they do they only get as far as languages with the same semantics but slightly different syntax.
> In contrast, a product type says "I have both a result and an error", which makes no sense
As an aside, there are many situations where this does make sense. Consider even the lowly Write, which returns (int, error). On a partial write, you will get the number of bytes written successfully (the result) and the error encountered.
I agree that most result/errors are sum types, but part of the philosophy of Go the programmer is ultimately in charge, and that while types are important, they can’t substitute for an actual understanding of the semantics of a given function call, etc. For example, even if Write was a sum type, what would a result of zero mean?
So while there’s value in reducing the invalid states that representable, there’s also diminishing returns. So the question is where to draw the line in the language. I agree that having sum types would be convenient, but I’m not sure that Go has made the wrong choice here, given that it seems to have found a really productive balance.
counts as a legitimate error handling strategy, you either have an extremely low bar set or haven't used Go much. Then again, people seem to be okay with Golang allowing nil pointer exceptions to exist in 2021, so maybe the bar is underground.