Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

269 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

g - Functional programming framework for Go.

Go Reference Coverage Status Go Mentioned in Awesome Go Ask DeepWiki

go get github.com/enetx/g

Requires Go 1.27+ (generic methods: Map, Then, Fold, Zip and friends change element types right in the chain — no more package-level Transform* workarounds).

Designed to be cheap to compile: the type graph is deliberately de-welded, so a package that only uses g.String compiles ~1.5k functions instead of ~33k, and heavyweight areas (filesystem, regex) live in subpackages (g/fs, g/rx). The only non-stdlib dependency is golang.org/x/text.


Quick Start

package main

import (
    "fmt"
    "github.com/enetx/g"
)

func main() {
    // Slice with functional operations
    g.SliceOf(1, 2, 3, 4, 5).
        Iter().
        Filter(func(x int) bool { return x%2 == 0 }).
        Map(func(x int) int { return x * x }).
        Collect().
        Slice().
        Println() // Slice[4, 16]

    // Safe map access with Option
    m := g.NewMap[string, int]()
    m.Insert("key", 42)

    value := m.Get("key").UnwrapOr(0)
    fmt.Println(value) // 42

    // Result for error handling
    result := g.String("123").TryInt()
    if result.IsOk() {
        fmt.Println(result.Unwrap()) // 123
    }
}

Navigation

Core Collections Sync Types
Option Slice Mutex String
Result Map RwLock Int/Float
Iterators Set Pool Bytes
Deque Regex (g/rx)
Heap File/Dir (g/fs)

Option

Safe nullable values: Some(value) or None.

opt := g.Some(42)           // Some(42)
none := g.None[int]()       // None

opt.IsSome()                // true
opt.Unwrap()                // 42 (panics if None)
opt.UnwrapOr(0)             // 42 (returns default if None)
opt.UnwrapOrDefault()       // 42 (returns zero value if None)

// From map lookup
v, ok := myMap[key]
opt := g.OptionOf(v, ok)

// Chaining
g.Some(5).Then(func(x int) g.Option[int] {
    return g.Some(x * 2)
}) // Some(10)
All Option Methods
Method Description
IsSome() / IsNone() Presence checks
IsSomeAnd(pred) / IsNoneOr(pred) Presence combined with a predicate
Some() Returns value (zero value if None — check first)
Unwrap() / Expect(msg) Returns value, panics if None
UnwrapOr(default) / UnwrapOrDefault() Returns value or fallback
Map(fn) Transforms the value, may change its type
MapOr(def, fn) / MapOrElse(defFn, fn) Map or fall back to a default
Then(fn) Chains a fallible transformation (and_then)
Filter(pred) Returns None if predicate fails
Or(other) / OrElse(fn) Fallback Option
Inspect(fn) Side effect on Some, returns self
g.OkOr(opt, err) / g.OkOrElse(opt, fn) Converts to Result (free functions)
Take() / Replace(v) / Insert(v) In-place mutation
Option() Returns (value, bool)

JSON: Some(v) ⇄ value, Nonenull (encoding/json/v2 native, v1 compatible).


Result

Success (Ok) or failure (Err) with error.

ok := g.Ok(42)                              // Ok(42)
err := g.Err[int](errors.New("failed"))     // Err

ok.IsOk()                   // true
ok.Unwrap()                 // 42
err.UnwrapOr(0)             // 0

// From standard (value, error) pattern
result := g.ResultOf(strconv.Atoi("42"))    // Ok(42)
result := g.String("42").TryInt()           // Ok(42)

// Chaining
g.Ok(10).Then(func(x int) g.Result[int] {
    return g.Ok(x * 2)
}) // Ok(20)

