It amazes me that in 2023 this is not a solved problem by design of the language. Why go doesn’t adapt the “optional” notion of other languages so that if you have a variable you either know it is not null or know that you must check for nullness. The technology exists
I write a lot of Go and used to write a lot of Swift. Swift is what you’ll consider a modern language (optionals, generics, strong focus on value types), while Go is Go.
I appreciate both languages, and of course Swift feels like what you’d pick any day.
But, after using both nearly side by side and comparing the experience directly, I’ve got to say, I’m so much more productive in Go, there’s SO much less mental burden when writing the code, — and it does not result in more bugs or other sorts of problems.
Thing is, I, of course, am always thinking about types, nullability and the like. The mental type model is pretty rich. But the more intricacies of it that I have to explain to the compiler, the more drag I feel on getting things shipped.
And because Go is so simple, idiomatic, and basically things are generally as I expect them to be, maintenance is not an issue either. Yes, occasionally you are left wondering if a particular field can or cannot be nil / invalid-zero-value, but those cases are few enough to not become a problem.
This is a popular view, but, again, does not match my experience. I have only lead small teams (say 3 to 10 people) of either senior or very intelligent and motivated middles, but for those, limitations of Go are not a problem in any shape or form. Comparatively, we had significantly more mess (and debates) in Swift.
Try (the current incarnation of the ? operator) is actually a very clever trait which does rather more than that.
Types for which Try is implemented can Try::branch() to get a ControlFlow, a sum type representing the answer to the question "Stop now or keep going?". In the use you're thinking of where we're using ? on a Result, if we're Err we should stop now, returning the error, whereas if we're OK we should keep going.
And that's why this works in Rust (today), when you write doSomething()? the Try::branch() is executed for your Result and resolves into a Break or a Continue which is used to decide to return immediately with an error or continue.
But this is also exactly the right shape for other types in situations where failure has the opposite expectation, and we should keep going if we failed, hoping to succeed later, but stop early if we have a good answer now.
A big problem with Try is the function signatures...excuse me, I would like a <<T as Try>::Residual as FromResidual<Result<T, !>>::Output, please. Yes, that is a caricature and I don't know the proper signatures, but c'mon. Read the discussion for the Try v2 RFC if you want a better idea.
...and then they add more syntax sugar to partly sweep the complexity under the rug. I like Rust as much as the next person, but I'm apprehensive about how this will play out.
That won't work because in Go you often need to wrap errors with additional context.
I have worked with Rust Option/Rust types and found them extremely unergonomic and painful. The ?s and method chains are an eyesore. Surely PLT has something better for us.
There are several language design problems solved in the 20th century that Go designers decided to ignore, because they require PhD level skills to master, apparently.
Hence why the language is full of gotchas like these.
Had it not been for Docker and Kubernetes success, and most likely it wouldn't have gotten thus far.
speaking from personal experience, i selected go for a project because it is high perf, automatically uses all cores w/ goroutines, and is type checked
I agree I probably should have said strongly typed instead of safe, as yes, if you dereference a pointer to nil you are going to crash. That being said, I do think "possesses an untyped nil" is a pretty far cry from "not type checked at all". It's certainly much safer than languages like C or C++ which allow type punning, or Java, where both nullables and runtime exceptions associated with types are generally a more pernicious problem.
That's what the `func foo() (*T, error)` pattern is for. It's actually better than syntactic sugar for optional values because now you also have a descriptive reason for why the value is nil.
But if you really cannot afford to return more than one bit of information, do `func foo() (*T, bool)`.
Result<T,E> does this. I forget exactly why Result is actually different from, and in fact superior to, `func foo() (*T, error)` but IIRC it has to do with function composition and concrete vs generic types.
Result<T,E> is in one of two states: It either has value of type T, or error of type E.
(*T, error) is either T (non-nil, nil), or error (nil/undefined, non-nil), or both (non-nil, non-nil), or neither (nil, nil). By convention usually only the first two are used, but 1) not always, 2) if you rely on convention why even have type system, I have conventions in Python.
Leaving aside pattern matching and all other things which make Rust way more ergonomic and harder to misuse, Go simply lacks a proper sum type that can express exactly one of two options and won't let you use it wrong. Errors should have been done this way from the start, all the theory was known and many practical implementations existed.
I don't know much Rust, but wouldn't the analogy to (*T, error) be Result<Option<T>, E>, which has 3 states? Or is that not a common construct?
Because *T could be nil or non-nil, it seems like the analogy would be a nullable type in the Result<>. In Go, (T, error) would only have the states (non-nil, nil) and (non-nil, non-nil) if T is not a pointer. Still, the Result type seems better to me because the type itself is encapsulating all of this (and the error I guess cannot be null).
As others have mentioned, Result is a sum type so you either have a T or an E, there's no situation in which you can get both or neither.
The second part is that it's reified as a single value, so it works just fine as a normal value e.g. you can map a value to a result, or put results in a map, etc... , language doesn't really care.
And then you can build fun utilities around it e.g. a transformer from Iterator<Item=Result<T, E>> to Result<Vec<T>, E> (iterates and collects values until it encounters an error, in which case it aborts and immediately returns said error).
Don’t rely on half remembering how specific languages implement things, try and internalise the fundamentals. Go functions tend to return a tuple which is a product type, while rust’s result type is sum type. Product types contain. Both things (a result and an error) while a sum type contains a result or an error.