Nil-safe access to deeply nested struct pointer chains in Go.
No more if a != nil && a.B != nil && a.B.C != nil boilerplate.
Installation • Quick Start • API • Benchmark • 中文文档
Go has no optional chaining. Accessing req.A.B.C.D requires checking every pointer:
var token string
if req != nil && req.Auth != nil && req.Auth.Key != nil &&
req.Auth.Key.Session != nil && req.Auth.Key.Session.Token != nil {
token = *req.Auth.Key.Session.Token
}safechain eliminates this boilerplate with two approaches:
| Approach | Use When | Overhead |
|---|---|---|
Safe / Must / OrVal |
Don't need to know which field is nil | ~4 ns, 0 alloc |
Dig + S() |
Need precise error: which field was nil | ~190 ns/100 depth, 0 alloc |
Ensure + Set |
Build/assign deeply nested structs without nil checks | ~4 ns, 0 alloc |
go get github.com/mredencom/safechainimport "github.com/mredencom/safechain"
// Read — returns zero value if any pointer is nil
token := safechain.Must(func() string {
return *req.Auth.Key.Session.Token
})
// Read — with fallback
token := safechain.OrVal(func() string {
return *req.Auth.Key.Session.Token
}, "N/A")
// Read — comma-ok style
token, ok := safechain.Safe(func() string {
return *req.Auth.Key.Session.Token
})
// Write — build nested struct and assign, no nil checks
var req Request
safechain.E(&safechain.E(&safechain.E(&req.Auth).Key).Session).Token = ptr("my_token")// Returns (value, ok) — use * to dereference pointer fields like *string
val, ok := Safe(func() string { return *req.Auth.Key.Session.Token })
// Returns value or zero
val := Must(func() string { return *req.Auth.Key.Session.Token })
// Returns value or fallback
val := OrVal(func() string { return *req.Auth.Key.Session.Token }, "N/A")
// For non-pointer fields (e.g. int), no * needed
count, ok := Safe(func() int { return req.Auth.Key.Session.RetryCount })// ALL conditions must be true
ok := And(
Check(func() { _ = *req.Auth.Key.Session.Token }),
HasPrefix(func() string { return *req.Auth.Key.Session.Token }, "Bearer"),
Gt(func() int { return *req.Auth.RetryCount }, 0),
)
// At least ONE condition is true (Or is an alias for Any)
ok := Or(
Check(func() { _ = *req.Auth.Key.Session.Token }),
Check(func() { _ = *req.Fallback }),
)
// Negate a condition
ok := And(
Check(func() { _ = *req.Auth.Key.Session.Token }),
Not(HasPrefix(func() string { return *req.Auth.Key.Session.Token }, "Bearer")),
)
// ALL conditions must be false
ok := None(
Check(func() { _ = *req.BannedToken }),
Check(func() { _ = *req.ExpiredToken }),
)
// Count how many conditions are true
n := Count(
Check(func() { _ = *req.Auth.Key.Session.Token }),
Check(func() { _ = *req.Fallback }),
Check(func() { _ = *req.Meta.TraceID }),
)
// At least N conditions must be true
ok := AtLeast(2,
Check(func() { _ = *req.Auth.Key.Session.Token }),
Check(func() { _ = *req.Fallback }),
Check(func() { _ = *req.Meta.TraceID }),
)
// NotNil — simplified nil check, no need for _ = or *
ok := NotNil(func() any { return req.Auth.Key.Session })// First non-nil value wins (like SQL COALESCE)
token := MustFirst(
func() string { return *req.Auth.Key.Session.Token },
func() string { return *req.Fallback },
func() string { return "anonymous" },
)Use * to dereference pointer fields (e.g. *string, *int). For value fields (e.g. int, string), no * needed.
// *r.A.Name — Name is *string, needs *
Eq(func() string { return *r.A.Name }, "admin")
Ne(func() string { return *r.A.Name }, "guest")
// *r.A.Score — Score is *int, needs *
Gt(func() int { return *r.A.Score }, 10)
Gte(func() int { return *r.A.Score }, 10)
Lt(func() float64 { return *r.A.Rate }, 3.14)
Lte(func() float64 { return *r.A.Rate }, 3.14)
Between(func() int { return *r.A.Score }, 1, 100)
// r.A.Count — Count is int (value type), no * needed
Gt(func() int { return r.A.Count }, 0)
// Interval variants
BetweenExcl(func() int { return *r.A.Score }, 0, 100) // (0, 100) open
BetweenLExcl(func() int { return *r.A.Score }, 0, 100) // (0, 100] left-open
BetweenRExcl(func() int { return *r.A.Score }, 0, 100) // [0, 100) right-open
// Custom predicate
Match(func() string { return *r.A.Name }, func(v string) bool { return len(v) > 3 })HasPrefix(func() string { return *r.A.Name }, "hello")
HasSuffix(func() string { return *r.A.Name }, "world")
Contains(func() string { return *r.A.Name }, "llo_wor")
EqFold(func() string { return *r.A.Name }, "HELLO")
MatchRegexp(func() string { return *r.A.Name }, `^\d+$`)
MatchRegexpCompiled(func() string { return *r.A.Name }, re) // pre-compiled, for hot pathsBytesHasPrefix(func() []byte { return *r.A.Data }, []byte("hello"))
BytesHasSuffix(func() []byte { return *r.A.Data }, []byte("world"))
BytesContains(func() []byte { return *r.A.Data }, []byte("llo"))
BytesEq(func() []byte { return *r.A.Data }, []byte("exact"))
BytesMatchRegexp(func() []byte { return *r.A.Data }, `\d+`)
BytesMatchRegexpCompiled(func() []byte { return *r.A.Data }, re)No unsafe.Pointer needed. Use F() to define each step:
// MustSafeDig — just check existence, returns bool
ok := MustSafeDig(req,
F("Auth", func(r *Request) any { return r.Auth }),
F("Key", func(a *Auth) any { return a.Key }),
F("Session", func(k *Key) any { return k.Session }),
)
// SafeDig — get value + bool
token, ok := SafeDig[string](req,
F("Auth", func(r *Request) any { return r.Auth }),
F("Key", func(a *Auth) any { return a.Key }),
F("Session", func(k *Key) any { return k.Session }),
F("Token", func(s *Session) any { return s.Token }),
)
// SafeDigErr — get value + *NilError pinpointing the nil field
token, err := SafeDigErr[string](req,
F("Auth", func(r *Request) any { return r.Auth }),
F("Key", func(a *Auth) any { return a.Key }),
)
// err: nil pointer at field "Key" in path "Auth.Key"For hot paths where allocation matters:
import "unsafe"
val, err := safechain.Dig[string](req,
safechain.S("Auth", func(r *Request) unsafe.Pointer { return unsafe.Pointer(r.Auth) }),
safechain.S("Key", func(a *Auth) unsafe.Pointer { return unsafe.Pointer(a.Key) }),
safechain.S("Session", func(k *Key) unsafe.Pointer { return unsafe.Pointer(k.Session) }),
safechain.S("Token", func(s *Session) unsafe.Pointer { return unsafe.Pointer(s.Token) }),
)All comparison and matcher functions return bool, so they plug directly into And/Or:
ok := And(
HasPrefix(func() string { return *req.Auth.Key.Session.Token }, "Bearer"),
Gt(func() int { return *req.Auth.RetryCount }, 0),
Check(func() { _ = *req.Meta.TraceID }),
)// Safely get a value and transform it
upper, ok := Map(func() string { return *r.A.Name }, strings.ToUpper)
upper := MustMap(func() string { return *r.A.Name }, strings.ToUpper)
length := MustMap(func() string { return *r.A.Name }, func(s string) int { return len(s) })In(func() string { return *r.A.Role }, "admin", "superadmin")
NotIn(func() string { return *r.A.Role }, "banned", "suspended")IsZero(func() string { return *r.A.Name }) // *string: "" → true, "abc" → false
NotZero(func() int { return r.A.Count }) // int (value type): 0 → false, 5 → truen, ok := Len(func() string { return *r.A.Name })
n := MustLen(func() []byte { return *r.A.Data })IfOk(func() string { return *r.A.Token }, func(token string) {
fmt.Println("got token:", token)
})// Like Safe but returns error instead of bool
val, err := SafeErr(func() string { return *r.A.Name })
// err: "nil pointer dereference: runtime error: ..."Build out deeply nested structs without nil checks. E() (short for Ensure) auto-allocates nil pointer fields and returns the value, enabling one-liner chain assignment.
// E(): one-liner chain — auto-create all intermediate pointers and assign
var req Request
E(&E(&E(&req.Auth).Key).Session).Token = ptr("my_token")
// Works with value-type fields too — just chain to the parent
E(&E(&req.Auth).Key).Name = "admin" // string field
E(&E(&req.Auth).Key).Score = 100 // int field
// Grab a reference to avoid repeating the chain
key := E(&E(&req.Auth).Key)
key.Name = "admin"
key.Score = 100
key.Token = ptr("abc")
// Set: chain + assign with panic recovery, returns bool
ok := Set(func() **string {
return &E(&E(&E(&req.Auth).Key).Session).Token
}, ptr("my_token"))
// SetErr: like Set but returns error on failure
_, err := SetErr(func() **string {
return &E(&E(&E(&req.Auth).Key).Session).Token
}, ptr("my_token"))
// err: "set failed: runtime error: invalid memory address..."Tested on Apple M4 (10 cores), Go 1.22+.
| Function | ns/op | allocs |
|---|---|---|
Safe |
7.0 | 0 |
Must |
8.0 | 0 |
OrVal |
8.3 | 0 |
Check |
8.9 | 0 |
NotNil |
8.2 | 0 |
Eq |
9.1 | 0 |
Gt |
7.8 | 0 |
Between |
8.2 | 0 |
In (3 values) |
13 | 0 |
Match |
9.4 | 0 |
HasPrefix |
12 | 0 |
Contains |
13 | 0 |
Map |
9.5 | 0 |
First |
9.7 | 0 |
SafeErr |
7.0 | 0 |
And (2 checks) |
18 | 0 |
SafeDig (4 levels) |
141 | 5 |
MustSafeDig (3 levels) |
110 | 4 |
Dig (4 levels) |
45 | 1 |
| Function | ns/op | allocs |
|---|---|---|
Safe |
2.2 | 0 |
Must |
2.2 | 0 |
Check |
2.5 | 0 |
Eq |
2.3 | 0 |
HasPrefix |
2.8 | 0 |
In |
3.2 | 0 |
Dig (4 levels) |
43 | 1 |
SafeDig (4 levels) |
102 | 5 |
| Approach | Single | Parallel | Allocs |
|---|---|---|---|
Manual if != nil |
93 ns | 16 ns | 0 |
Safe (recover) |
88 ns | 18 ns | 0 |
Dig (unsafe.Pointer) |
589 ns | 718 ns | 1 |
Safematches hand-written nil checks at any depth, zero allocation.- All recover-based functions scale linearly with goroutines — no contention.
Dig/SafeDighave allocation overhead from thenamesslice, but still sub-microsecond.
All functions are goroutine-safe — no shared state, all operations are stack-local. Verified with 1000-goroutine stress tests + -race detector on every public function:
Safe, Must, OrVal, Check, NotNil, And, Or, Eq, Gt, Between, In, Match, HasPrefix, Contains, Map, First, SafeErr, Dig, SafeDig, MustSafeDig
The only requirement is that the struct being accessed is not concurrently modified by another goroutine (same as hand-written if != nil).
- Go 1.22+