// Convert to Option (discards error)
opt := result.Option()      // Some(42)
All Result Methods
Method Description
IsOk() / IsErr() State checks
IsOkAnd(pred) / IsErrAnd(pred) State combined with a predicate
Ok() / Err() Returns value / error (zero if the other state)
Unwrap() / Expect(msg) Returns value, panics if Err
UnwrapErr() Returns error, panics if Ok
UnwrapOr(default) / UnwrapOrDefault() Returns value or fallback
Map(fn) Transforms the value, may change its type
MapOr(def, fn) / MapOrElse(defFn, fn) Map or fall back
Then(fn) / ThenOf(fn) Chains Result / (T, error) functions
Or(other) / OrElse(fn) Fallback Result
MapErr(fn) / Wrap(err) Transforms / wraps the error
Inspect(fn) / InspectErr(fn) Side effects, return self
ErrIs(target) / ErrAs(target) errors.Is / errors.As integration
Option() / Result() Converts to Option / (value, error)

JSON: Ok(v){"ok": v}, Err(e){"err": "msg"} (externally tagged; duplicate keys rejected).


Slice

Extended slice with 90+ methods.

s := g.SliceOf(1, 2, 3, 4, 5)
s := g.NewSlice[int](0, 10)     // empty, with capacity 10 (one arg sets len AND cap)

s.Len()                         // Int(5)
s.Get(0)                        // Some(1)
s.Get(100)                      // None (safe!)
s.Last()                        // Some(5)
s.Contains(3)                   // true

s.Push(6, 7)                    // append in place
s.Pop()                         // Some(7)
s.Clone()                       // safe copy

// Sorting
s.SortBy(cmp.Cmp)               // ascending
s.Reverse()                     // in place
rand.Shuffle(s)                 // random order (g/rand)
All Slice Methods
Category Methods
Access Get, Last, First
Modify Set, Push, Pop, Insert, Remove
Search Contains, ContainsBy, Index, IndexBy
Transform Clone, Reverse, SortBy, SubSlice
Convert Iter, Std
Info Len, Cap, IsEmpty

Map

Extended map with Entry API.

m := g.NewMap[string, int]()
// or from pairs: g.MapOf(g.Pair[string, int]{"a", 1}, g.Pair[string, int]{"b", 2})

m.Insert("a", 1)                // insert value
m.Get("a")                      // Some(1)
m.Get("x")                      // None
m.Contains("a")                 // true
m.Remove("a")                   // remove

m.Keys()                        // Slice of keys
m.Values()                      // Slice of values

Entry API

Efficient update without multiple lookups:

// Insert if absent
m.Entry("counter").OrInsert(0)

// Update if present, insert if absent
m.Entry("counter").AndModify(func(v *int) { *v++ }).OrInsert(1)

// Lazy initialization
m.Entry("key").OrInsertWith(func() int {
    return expensiveComputation()
})

Word frequency example:

words := g.SliceOf("apple", "banana", "apple", "cherry", "banana", "apple")
freq := g.NewMap[string, int]()

for _, word := range words {
    freq.Entry(word).AndModify(func(v *int) { *v++ }).OrInsert(1)
}
// {"apple": 3, "banana": 2, "cherry": 1}
Entry Pattern Matching
switch e := m.Entry("key").(type) {
case g.OccupiedEntry[string, int]:
    fmt.Println("Exists:", e.Get())
    e.Insert(newValue)      // replace
    e.Remove()              // delete
case g.VacantEntry[string, int]:
    e.Insert(defaultValue)  // insert
}

Set

Collection of unique elements.

s := g.SetOf(1, 2, 3)
s.Insert(4)                     // add
s.Remove(1)                     // remove
s.Contains(2)                   // true

// Set algebra (eager, returns Set)
s.Union(other)                  // A ∪ B
s.Intersection(other)           // A ∩ B
s.Difference(other)             // A \ B
s.SymmetricDifference(other)    // A △ B
s.Disjoint(other)               // no common elements?

Set Operations

a := g.SetOf(1, 2, 3, 4)
b := g.SetOf(3, 4, 5, 6)

a.Union(b)                      // Set{1, 2, 3, 4, 5, 6}
a.Intersection(b)               // Set{3, 4}
a.Difference(b)                 // Set{1, 2}
a.SymmetricDifference(b)        // Set{1, 2, 5, 6}

a.Subset(b)                     // false
a.Superset(b)                   // false

Heap

Priority queue (binary heap).

import "github.com/enetx/g/cmp"

