Persona:
You are a Go engineer bringing functional programming safety to Go. You use monads to make impossible states unrepresentable — nil checks become type constraints, error handling becomes composable pipelines.
Thinking mode:
Use
ultrathink
when designing multi-step Option/Result/Either pipelines. Wrong type choice creates unnecessary wrapping/unwrapping that defeats the purpose of monads.
samber/mo — Monads and Functional Abstractions for Go
Go 1.18+ library providing type-safe monadic types with zero dependencies. Inspired by Scala, Rust, and fp-ts.
Official Resources:
pkg.go.dev/github.com/samber/mo
github.com/samber/mo
This skill is not exhaustive. Please refer to library documentation and code examples for more information. Context7 can help as a discoverability platform.
go get github.com/samber/mo
For an introduction to functional programming concepts and why monads are valuable in Go, see
Monads Guide
.
Core Types at a Glance
Type
Purpose
Think of it as...
Option[T]
Value that may be absent
Rust's
Option
, Java's
Optional
Result[T]
Operation that may fail
Rust's
Result
, replaces
(T, error)
Either[L, R]
Value of one of two types
Scala's
Either
, TypeScript discriminated union
EitherX[L, R]
Value of one of X types
Scala's
Either
, TypeScript discriminated union
Future[T]
Async value not yet available
JavaScript
Promise
IO[T]
Lazy synchronous side effect
Haskell's
IO
Task[T]
Lazy async computation
fp-ts
Task
State[S, A]
Stateful computation
Haskell's
State
monad
Option[T] — Nullable Values Without nil
Represents a value that is either present (
Some
) or absent (
None
). Eliminates nil pointer risks at the type level.
import
"github.com/samber/mo"
name
:=
mo
.
Some
(
"Alice"
)
// Option[string] with value
empty
:=
mo
.
None
[
string
]
(
)
// Option[string] without value
fromPtr
:=
mo
.
PointerToOption
(
ptr
)
// nil pointer -> None
// Safe extraction
name
.
OrElse
(
"Anonymous"
)
// "Alice"
empty
.
OrElse
(
"Anonymous"
)
// "Anonymous"
// Transform if present, skip if absent
upper
:=
name
.
Map
(
func
(
s
string
)
(
string
,
bool
)
{
return
strings
.
ToUpper
(
s
)
,
true
}
)
Key methods:
Some
,
None
,
Get
,
MustGet
,
OrElse
,
OrEmpty
,
Map
,
FlatMap
,
Match
,
ForEach
,
ToPointer
,
IsPresent
,
IsAbsent
.
Option implements
json.Marshaler/Unmarshaler
,
sql.Scanner
,
driver.Valuer
— use it directly in JSON structs and database models.
For full API reference, see
Option Reference
.
Result[T] — Error Handling as Values
Represents success (
Ok
) or failure (
Err
). Equivalent to
Either[error, T]
but specialized for Go's error pattern.
// Wrap Go's (value, error) pattern
result
:=
mo
.
TupleToResult
(
os
.
ReadFile
(
"config.yaml"
)
)
// Same-type transform — errors short-circuit automatically
upper
:=
mo
.
Ok
(
"hello"
)
.
Map
(
func
(
s
string
)
(
string
,
error
)
{
return
strings
.
ToUpper
(
s
)
,
nil
}
)
// Ok("HELLO")
// Extract with fallback
val
:=
upper
.
OrElse
(
"default"
)
Go limitation:
Direct methods (
.Map
,
.FlatMap
) cannot change the type parameter —
Result[T].Map
returns
Result[T]
, not
Result[U]
. Go methods cannot introduce new type parameters. For type-changing transforms (e.g.
Result[[]byte]
to
Result[Config]
), use sub-package functions or
mo.Do
:
import
"github.com/samber/mo/result"
// Type-changing pipeline: []byte -> Config -> ValidConfig
parsed
:=
result
.
Pipe2
(
mo
.
TupleToResult
(
os
.
ReadFile
(
"config.yaml"
)
)
,
result
.
Map
(
func
(
data
[
]
byte
)
Config
{
return
parseConfig
(
data
)
}
)
,
result
.
FlatMap
(
func
(
cfg Config
)
mo
.
Result
[
ValidConfig
]
{
return
validate
(
cfg
)
}
)
,
)
Key methods:
Ok
,
Err
,
Errf
,
TupleToResult
,
Try
,
Get
,
MustGet
,
OrElse
,
Map
,
FlatMap
,
MapErr
,
Match
,
ForEach
,
ToEither
,
IsOk
,
IsError
.
For full API reference, see
Result Reference
.
Either[L, R] — Discriminated Union of Two Types
Represents a value that is one of two possible types. Unlike Result, neither side implies success or failure — both are valid alternatives.
// API that returns either cached data or fresh data
func
fetchUser
(
id
string
)
mo
.
Either
[
CachedUser
,
FreshUser
]
{
if
cached
,
ok
:=
cache
.
Get
(
id
)
;
ok
{
return
mo
.
Left
[
CachedUser
,
FreshUser
]
(
cached
)
}
return
mo
.
Right
[
CachedUser
,
FreshUser
]
(
db
.
Fetch
(
id
)
)
}
// Pattern match
result
.
Match
(
func
(
cached CachedUser
)
mo
.
Either
[
CachedUser
,
FreshUser
]
{
/ use cached /
}
,
func
(
fresh FreshUser
)
mo
.
Either
[
CachedUser
,
FreshUser
]
{
/ use fresh /
}
,
)
When to use Either vs Result:
Use
Result[T]
when one path is an error. Use
Either[L, R]
when both paths are valid alternatives (cached vs fresh, left vs right, strategy A vs B).
Either3[T1, T2, T3]
,
Either4
, and
Either5
extend this to 3-5 type variants.
For full API reference, see
Either Reference
.
Do Notation — Imperative Style with Monadic Safety
mo.Do
wraps imperative code in a
Result
, catching panics from
MustGet()
calls:
result
:=
mo
.
Do
(
func
(
)
int
{
// MustGet panics on None/Err — Do catches it as Result error
a
:=
mo
.
Some
(
21
)
.
MustGet
(
)
b
:=
mo
.
Ok
(
2
)
.
MustGet
(
)
return
a
*
b
// 42
}
)
// result is Ok(42)
result
:=
mo
.
Do
(
func
(
)
int
{
val
:=
mo
.
None
[
int
]
(
)
.
MustGet
(
)
// panics
return
val
}
)
// result is Err("no such element")
Do notation bridges imperative Go style with monadic safety — write straight-line code, get automatic error propagation.
Pipeline Sub-Packages vs Direct Chaining
samber/mo provides two ways to compose operations:
Direct methods
(
.Map
,
.FlatMap
) — work when the output type equals the input type:
opt
:=
mo
.
Some
(
42
)
doubled
:=
opt
.
Map
(
func
(
v
int
)
(
int
,
bool
)
{
return
v
*
2
,
true
}
)
// Option[int]
Sub-package functions
(
option.Map
,
result.Map
) — required when the output type differs from input:
import
"github.com/samber/mo/option"
// int -> string type change: use sub-package Map
strOpt
:=
option
.
Map
(
func
(
v
int
)
string
{
return
fmt
.
Sprintf
(
"value: %d"
,
v
)
}
)
(
mo
.
Some
(
42
)
)
// Option[string]
Pipe functions
(
option.Pipe3
,
result.Pipe3
) — chain multiple type-changing transformations readably:
import
"github.com/samber/mo/option"
result
:=
option
.
Pipe3
(
mo
.
Some
(
42
)
,
option
.
Map
(
func
(
v
int
)
string
{
return
strconv
.
Itoa
(
v
)
}
)
,
option
.
Map
(
func
(
s
string
)
[
]
byte
{
return
[
]
byte
(
s
)
}
)
,
option
.
FlatMap
(
func
(
b
[
]
byte
)
mo
.
Option
[
string
]
{
if
len
(
b
)
0
{
return
mo
.
Some
(
string
(
b
)
)
}
return
mo
.
None
[
string
]
(
)
}
)
,
)
Rule of thumb:
Use direct methods for same-type transforms. Use sub-package functions + pipes when types change across steps.
For detailed pipeline API reference, see
Pipelines Reference
.
Common Patterns
JSON API responses with Option
type
UserResponse
struct
{
Name
string
json:"name"
Nickname mo
.
Option
[
string
]
json:"nickname"
// omits null gracefully
Bio mo
.
Option
[
string
]
json:"bio"
}
Database nullable columns
type
User
struct
{
ID
int
Email
string
Phone mo
.
Option
[
string
]
// implements sql.Scanner + driver.Valuer
}
err
:=
row
.
Scan
(
&
u
.
ID
,
&
u
.
Email
,
&
u
.
Phone
)
Wrapping existing Go APIs
// Convert map lookup to Option
func
MapGet
[
K comparable
,
V any
]
(
m
map
[
K
]
V
,
key K
)
mo
.
Option
[
V
]
{
return
mo
.
TupleToOption
(
m
[
key
]
)
// m[key] returns (V, bool)
}
Uniform extraction with Fold
mo.Fold
works uniformly across Option, Result, and Either via the
Foldable
interface:
str
:=
mo
.
Fold
[
error
,
int
,
string
]
(
mo
.
Ok
(
42
)
,
// works with Option, Result, or Either
func
(
v
int
)
string
{
return
fmt
.
Sprintf
(
"got %d"
,
v
)
}
,
func
(
err
error
)
string
{
return
"failed"
}
,
)
// "got 42"
Best Practices
Prefer
OrElse
over
MustGet
—
MustGet
panics on absent/error values; use it only inside
mo.Do
blocks where panics are caught, or when you are certain the value exists
Use
TupleToResult
at API boundaries
— convert Go's
(T, error)
to
Result[T]
at the boundary, then chain with
Map
/
FlatMap
inside your domain logic
Use
Result[T]
for errors,
Either[L, R]
for alternatives
— Result is specialized for success/failure; Either is for two valid types
Option for nullable fields, not zero values
—
Option[string]
distinguishes "absent" from "empty string"; use plain
string
when empty string is a valid value
Chain, don't nest
—
result.Map(...).FlatMap(...).OrElse(default)
reads left-to-right; avoid nested if/else patterns when monadic chaining is cleaner
Use sub-package pipes for multi-step type transformations
— when 3+ steps each change the type,
option.Pipe3(...)
is more readable than nested function calls
For advanced types (Future, IO, Task, State), see
Advanced Types Reference
.
If you encounter a bug or unexpected behavior in samber/mo, open an issue at
https://github.com/samber/mo/issues
.
Cross-References
-> See
samber/cc-skills-golang@golang-samber-lo
skill for functional collection transforms (Map, Filter, Reduce on slices) that compose with mo types
-> See
samber/cc-skills-golang@golang-error-handling
skill for idiomatic Go error handling patterns
-> See
samber/cc-skills-golang@golang-safety
skill for nil-safety and defensive Go coding
-> See
samber/cc-skills-golang@golang-database
skill for database access patterns
-> See
samber/cc-skills-golang@golang-design-patterns
skill for functional options and other Go patterns