Skip to content

lesiw/zeros

Repository files navigation

zeros

Go Reference CI Release Go Version Discord License

Zero-valueable wrappers for Go's built-in types and sync patterns.

Overview

Package zeros provides types that are usable at their zero value:

  • Chan[T] and Map[K,V] auto-initialize on first use, eliminating the need for explicit make() calls
  • Slice[T] is a slice type whose Append mutates in place and returns the updated slice, so package-level var initializers across files can build up a single value
  • OnceValue[T] and OnceValues[T1, T2] provide zero-valueable alternatives to sync.OnceValue and sync.OnceValues

All types follow the same principle as bytes.Buffer and sync.Mutex: they work immediately without initialization.

Installation

go get lesiw.io/zeros

Features

  • Zero-value usability: Use var ch zeros.Chan[int] or var m zeros.Map[string,int] without initialization
  • Thread-safe initialization: Initialization is thread-safe via sync.Once
  • Minimal API: Simple wrappers that mirror built-in types
  • Modern Go: Uses Go 1.23+ features including range-over-func iterators

Usage

Chan

▶️ Run this example on the Go Playground

package main

import (
    "fmt"

    "lesiw.io/zeros"
)

func main() {
    var ch zeros.Chan[int]

    go func() {
        ch.Send(42) // auto-initializes the channel
    }()

    value := ch.Recv()
    fmt.Println(value)
}

Available methods:

  • Chan() chan T - Returns the underlying channel
  • Send(v T) - Sends a value on the channel (blocks)
  • Recv() T - Receives a value from the channel (blocks)
  • CheckRecv() (T, bool) - Receives with channel status indicator
  • TrySend(v T) bool - Attempts to send without blocking
  • TryRecv() (T, bool) - Attempts to receive without blocking
  • Close() - Closes the underlying channel

Map

▶️ Run this example on the Go Playground

package main

import (
    "fmt"

    "lesiw.io/zeros"
)

func main() {
    var m zeros.Map[string, int]

    m.Set("answer", 42) // auto-initializes the map

    fmt.Println(m.Get("answer"))

    if _, ok := m.CheckGet("missing"); !ok {
        fmt.Println("key not found")
    }

    for k, v := range m.All() {
        fmt.Println(k, v)
    }
}

Available methods:

  • Map() map[K]V - Returns the underlying map
  • Set(key K, value V) - Sets a key-value pair
  • Get(key K) V - Returns value or zero value if missing
  • CheckGet(key K) (V, bool) - Returns value and presence indicator
  • Delete(key K) - Removes a key
  • Len() int - Returns the number of elements
  • Keys() iter.Seq[K] - Returns an iterator over keys
  • Values() iter.Seq[V] - Returns an iterator over values
  • All() iter.Seq2[K, V] - Returns an iterator over key-value pairs
  • Clear() - Removes all elements

Slice

A slice type whose Append mutates in place and returns the updated slice — usable from package-level var initializers across multiple files:

▶️ Run this example on the Go Playground

-- go.mod --
module example

go 1.23

require lesiw.io/zeros v0.4.0
-- registry.go --
package main

import "lesiw.io/zeros"

var inits zeros.Slice[func()]
-- a.go --
package main

import "fmt"

var _ = inits.Append(func() { fmt.Println("setup A") })
-- b.go --
package main

import "fmt"

var _ = inits.Append(func() { fmt.Println("setup B") })
-- main.go --
package main

func main() {
	for _, fn := range inits {
		fn()
	}
}

Slice[T] is defined as []T, so index, len, range, and the standard slices package all work on it directly.

Available methods:

  • Append(v ...T) Slice[T] - Appends values in place and returns the updated slice

OnceValue

▶️ Run this example on the Go Playground

Zero-valueable lazy initialization for a single value:

package main

import (
    "fmt"

    "lesiw.io/zeros"
)

type LazyReader struct {
    init zeros.OnceValue[string]
}

func main() {
    var r LazyReader

    // First call executes the function
    data := r.init.Do(func() string {
        fmt.Println("Loading data")
        return "Hello, World!"
    })
    fmt.Println(data)

    // Subsequent calls return cached value
    data = r.init.Do(func() string {
        fmt.Println("This won't print")
        return "Goodbye"
    })
    fmt.Println(data)
}

OnceValues

▶️ Run this example on the Go Playground

Zero-valueable lazy initialization for two values:

package main

import (
    "fmt"
    "io"
    "log"
    "os"

    "lesiw.io/zeros"
)

type LazyFile struct {
	once zeros.OnceValues[*os.File, error]
}

func (f *LazyFile) init() (*os.File, error) {
    fmt.Println("Creating temp file")
    return os.CreateTemp("", "example")
}

func (f *LazyFile) Write(p []byte) (int, error) {
    file, err := f.once.Do(f.init)
    if err != nil {
        return 0, fmt.Errorf("failed to open file: %w", err)
    }
    return file.Write(p)
}

func (f *LazyFile) Stat() (os.FileInfo, error) {
    file, err := f.once.Do(f.init)
    if err != nil {
        return nil, fmt.Errorf("failed to stat file: %w", err)
    }
    return file.Stat()
}

func main() {
    var f LazyFile

    info, err := f.Stat()
    if err != nil {
        log.Fatalf("Stat failed: %v", err)
    }
    fmt.Printf("Initial size: %d bytes\n", info.Size())

    if _, err := io.WriteString(&f, "Hello, world!"); err != nil {
        log.Fatalf("Write failed: %v", err)
    }

    info, err = f.Stat()
    if err != nil {
        log.Fatalf("Stat failed: %v", err)
    }
    fmt.Printf("Final size: %d bytes\n", info.Size())
}

Thread Safety

OnceValue and OnceValues are fully thread-safe. The wrapped function is guaranteed to execute exactly once, even with concurrent calls.

Chan and Map have thread-safe initialization, but the types themselves are not safe for concurrent access without external synchronization (like Go's built-in chan and map types). If you need concurrent map access, use external locking or sync.Map.

Slice is not safe for concurrent modification, matching Go's built-in slice type.

Why?

In Go, maps and channels have nil zero values and must be initialized with make() before use. This can be inconvenient when embedding these types in structs. The zeros package makes these types work like other zero-value-usable types in Go's standard library.

Requirements

Go 1.23 or later (for iter.Seq2 support)

License

BSD-3-Clause