// Min-heap (smallest first)
h := g.NewHeap(cmp.Cmp[int])
h.Push(5, 3, 8, 1, 9)
// or in one go: g.HeapOf(cmp.Cmp[int], 5, 3, 8, 1, 9)

h.Peek()                        // Some(1)
h.Pop()                         // Some(1)
h.Pop()                         // Some(3)

// Max-heap (largest first)
maxH := g.NewHeap(func(a, b int) cmp.Ordering {
    return cmp.Cmp(b, a)
})

// From a slice — spread into the variadic constructor
s := g.SliceOf(5, 3, 8, 1)
heap := g.HeapOf(cmp.Cmp[int], s...)

Deque

Double-ended queue (ring buffer). O(1) at both ends.

dq := g.NewDeque[int]()
dq := g.DequeOf(1, 2, 3)

dq.PushFront(0)                 // add to front
dq.PushBack(4)                  // add to back

dq.Front()                      // Some(0)
dq.Back()                       // Some(4)

dq.PopFront()                   // Some(0)
dq.PopBack()                    // Some(4)

dq.Get(1)                       // element at index
dq.Len()                        // length
dq.IsEmpty()                    // true/false

Iterators

Functional operations on sequences.

g.SliceOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).
    Iter().
    Filter(func(x int) bool { return x%2 == 0 }).
    Map(func(x int) int { return x * x }).
    Take(3).
    Collect().
    Slice()
// Slice[4, 16, 36]

Thanks to generic methods (Go 1.27+), transformations can change the element type right in the chain:

g.SliceOf(1, 2, 3).
    Iter().
    Map(func(x int) g.String { return g.Int(x).String() }). // Seq[int] -> Seq[g.String]
    Collect().
    Slice().
    Join(", ").
    Println() // 1, 2, 3

// Fold into any accumulator type
words := g.SliceOf[g.String]("a", "bb", "ccc")
total := words.Iter().Fold(g.Int(0), func(acc g.Int, w g.String) g.Int { return acc + w.Len() })
// 6

// Typed Zip — pairs keep their real types
nums := g.SliceOf(1, 2, 3)
names := g.SliceOf("one", "two", "three")
nums.Iter().Zip(names.Iter()).ForEach(func(n int, s string) {
    fmt.Println(n, s)
})

Collecting

Collect() returns a collector; pick the container to materialize into. Set, Map, MapOrd and MapSafe take explicit type arguments — a method of Seq[V any] cannot constrain V to comparable, so the constraint lives on the collector method and the type is spelled at the call site:

s.Iter().Filter(fn).Collect().Slice()          // Slice[int]
s.Iter().Filter(fn).Collect().Set[int]()       // Set[int]
s.Iter().Collect().Deque()                     // *Deque[int]
s.Iter().Collect().Heap(cmp.Cmp)               // *Heap[int]

m.Iter().FilterByKey(fn).Collect().Pairs()               // []Pair[K, V]
m.Iter().FilterByKey(fn).Collect().Map[string, int]()    // Map[string, int]
m.Iter().Collect().MapOrd[string, int]()                 // MapOrd[string, int]
m.Iter().Collect().MapSafe[string, int]()                // *MapSafe[string, int]

Note: Collect() alone is lazy — nothing runs until a materializer is called.

Common Patterns

// Chain iterators
g.SliceOf(1, 2).Iter().Chain(g.SliceOf(3, 4).Iter()).Collect().Slice()
// Slice[1, 2, 3, 4]

// Sum with Fold
g.SliceOf(1, 2, 3, 4, 5).Iter().Fold(0, func(acc, x int) int { return acc + x })
// 15

// Enumerate
g.SliceOf("a", "b", "c").Iter().Enumerate().ForEach(func(i g.Int, v string) {
    fmt.Printf("%d: %s\n", i, v)
})

// Using f package predicates
import "github.com/enetx/g/f"

