k1 (read: "k1t") is a small toolkit of the helpers you keep rewriting between projects: type casting that survives custom types and deep pointers, an Option type, safe pointer dereferencing, and set lookups.
The core idea: try a direct type switch first, fall back to reflection. So cast functions accept anything shaped right, not just exact types:
type UserID string
id := UserID("u-42")
p := &id
cast.AsString(id) // "u-42"
cast.AsString(&p) // "u-42" - pointers are dereferenced deeplyNote
As* functions panic on impossible conversions instead of returning errors. That is by design: k1 is testing-oriented, and in tests a panic is a failure you want loud.
go get github.com/amberpixels/k1package main
import (
"fmt"
"github.com/amberpixels/k1/cast"
"github.com/amberpixels/k1/maybe"
"github.com/amberpixels/k1/ptr"
"github.com/amberpixels/k1/set"
)
type UserID string
func main() {
// cast: conversions that survive custom types and pointers
id := UserID("u-42")
fmt.Println(cast.AsString(&id)) // u-42
// maybe: Option[T] instead of *T
port := maybe.Some(8080)
if port.Some() {
fmt.Println(port.Unwrap()) // 8080
}
// ptr: dereference with a zero-value fallback
var name *string
fmt.Printf("%q\n", ptr.Deref(name)) // ""
// set: map[T]struct{} without the ceremony
admins := set.NewLookup("alice", "bob")
fmt.Println(admins.Has("mallory")) // false
}The cast package converts (As*) and checks (Is*):
cast.AsString([]byte("data")) // "data"
cast.AsBytes("data") // []byte("data")
cast.AsInt(42.0) // 42 - integral floats convert; 42.5 panics
cast.AsFloat(42) // 42.0
cast.AsTime(&customTime) // time.Time, also from custom time typesFull set: AsString, AsBytes, AsBool, AsInt, AsFloat, AsTime, AsKind, AsSliceOfAny, AsStrings - plus IsString, IsStringish, IsNil, IsInt, IsStrings, IsTime for checks.
IsString is strict by default (true only for an actual string); loosen it per call or globally:
cast.IsString(UserID("u-42")) // false - strict by default
cast.IsString(UserID("u-42"), cast.AllowCustomTypes()) // true
cast.IsString([]byte("hi"), cast.AllowAll()) // true - most permissive
cast.ConfigureIsStringConfig(cast.AllowAll()) // change the default globallyThe maybe package is an Option[T] for comparable types, with marshaling that behaves well in configs and APIs:
port := maybe.Some(8080)
port.Some() // true
port.Unwrap() // 8080; panics on None
none := maybe.None[int]()
json.Marshal(port) // 8080
json.Marshal(none) // nullNone marshals as null in JSON and as the "None" sentinel in TOML; text unmarshalling treats empty, "null", and "None" as None. Shorthands: maybe.True(), maybe.False(), maybe.NoneBool(), maybe.NoneInt().
ptr-ptr.Deref(p)dereferences with a zero-value fallback for nil;ptr.Clone(p)copies a pointee.set-set.Lookup[T]ismap[T]struct{}withHas/Add/Delete/Clear; build one withset.NewLookup("a", "b").quick-quick.Append(a, b...)appends only elements not already present; trades extra memory (and GC pressure) for speed on large slices.errs-errs.UnwrapDeep(err)walks a wrapped error chain to the root cause.reflectish-IndirectDeepfor deep pointer dereferencing,LengthOffor the length of anything length-y, panic-safeInterface.k1(root) -k1.JoinStringers(vals, ", ")joins any slice offmt.Stringers.
k1 is a solo, opinionated project - but if you stumbled upon it and have ideas, questions, or bug reports, an issue is always welcome :)