A simple result type in Golang 1.18
Bob Fang
Posted on December 14, 2021
With the introduction of generics in Go 1.18. One of the pain point I had with Golang has been finanly resolved.
In my day job, there are a lot of times I need to implement a "map-reduce" style algorithm using goroutines. Some thing like this:
func processing() {
works := []Work{
{
Name: "John",
Age: 30,
},
{
Name: "Jane",
Age: 25,
},
...
}
var wg sync.WaitGroup
result := make(chan ProcessedWork, len(works))
for _, work := range works {
wg.Add(1)
go func(work Work) {
defer wg.Done()
// do something
newData, err := doSomething(work)
result <- newData
}(work)
}
wg.Wait()
close(result)
for r := range result {
// combine results in some way
}
return ...
}
This may looks like all good, but there is a problem. What if one of the sub goroutine failed, what if this line actually return an error?
newData, err := doSomething(work)
So usually you have two options, one is to simply introduce a seperate error channel, and the next is to introduce a new Result type locally and change the result
channel here to accept the Result
type. In this example we can do this:
type Result struct {
Data ProcessedWork
Err error
}
result := make(chan Result, len(works))
I usually opt for the second solution, but having to define this type every time is a pain. There were some suggestions to use interface{}
to represent the Data and do type assertion when using the data, but generally I am not a big fan of using interface{}
in Go.
Luckily we got generics in Go 1.18, so our Result
type can be defined using this feature.
type Result[T any] struct {
value T
err error
}
Having this type is much better than using ad-hoc Result
types for each processing
function in the code base.
A number of useful methods can be added to the Result
type, for example
func (r Result[T]) Ok() bool {
return r.err == nil
}
func (r Result[T]) ValueOr(v T) T {
if r.Ok() {
return r.value
}
return v
}
func (r Result[T]) ValueOrPanic() T {
if r.Ok() {
return r.value
}
panic(r.err)
}
There are some obvious things I want to point out though.
- Golang does not do sum type yet, there are proposals to add this, but I think it will come much later. Currenlty the best way to emulate a sum type in Golang is to simply use a struct and remeber which field you have used, like what we did here with the Result type. Other languages call this discrimated union if you include a special field which denote the field being used.
-
Result
type, no matter what method you use to implement it, is not a new concept. C++ hasstd::optional
since C++17.Rust
, the golden boy of this era, hasResult<T, E>
. Haskell, where I first learned the conecpt of using types to represent the outcome of an operation that may or may not success, hasMaybe
.
Further to point 2, another thing to notice is that Result
is actually a monad, C++23
recently added monadic operation for std::optional
, and Hasekll has the following functions in its stdlib since long before this conecpt is popular.
return :: a -> Maybe a
return x = Just x
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
(>>=) m g = case m of
Nothing -> Nothing
Just x -> g x
The nice thing about these two functions, especially for the second function (>>=)
, is that it allows you to easily combine/chain multiple operations that may or may not yield a result together without the need to keep using if
to check if the result of last operation is Ok
or not. I am not going to enumerate an example, but if you are curious you can look at the Hasekll example here.
But Golang is a bit lacking, at this moment, if everything goes according to the plan, then we will not be able to have another idependent type variable in a generic type's method. So we cannot have something like this:
func (r Result[T]) Then(f func(T) Result[S]) Result[S] { // <-- S is not allowed, we can only use T
if r.Ok() {
return f(r.value)
}
return r
}
IMO, this restriction quite serverly limited the usefulness of the result type.
Another thing I wish we had is C++ style "partial specialization" in Go's generics. For now the constraints for Result
is any
, but I do want to provide a function like this for user:
func (r Result[T]) Eq(v T) bool {
if r.Ok() {
return r.value == v
}
return false
}
But since T
is not comparable
, the ==
operatior will not work. If Go can provide a way to refine the constraints for some methods of a generic type, then it would be a nice feature. E.g.
// here we refined the T type from any to comparable by providing a more precise constraints in the method receiver type
// now only Result that holds a T that are in the constraint comparable will have this method enabled.
func (r Result[T comparable]) Eq(v T) bool {
if r.Ok() {
return r.value == v
}
return false
}
Sample code can be found here
Posted on December 14, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024