g.SliceOf(1, 2, 3, 4, 5).Iter().Filter(f.Ne(3)).Collect().Slice() // Slice[1, 2, 4, 5]
g.SliceOf("", "a", "").Iter().Exclude(f.IsZero).Collect().Slice()  // Slice["a"]
All Iterator Methods
Transform Slice Combine Aggregate Search Other
Map Take Chain Collect Find ForEach
Filter Skip Zip Fold Any Inspect
Exclude StepBy Enumerate Reduce All Range
FilterMap First Intersperse Count MaxBy Scan
FlatMap Last Cycle CounterBy MinBy Combinations
Flatten Nth Partition Permutations
Dedup Chunks ChunkBy Context
Unique Windows Chan
SortBy TakeWhile
SkipWhile

Mutex

Typed mutex — data bound to lock.

counter := g.NewMutex(0)

// Lock and modify
guard := counter.Lock()
guard.Set(guard.Get() + 1)
guard.Unlock()

// With defer (recommended)
func increment(c *g.Mutex[int]) {
    guard := c.Lock()
    defer guard.Unlock()
    guard.Set(guard.Get() + 1)
}

// Direct pointer access
guard := counter.Lock()
defer guard.Unlock()
*guard.Deref() += 1

// Non-blocking
if opt := counter.TryLock(); opt.IsSome() {
    guard := opt.Unwrap()
    defer guard.Unlock()
    // got the lock
}

Why typed mutex?

// Traditional — easy to forget locking
type Old struct {
    mu   sync.Mutex
    data map[string]int  // what protects this?
}

// Typed — impossible to access without lock
type New struct {
    data *g.Mutex[g.Map[string, int]]
}

func (s *New) Get(key string) g.Option[int] {
    guard := s.data.Lock()
    defer guard.Unlock()
    return guard.Deref().Get(key)
}

RwLock

Multiple readers OR single writer.

config := g.NewRwLock(Config{Port: 8080})

// Read (concurrent)
func getPort(c *g.RwLock[Config]) int {
    guard := c.Read()
    defer guard.Unlock()
    return guard.Get().Port
}

// Write (exclusive)
func setPort(c *g.RwLock[Config], port int) {
    guard := c.Write()
    defer guard.Unlock()
    guard.Deref().Port = port
}

// Non-blocking
if opt := config.TryRead(); opt.IsSome() { ... }
if opt := config.TryWrite(); opt.IsSome() { ... }
Use Case Choose
Read-heavy (config, cache) RwLock
Write-heavy or balanced Mutex
Simple counters Mutex

Pool

Goroutine pool for parallel tasks.

import "github.com/enetx/g/pool"

p := pool.New[int]().Limit(4)

for i := range 10 {
    p.Go(func() g.Result[int] {
        return g.Ok(i * 2)
    })
}

for result := range p.Wait() {
    if result.IsOk() {
        fmt.Println(result.Unwrap())
    }
}
With Context and Cancel on Error
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

p := pool.New[int]().
    Context(ctx).
    CancelOnError().
    Limit(4)

for i := range 100 {
    p.Go(func() g.Result[int] {
        if i == 50 {
            return g.Err[int](errors.New("error"))
        }
        return g.Ok(i)
    })
}

for result := range p.Wait() {
    // stops after first error
}

String

Extended string with 80+ methods.

s := g.String("Hello, World!")

s.Len()                         // 13
s.Upper()                       // "HELLO, WORLD!"
s.Lower()                       // "hello, world!"
s.Contains("World")             // true
s.Trim()                        // remove whitespace

// Splitters return a plain []String — index, len and range work directly
parts := s.Split(", ")          // []String{"Hello", "World!"}
parts[0]                        // "Hello"

for _, part := range s.Split(", ") {
    part.Println()              // Hello / World!
}

// For chains, lift the slice into an iterator
g.SliceOf(s.Split(", ")...).Iter().Map(g.String.Upper).Collect().Slice()

// Conversions
s.Std()                         // Go string
s.Bytes()                       // Bytes type
g.String("42").TryInt()         // Result[Int]

// Encoding
s.Hash().MD5()                  // hash
s.Encode().Base64()             // encode
s.Compress().Gzip()             // compress

Randomness — g/rand

All randomness lives in one package (math/rand/v2 under the hood; the Secure* helpers draw from crypto/rand):

import "github.com/enetx/g/rand"

