typeutil

typeutil detects nil through interfaces, obtains zero values generically, dereferences pointers safely, and converts booleans to integers without a branch.

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

Functionns/opallocs/op
Zero0.440
Value0.440
BoolToInt0.430
BoolToNumFloat640.450
IsNil2.070
IsZeroInt3.630
IsZeroStruct5.690

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.