Table of contents
Part of nurago, a collection of independent Go packages for backend services.
import "github.com/tecnickcom/nurago/pkg/typeutil"
Nil detection through an interface, zero values for a type parameter, safe pointer dereference, and branch-free bool-to-integer conversion, plus a set of numeric type constraints.
Four of the five helpers are conveniences. IsNil exists because of a Go language trap.
The typed nil
The trap:
var p *MyStruct // nil pointer
var i any = p // non-nil interface holding a nil pointer
i == nil // false
typeutil.IsNil(i) // true
An interface value carries two words, a type and a pointer. Assigning a nil *MyStruct to an any fills in the type word, so the interface is no longer nil even though what it points at is. The comparison i == nil asks about the interface, not about the pointer inside it.
Where this goes wrong in practice is the error return:
func do() error {
var err *MyError // nil
// ... nothing sets err ...
return err // non-nil error interface
}
if err := do(); err != nil {
// taken, every time
}
IsNil goes through reflect to ask the question you meant, handling every nilable kind: channel, func, interface, map, pointer, slice, unsafe pointer. The reflection costs 2.07 ns and allocates nothing, which is cheap enough that there is no reason to hand-roll a type switch instead.
The other four
Zero returns the zero value of a type parameter, taking an argument only to infer T. It reads better than a var zero T line in a generic error return:
func check[T any](v T) (T, error) {
if !valid(v) {
return typeutil.Zero(v), errInvalid
}
return v, nil
}
The argument is evaluated and discarded, so do not pass anything expensive or panicking.
Value dereferences a pointer and yields the zero value when it is nil, which removes the three-line guard around every optional config field. It compares the pointer directly rather than going through reflection.
IsZero reports whether a value equals its type’s zero value without requiring a comparable constraint. A direct v == zero comparison would require one, so it cannot be written in a function generic over any.
BoolToInt turns a bool into 0 or 1 using the form the Go compiler recognises and lowers to a single MOVBLZX, with no branch and therefore no branch misprediction:
score += typeutil.BoolToInt(isBonus) * bonusPoints
Measured
| Function | ns/op | allocs/op |
|---|---|---|
Zero | 0.44 | 0 |
Value | 0.44 | 0 |
BoolToInt | 0.43 | 0 |
BoolToNumFloat64 | 0.45 | 0 |
IsNil | 2.07 | 0 |
IsZeroInt | 3.63 | 0 |
IsZeroStruct | 5.69 | 0 |
Measured with go test -bench on Go 1.27.1, linux/amd64, Intel Core i7-1260P. Run go test -bench=. ./pkg/typeutil/ to reproduce them on your own hardware; the absolute values will differ, the ratios much less so.
Sub-nanosecond figures mean the call inlined away to nothing, which is the expected result for the first four and confirms the branch-free claim for BoolToInt. The two IsZero rows are the ones to read carefully: the cost scales with the type, because reflection walks a struct field by field. On a hot path with a known comparable type, write v == zero and skip the package.
One caveat from the source: IsZero allocates nothing when the call inlines, but if v or its address escapes at the call site, the value is heap-allocated.
Constraints
types.go declares Int, UInt, Float, Number and Ordered for use in generic signatures. Ordered matches the standard library’s cmp.Ordered and is kept here so that a codebase using Number does not have to import two constraint vocabularies to write one function signature.
Example
var nilChan chan int
fmt.Println(typeutil.IsNil(nilChan))
// Output:
// true
Full source is in example_typeutil_test.go. More runnable examples are on pkg.go.dev.
Dependencies
This package reaches no external module: it uses only the Go standard library.