Zero-valueable wrappers for Go's built-in types and sync patterns.
Package zeros provides types that are usable at their zero value:
Chan[T]andMap[K,V]auto-initialize on first use, eliminating the need for explicitmake()callsSlice[T]is a slice type whoseAppendmutates in place and returns the updated slice, so package-levelvarinitializers across files can build up a single valueOnceValue[T]andOnceValues[T1, T2]provide zero-valueable alternatives tosync.OnceValueandsync.OnceValues
All types follow the same principle as bytes.Buffer and sync.Mutex: they work immediately without initialization.
go get lesiw.io/zeros- Zero-value usability: Use
var ch zeros.Chan[int]orvar 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
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 channelSend(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 indicatorTrySend(v T) bool- Attempts to send without blockingTryRecv() (T, bool)- Attempts to receive without blockingClose()- Closes the underlying channel
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 mapSet(key K, value V)- Sets a key-value pairGet(key K) V- Returns value or zero value if missingCheckGet(key K) (V, bool)- Returns value and presence indicatorDelete(key K)- Removes a keyLen() int- Returns the number of elementsKeys() iter.Seq[K]- Returns an iterator over keysValues() iter.Seq[V]- Returns an iterator over valuesAll() iter.Seq2[K, V]- Returns an iterator over key-value pairsClear()- Removes all elements
A slice type whose Append mutates in place and returns the updated slice — usable from package-level var initializers across multiple files:
-- 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
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)
}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())
}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.
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.
Go 1.23 or later (for iter.Seq2 support)
BSD-3-Clause