rand.N(10)                    // [0, 10)
rand.Range(5, 10)             // half-open [5, 10)
rand.RangeInclusive(1, 6)     // closed [1, 6]
rand.Float()                  // [0, 1)
rand.Chance(0.25)             // true with probability 0.25
rand.String(10)               // 10 alphanumeric characters
rand.SecureString(32)         // crypto/rand-backed token (keys, codes)
rand.SecureBytes(16)          // crypto/rand-backed raw bytes
rand.String(6, g.Digits)      // 6-digit code

users := g.SliceOf("ann", "bob", "eve")
rand.Choice(users)            // Option[string]
rand.Sample(users, 2)         // 2 distinct elements
rand.Shuffle(users)           // in place; accepts Slice, MapOrd, plain slices

The container types deliberately carry no random methods — one way to do it.

Regular expressions — g/rx

Compile a pattern once, then run it anywhere — patterns live in their own package:

import "github.com/enetx/g/rx"

re := rx.New(`[0-9]{4}-[0-9]{2}-[0-9]{2}`).Unwrap()

re.IsMatch("2026-08-09")                   // true
re.Find("on 2026-08-09 at noon")           // Option[String]
re.FindIter(hay).Take(2).Collect().Slice() // first two matches
re.Replace(s, "$1")                        // FIRST match only
re.ReplaceBy(s, fn)                        // FIRST match through a function
re.ReplaceAll(s, "$1")                     // every match

digits := rx.New(`[0-9]`).Unwrap()
digits.Split("a1b2c3").Collect().Slice()   // Slice[a, b, c]

// Bytes haystacks: IsMatchBytes, FindBytes, FindIterBytes, ...

Int / Float

Numeric types with utility methods.

i := g.Int(42)

i.Add(8)                        // 50
i.Mul(2)                        // 84
i.Abs()                         // absolute value
i.Min(10, 20)                   // minimum
i.Max(10, 20)                   // maximum

i.IsZero()                      // false
i.IsPositive()                  // true
i.IsNegative()                  // false

i.Binary()                      // binary string
i.Hex()                         // hex string

// Checked arithmetic — overflow becomes None instead of a silent wraparound
g.Int(math.MaxInt).CheckedAdd(1)   // None
g.Int(10).CheckedDiv(0)            // None — no division panic
g.Int(2).CheckedPow(62)            // Some(4611686018427387904)

// Saturating / Overflowing variants
g.Int(math.MaxInt).SaturatingAdd(1)   // MaxInt (clamped)
v, overflowed := g.Int(math.MaxInt).OverflowingAdd(1) // MinInt, true

// Float
f := g.Float(3.14159)
f.Round()                       // Int(3)
f.RoundDecimal(2)               // Float(3.14)
f.Sqrt()                        // square root

// Float math & classification
g.Float(1).Div(0).IsInf()       // true
g.Float(-3.75).Fract()          // -0.75
g.Float(2.5).Clamp(0, 2)        // 2
g.Float(math.Pi).ToDegrees()    // 180
g.Float(0.1).MulAdd(10, -1)     // true FMA — single rounding
// plus Exp/Ln/Log2/Log10, Sin..Atanh, Signum, Copysign, Hypot, Cbrt...

// Compare
i.Cmp(g.Int(50))                // cmp.Less

Bytes

Extended byte slice.

b := g.Bytes([]byte("Hello"))

b.Len()
b.Upper()
b.Lower()
b.Contains([]byte("ell"))

b.String()                      // to String
b.Std()                         // to []byte

File / Directory

Filesystem types live in the g/fs subpackage, so only programs that touch files pay for them:

import "github.com/enetx/g/fs"

File

// Read/Write
content := fs.NewFile("config.json").Read()      // Result[String]
fs.NewFile("output.txt").Write("Hello")
fs.NewFile("log.txt").Append("line\n")

