Handling Optional Values in Go Using the samber/mo Library
The samber/mo library brings functional programming abstractions, specifically monads like Option, to Go. A monad can be visualized as a wrapper that holds a value and defines a standard way to chain operations. This chain stops immediate if any step fails to produce a value, lifting the burden of repetitive nil checks and error propagation from the developer's logic. Option[T] represents a value of type T that may or may not exist, eliminating the direct use of nil pointers and making the data flow and potential absence of a value explicit in the type system.
Building Option Instances
Two primary constructors create an Option:
Some[T](value T): Wraps a concrete value into anOptionthat is present.None[T](): Produces anOptionthat holds nothing.
A common pipeline might involve a sequence of transformations where each stage relies on the success of the previous one. The code below implements this pattern using a different domain: processing a user entity. The process attempts to fetch a user, then validates their profile, and finally generates a greeting. A failure at any point short-circuits the rest of the chain.
package main
import (
"fmt"
"strings"
"github.com/samber/mo"
)
type Profile struct {
Name string
Email string
}
func fetchUser(id int) mo.Option[Profile] {
// Simulating a user found in the database
return mo.Some(Profile{Name: "Alex", Email: "alex@example.com"})
}
func validateProfile(p Profile) mo.Option[Profile] {
if strings.Contains(p.Email, "@") {
return mo.Some(p)
}
// Validation fails, returning empty Option
return mo.None[Profile]()
}
func formatGreeting(p Profile) mo.Option[string] {
// Simulating a formatting operation that always succeeds
greeting := fmt.Sprintf("Welcome, %s", p.Name)
return mo.Some(greeting)
}
func main() {
resultOption := mo.Some(42).FlatMap(func(id int) mo.Option[Profile] {
return fetchUser(id)
}).FlatMap(func(profile Profile) mo.Option[Profile] {
return validateProfile(profile)
}).FlatMap(func(profile Profile) mo.Option[string] {
return formatGreeting(profile)
})
finalMessage, isValid := resultOption.Match(
func(msg string) (string, bool) {
return msg, true
},
func() (string, bool) {
return "Could not generate greeting", false
},
)
if isValid {
fmt.Println(finalMessage)
} else {
fmt.Println(finalMessage)
}
}
If the email validation fails in validateProfile, the None propagates, and formatGreeting is never invoked.
Core Operations on Option[T]
Beyond FlatMap, the library offers a rich set of methods to inspect, extract, and transform the contained value:
Inspecting State
IsPresent(): returns true if the Option holds a value.IsAbsent(): returns true if the Option is empty.Size(): yields 1 for a presant Option, 0 otherwise.
Extracting Values
Get(): returns the inner value and a boolean flag indicating presence.MustGet(): returns the value but panics if the Option is None. Use only when certain of a value.OrElse(fallback T): providess the wrapped value or a default if None.OrEmpty(): returns the Option if present, or an empty Option for the same type.ToPointer(): converts the inner value to a pointer; returns nil for None.
Applying Side Effects
ForEach(fn func(T)): executes the given function only if a value exists, otherwise does nothing.
Transforming
Map(fn func(T) T1): applies a transformation to the value if present, returningSome[T1]; otherwise returnsNone[T1].MapNone(fn func() T): transforms an empty Option intoSome[T]; does nothing if the Option is already present.FlatMap(fn func(T) Option[T1]): similar to Map, but the transformation itself returns an Option, preventing nestedOption[Option[T]].
Conditional Execution
Match(onPresent func(T) (R, bool), onNone func() (R, bool)): evaluates one of two functions and returns both a result and a flag indicating which path was taken.
Serialization
MarshalJSON(): serializes the value as JSON; for None, it producesnull.UnmarshalJSON(data []byte): deserializes JSON into an Option.
Database Integration
Scan(src interface{}): implementssql.Scanner, allowing a database NULL to become a None.Value(): implementsdriver.Valuer, converting a None into an SQL NULL.