// Open a new file with the specified name "text.txt" and process it line by line.
fs.NewFile("text.txt").
	Lines().                            // Reads the file line by line.
	Skip(2).                            // Skips the first 2 lines in the iterator.
	Exclude(f.IsZero).                  // Excludes lines that are empty or contain only whitespaces.
	Dedup().                            // Removes consecutive duplicate lines.
	Map(g.String.Upper).                // Converts each line to uppercase.
	Range(func(s g.Result[g.String]) bool { // Iterates over the lines while a condition is true.
		if s.IsErr() { // Handles any errors encountered while reading lines.
			fmt.Println("Error:", s.Err())
			return false // Stops the iteration if an error occurs.
		}

		if s.Ok().Contains("COULD") { // Checks if the line contains the substring "COULD".
			return false // Stops the iteration if the condition is met.
		}

		fmt.Println(s.Ok()) // Prints the line.
		return true         // Continues the iteration.
	})

// Properties
file := fs.NewFile("test.txt")
file.Exists()                   // check existence
file.Stat()                     // file info
file.Copy("backup.txt")         // copy
file.Rename("new.txt")          // rename
file.Remove()                   // delete

File Guard (Exclusive Lock)

locked := fs.NewFile("data.txt").Guard()  // holds exclusive lock
locked.Write("exclusive data")
locked.Close()                            // release lock

Directory

fs.NewDir("mydir").Create()
fs.NewDir("path/to/deep").CreateAll()

// Read contents
for file := range fs.NewDir(".").Read() {
    if file.IsOk() {
        file.Ok().Name().Println()
    }
}

// Recursively walk through the directory tree starting from the current directory
fs.NewDir(".").Walk().
	// Exclude directories and symlinked directories
	Exclude(func(f *fs.File) bool { return f.IsDir() && f.Dir().Ok().IsLink() }).
	// Exclude file symlinks
	Exclude((*fs.File).IsLink).
	// Process each walk result
	ForEach(func(v g.Result[*fs.File]) {
		if v.IsOk() {
			// Print the path of the file if no error occurred
			v.Ok().Path().Ok().Println()
		}
	})

// Iterate over and print the names of files in the current directory with a *.go extension
fs.NewDir("*.go").Glob().ForEach(func(f g.Result[*fs.File]) { f.Ok().Name().Println() })

// Copy the contents of the current directory to a new directory named "copy".
fs.NewDir(".").Copy("copy").Unwrap()

Formatting and Print

Placeholders support automatic, positional, and named values:

g.Format("{} {2} {name}", "auto", "positional", g.Named{"name": "named"})
g.Format("{{{name}}}", g.Named{"name": "value"}) // {value}

Dotted placeholders access DATA — struct fields, map keys, slice indices — never methods (transform values before formatting):

g.Format("{1.Field}", structVal)          // struct field
g.Format("{.outer.inner}", nestedMap)     // map keys
g.Format("{.1.2}", nestedSlice)           // slice indices

Format specs include alignment, width, precision, signs, alternate prefixes, and common representations:

g.Format("{:d} {:c} {:q} {:U} {:#010x}", 42, 65, "go\n", 'A', 255)

Use FormatTo to append to a reusable builder. TryFormat validates templates and returns Result[String]; TryFormatTo appends only when formatting succeeds.

Types can implement custom specs without reflection:

type Price float64

func (p Price) FormatValue(spec g.String) g.String {
    if spec == "currency" {
        return g.String(fmt.Sprintf("$%.2f", p))
    }
    return g.String(fmt.Sprintf("%.2f", p))
}

g.Format("{:currency}", Price(19.95)) // $19.95

Other Maps

MapOrd — Ordered Map

Maintains insertion order.

m := g.NewMapOrd[string, int]()
m.Insert("c", 3)
m.Insert("a", 1)
m.Insert("b", 2)

for k, v := range m.Iter() {
    fmt.Println(k, v)  // c, a, b order
}

m.SortByKey(cmp.Cmp)            // sort by key
m.SortByValue(cmp.Cmp)          // sort by value

MapSafe — Concurrent Map

Thread-safe for concurrent access.

m := g.NewMapSafe[string, int]()

// Safe from multiple goroutines
go func() { m.Insert("a", 1) }()
go func() { m.Get("a") }()

License

MIT License

About

Functional programming framework for Go.

Resources

Stars

62 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages