├── .gitattributes ├── go.mod ├── internal └── assert │ ├── README.md │ └── assert.go ├── bitmask ├── README.md ├── bitmask_test.go └── bitmask.go ├── val ├── default.go ├── pointer.go ├── pointer_value.go ├── pointer_test.go ├── default_test.go ├── pointer_nil.go ├── pointer_nil_test.go └── pointer_value_test.go ├── utils ├── first_of.go ├── invert_map.go ├── reverse_slice.go ├── remove_from_slice.go ├── ternary.go ├── index_in_slice.go ├── any_value_in_map.go ├── map_keys.go ├── value_at_index.go ├── map_values.go ├── slice_map.go ├── chunk_slice.go ├── slice_filter.go ├── slice_find.go ├── ternary_test.go ├── insert_in_slice.go ├── first_of_test.go ├── move_in_slice.go ├── any_value_in_map_test.go ├── unique_slice.go ├── invert_map_test.go ├── index_in_slice_test.go ├── chunk_slice_test.go ├── map_keys_test.go ├── slice_find_test.go ├── slice_filter_test.go ├── map_values_test.go ├── reverse_slice_test.go ├── slice_map_test.go ├── unique_slice_test.go ├── remove_from_slice_test.go ├── move_in_slice_test.go ├── value_at_index_test.go └── insert_in_slice_test.go ├── mathutil ├── number.go ├── clamp.go ├── max.go ├── min.go ├── clamp_test.go ├── max_test.go └── min_test.go ├── view-coverage.sh ├── .gitignore ├── go.sum ├── worker ├── unbuffered_pool_test.go ├── unbuffered_pool.go ├── buffered_pool_test.go └── buffered_pool.go ├── factory ├── chainable_modifier_test.go └── chainable_modifier.go ├── localid ├── generator_test.go ├── README.md └── generator.go ├── README.md ├── LICENSE └── .github └── workflows └── ci.yaml /.gitattributes: -------------------------------------------------------------------------------- 1 | *.go text eol=lf 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aidenwallis/go-utils 2 | 3 | go 1.18 4 | 5 | require golang.org/x/exp v0.0.0-20221110155412-d0897a79cd37 6 | -------------------------------------------------------------------------------- /internal/assert/README.md: -------------------------------------------------------------------------------- 1 | # assert 2 | 3 | Barebones assertion library to avoid having to bring another in and adding a dependency to this package. 4 | -------------------------------------------------------------------------------- /bitmask/README.md: -------------------------------------------------------------------------------- 1 | # bitmask 2 | 3 | Simple utilities for working with bitmasks. Built with generics so any form of unsigned or signed integer is supported. 4 | -------------------------------------------------------------------------------- /val/default.go: -------------------------------------------------------------------------------- 1 | package val 2 | 3 | // Default returns the default value of any type (if pointer, it is nil). 4 | func Default[T any]() T { 5 | var r T 6 | return r 7 | } 8 | -------------------------------------------------------------------------------- /utils/first_of.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // FirstOf returns the first item in a slice 4 | func FirstOf[T any](v []T) T { 5 | if len(v) == 0 { 6 | var zeroValue T 7 | return zeroValue 8 | } 9 | return v[0] 10 | } 11 | -------------------------------------------------------------------------------- /mathutil/number.go: -------------------------------------------------------------------------------- 1 | package mathutil 2 | 3 | import "golang.org/x/exp/constraints" 4 | 5 | // Number is the type that supports any form of integer or float 6 | type Number interface { 7 | float32 | float64 | constraints.Integer 8 | } 9 | -------------------------------------------------------------------------------- /val/pointer.go: -------------------------------------------------------------------------------- 1 | package val 2 | 3 | // AsPointer returns a pointer of a given value, useful if you need to generate a pointer to a string. 4 | // For example, val.AsPointer("string") 5 | func Pointer[T any](v T) *T { 6 | return &v 7 | } 8 | -------------------------------------------------------------------------------- /mathutil/clamp.go: -------------------------------------------------------------------------------- 1 | package mathutil 2 | 3 | // Clamp returns a value restricted between lo and hi. 4 | func Clamp[T Number](v, lo, hi T) T { 5 | if v < lo { 6 | return lo 7 | } 8 | if v > hi { 9 | return hi 10 | } 11 | return v 12 | } 13 | -------------------------------------------------------------------------------- /utils/invert_map.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // InvertMap inverts the map keys and values 4 | func InvertMap[K comparable, V comparable](m map[K]V) map[V]K { 5 | resp := make(map[V]K) 6 | for k, v := range m { 7 | resp[v] = k 8 | } 9 | return resp 10 | } 11 | -------------------------------------------------------------------------------- /utils/reverse_slice.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // ReverseSlice reverses all slice elements in place. Note: this will mutate your given slice. 4 | func ReverseSlice[T any](s []T) { 5 | for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { 6 | s[i], s[j] = s[j], s[i] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /utils/remove_from_slice.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // RemoveFromSlice removes a value from slice at a given index 4 | func RemoveFromSlice[T any](in []T, index int) []T { 5 | if index >= len(in) { 6 | return in 7 | } 8 | return append(in[:index], in[index+1:]...) 9 | } 10 | -------------------------------------------------------------------------------- /val/pointer_value.go: -------------------------------------------------------------------------------- 1 | package val 2 | 3 | // PointerValue returns the shallow deferenced pointer of a given value, or the zeroed value if nil. 4 | func PointerValue[T any](v *T) T { 5 | if v == nil { 6 | var zeroValue T 7 | return zeroValue 8 | } 9 | return *v 10 | } 11 | -------------------------------------------------------------------------------- /mathutil/max.go: -------------------------------------------------------------------------------- 1 | package mathutil 2 | 3 | // Max finds the largest value of all given values 4 | func Max[T Number](a T, rest ...T) T { 5 | result := a 6 | 7 | for _, v := range rest { 8 | if v > result { 9 | result = v 10 | } 11 | } 12 | 13 | return result 14 | } 15 | -------------------------------------------------------------------------------- /mathutil/min.go: -------------------------------------------------------------------------------- 1 | package mathutil 2 | 3 | // Min finds the smallest value of all given values 4 | func Min[T Number](a T, rest ...T) T { 5 | result := a 6 | 7 | for _, v := range rest { 8 | if v < result { 9 | result = v 10 | } 11 | } 12 | 13 | return result 14 | } 15 | -------------------------------------------------------------------------------- /view-coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This small script simply runs the tests and opens a browser window of the results. 4 | # From: https://stackoverflow.com/a/27284510 5 | 6 | t="/tmp/go-cover.$$.tmp" 7 | go test -coverprofile=$t -race ./... $@ && go tool cover -html=$t && unlink $t 8 | -------------------------------------------------------------------------------- /utils/ternary.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // Ternary allows you to write ternary expressions in go, if exp is true, then ifCond is returned, else, elseCond is returned. 4 | func Ternary[T any](exp bool, ifCond T, elseCond T) T { 5 | if exp { 6 | return ifCond 7 | } 8 | return elseCond 9 | } 10 | -------------------------------------------------------------------------------- /utils/index_in_slice.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // IndexInSlice returns whether an index exists within a given slice 4 | func IndexInSlice[T any](v []T, index int) bool { 5 | if index < 0 { 6 | // slices cannot be less than 0 in size 7 | return false 8 | } 9 | return len(v) > index 10 | } 11 | -------------------------------------------------------------------------------- /utils/any_value_in_map.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // AnyValueInMap iterates through values and returns true if any has a key in the map. 4 | func AnyValueInMap[T comparable, V any](m map[T]V, values ...T) bool { 5 | for _, v := range values { 6 | if _, ok := m[v]; ok { 7 | return true 8 | } 9 | } 10 | return false 11 | } 12 | -------------------------------------------------------------------------------- /utils/map_keys.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // MapKeys returns all keys in a given map, **the order will not be stable, you should sort these values if needed.** 4 | func MapKeys[K comparable, V any](v map[K]V) []K { 5 | resp := make([]K, 0, len(v)) 6 | for k := range v { 7 | resp = append(resp, k) 8 | } 9 | return resp 10 | } 11 | -------------------------------------------------------------------------------- /utils/value_at_index.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/aidenwallis/go-utils/val" 4 | 5 | // ValueAtIndex returns the value at a given index in a slice 6 | func ValueAtIndex[T any](v []T, index int) (T, bool) { 7 | if IndexInSlice(v, index) { 8 | return v[index], true 9 | } 10 | return val.Default[T](), false 11 | } 12 | -------------------------------------------------------------------------------- /val/pointer_test.go: -------------------------------------------------------------------------------- 1 | package val_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/val" 8 | ) 9 | 10 | func TestPointer(t *testing.T) { 11 | t.Parallel() 12 | value := "string" 13 | assert.Equal(t, value, val.PointerValue(val.Pointer(value))) 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /utils/map_values.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // MapValues returns all values in a given map, **the order is not stable, you should sort it if you need a stable order.** 4 | func MapValues[K comparable, V any](in map[K]V) []V { 5 | out := make([]V, 0, len(in)) 6 | for _, v := range in { 7 | out = append(out, v) 8 | } 9 | return out 10 | } 11 | -------------------------------------------------------------------------------- /val/default_test.go: -------------------------------------------------------------------------------- 1 | package val_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/val" 8 | ) 9 | 10 | func TestDefault(t *testing.T) { 11 | t.Parallel() 12 | assert.Equal(t, 0, val.Default[int]()) 13 | assert.Equal(t, nil, val.Default[*int]()) 14 | } 15 | -------------------------------------------------------------------------------- /utils/slice_map.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // SliceMap takes a slice, calls the func given for every item, then returns a new slice of the 4 | // transformed value. 5 | func SliceMap[T any, R any](input []T, mapFunc func(T) R) []R { 6 | v := make([]R, len(input)) 7 | for i, item := range input { 8 | v[i] = mapFunc(item) 9 | } 10 | return v 11 | } 12 | -------------------------------------------------------------------------------- /utils/chunk_slice.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // ChunkSlice splits a slice into chunks of a given size. 4 | func ChunkSlice[T any](s []T, chunkSize int) [][]T { 5 | var chunks [][]T 6 | for i := 0; i < len(s); i += chunkSize { 7 | end := i + chunkSize 8 | if end > len(s) { 9 | end = len(s) 10 | } 11 | chunks = append(chunks, s[i:end]) 12 | } 13 | return chunks 14 | } 15 | -------------------------------------------------------------------------------- /val/pointer_nil.go: -------------------------------------------------------------------------------- 1 | package val 2 | 3 | // PointerNil behaves similarly to Pointer, except if the value of v is a zeroed value (eg. empty string), it returns nil instead. 4 | func PointerNil[T comparable](v T) *T { 5 | var zeroValue T 6 | if zeroValue == v { 7 | // zeroed values with PointerNil should therefore return nil 8 | return nil 9 | } 10 | return Pointer(v) 11 | } 12 | -------------------------------------------------------------------------------- /utils/slice_filter.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // SliceFilter calls testFunc on every item given in input, and only returns those that return true. 4 | func SliceFilter[T any](input []T, testFunc func(T) bool) []T { 5 | resp := make([]T, 0, len(input)) 6 | for _, item := range input { 7 | if testFunc(item) { 8 | resp = append(resp, item) 9 | } 10 | } 11 | return resp 12 | } 13 | -------------------------------------------------------------------------------- /val/pointer_nil_test.go: -------------------------------------------------------------------------------- 1 | package val_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/val" 8 | ) 9 | 10 | func TestPointerNil(t *testing.T) { 11 | t.Parallel() 12 | assert.Equal(t, nil, val.PointerNil("")) 13 | assert.Equal(t, "some string", val.PointerValue(val.PointerNil("some string"))) 14 | } 15 | -------------------------------------------------------------------------------- /mathutil/clamp_test.go: -------------------------------------------------------------------------------- 1 | package mathutil_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/mathutil" 8 | ) 9 | 10 | func TestClamp(t *testing.T) { 11 | assert.Equal(t, 20, mathutil.Clamp(1, 20, 100)) 12 | assert.Equal(t, 100, mathutil.Clamp(101, 20, 100)) 13 | assert.Equal(t, 35, mathutil.Clamp(35, 20, 100)) 14 | } 15 | -------------------------------------------------------------------------------- /utils/slice_find.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // SliceFilter calls testFunc on every item given in input, and returns the first item that returns true 4 | func SliceFind[T any](input []T, testFunc func(T) bool) (item T, ok bool) { 5 | for _, item := range input { 6 | if testFunc(item) { 7 | return item, true 8 | } 9 | } 10 | 11 | // else return zero value 12 | var zero T 13 | return zero, false 14 | } 15 | -------------------------------------------------------------------------------- /utils/ternary_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/utils" 8 | ) 9 | 10 | func TestTernary(t *testing.T) { 11 | t.Parallel() 12 | 13 | assert.Equal(t, "a", utils.Ternary(1 == 1, "a", "b")) // nolint:staticcheck // intended 14 | assert.Equal(t, "b", utils.Ternary(1 == 2, "a", "b")) 15 | } 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= 2 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= 3 | golang.org/x/exp v0.0.0-20221110155412-d0897a79cd37 h1:wKMvZzBFHbOCGvF2OmxR5Fqv/jDlkt7slnPz5ejEU8A= 4 | golang.org/x/exp v0.0.0-20221110155412-d0897a79cd37/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 5 | -------------------------------------------------------------------------------- /utils/insert_in_slice.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // InsertInSlice inserts a value into a slice at a given position 4 | // 5 | // If the position gives is out of bounds then it resorts to inserting to the end of the slice 6 | func InsertInSlice[T any](in []T, valueToAdd T, index int) []T { 7 | if index > len(in) { 8 | index = len(in) 9 | } 10 | return append(in[:index], append([]T{valueToAdd}, in[index:]...)...) 11 | } 12 | -------------------------------------------------------------------------------- /utils/first_of_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/utils" 8 | ) 9 | 10 | func TestFirstOf(t *testing.T) { 11 | t.Parallel() 12 | 13 | assert.Equal(t, "a", utils.FirstOf([]string{"a", "b", "c"})) 14 | assert.Equal(t, "", utils.FirstOf([]string{})) 15 | assert.Equal(t, "", utils.FirstOf[string](nil)) 16 | } 17 | -------------------------------------------------------------------------------- /mathutil/max_test.go: -------------------------------------------------------------------------------- 1 | package mathutil_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/mathutil" 8 | ) 9 | 10 | func TestMax(t *testing.T) { 11 | t.Parallel() 12 | assert.Equal(t, 6, mathutil.Max(1, 2, 3, 4, 5, 6)) 13 | assert.Equal(t, 4, mathutil.Max(4)) 14 | assert.Equal(t, 7.0, mathutil.Max(4.0, 3.234, 1.0, 1.01, 5.0, 7.0, 2.0, 5.0, 5.0)) 15 | } 16 | -------------------------------------------------------------------------------- /mathutil/min_test.go: -------------------------------------------------------------------------------- 1 | package mathutil_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/mathutil" 8 | ) 9 | 10 | func TestMin(t *testing.T) { 11 | t.Parallel() 12 | assert.Equal(t, 1, mathutil.Min(1, 2, 3, 4, 5, 6)) 13 | assert.Equal(t, 4, mathutil.Min(4)) 14 | assert.Equal(t, 1.0, mathutil.Min(4.0, 3.234, 1.0, 1.01, 5.0, 7.0, 2.0, 5.0, 5.0)) 15 | } 16 | -------------------------------------------------------------------------------- /val/pointer_value_test.go: -------------------------------------------------------------------------------- 1 | package val_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/val" 8 | ) 9 | 10 | func TestPointerValue(t *testing.T) { 11 | t.Parallel() 12 | 13 | value := "some string" 14 | assert.Equal(t, value, val.PointerValue(val.Pointer("some string"))) 15 | 16 | var nilString *string 17 | assert.Equal(t, "", val.PointerValue(nilString)) 18 | } 19 | -------------------------------------------------------------------------------- /utils/move_in_slice.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // MoveInSlice moves a value within the slice to a specified new position 4 | // 5 | // If either the old or new position is out of bounds, the existing slice contents is returned. 6 | func MoveInSlice[T any](in []T, oldPosition, newPosition int) []T { 7 | if oldPosition >= len(in) { 8 | return in 9 | } 10 | value := in[oldPosition] 11 | return InsertInSlice(RemoveFromSlice(in, oldPosition), value, newPosition) 12 | } 13 | -------------------------------------------------------------------------------- /utils/any_value_in_map_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/utils" 8 | ) 9 | 10 | func TestAnyValueInMap(t *testing.T) { 11 | t.Parallel() 12 | 13 | m := map[string]struct{}{ 14 | "1": {}, 15 | "2": {}, 16 | "3": {}, 17 | } 18 | 19 | assert.True(t, utils.AnyValueInMap(m, "foobar", "1")) 20 | assert.False(t, utils.AnyValueInMap(m, "foobar")) 21 | } 22 | -------------------------------------------------------------------------------- /utils/unique_slice.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // UniqueSlice is a helper function that iterates through your slice and returns a copy with only the unique values. 4 | func UniqueSlice[T comparable](input []T) []T { 5 | resp := make([]T, 0, len(input)) 6 | valuesMap := make(map[T]struct{}, len(resp)) 7 | for _, item := range input { 8 | if _, ok := valuesMap[item]; !ok { 9 | valuesMap[item] = struct{}{} 10 | resp = append(resp, item) 11 | } 12 | } 13 | return resp 14 | } 15 | -------------------------------------------------------------------------------- /internal/assert/assert.go: -------------------------------------------------------------------------------- 1 | package assert 2 | 3 | import "testing" 4 | 5 | // Equal checks if two values are equal else fails the test 6 | func Equal[T comparable](t *testing.T, expected T, v T) { 7 | if expected != v { 8 | t.Errorf("expected %#v but got %#v", expected, v) 9 | } 10 | } 11 | 12 | // False checks whether v is false 13 | func False(t *testing.T, v bool) { 14 | Equal(t, false, v) 15 | } 16 | 17 | // True checks whether v is true 18 | func True(t *testing.T, v bool) { 19 | Equal(t, true, v) 20 | } 21 | -------------------------------------------------------------------------------- /utils/invert_map_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/utils" 8 | ) 9 | 10 | func TestInvertMap(t *testing.T) { 11 | t.Parallel() 12 | 13 | input := map[string]int{ 14 | "a": 1, 15 | "b": 2, 16 | "c": 3, 17 | } 18 | 19 | output := map[int]string{ 20 | 1: "a", 21 | 2: "b", 22 | 3: "c", 23 | } 24 | 25 | for k, v := range input { 26 | assert.Equal(t, v, utils.InvertMap(output)[k]) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /worker/unbuffered_pool_test.go: -------------------------------------------------------------------------------- 1 | package worker_test 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/aidenwallis/go-utils/internal/assert" 8 | "github.com/aidenwallis/go-utils/worker" 9 | ) 10 | 11 | func TestUnbufferedPool(t *testing.T) { 12 | t.Parallel() 13 | 14 | p := worker.NewUnbufferedPool() 15 | 16 | var ( 17 | m sync.Mutex 18 | v int 19 | ) 20 | 21 | for i := 0; i < 10; i++ { 22 | p.Do(func() { 23 | m.Lock() 24 | v++ 25 | m.Unlock() 26 | }) 27 | } 28 | 29 | p.Wait() 30 | 31 | assert.Equal(t, 10, v) 32 | } 33 | -------------------------------------------------------------------------------- /utils/index_in_slice_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/utils" 8 | ) 9 | 10 | func TestIndexInSlice(t *testing.T) { 11 | t.Parallel() 12 | v := []int{0, 1, 2, 3} 13 | assert.False(t, utils.IndexInSlice(v, -1)) 14 | assert.True(t, utils.IndexInSlice(v, 0)) 15 | assert.True(t, utils.IndexInSlice(v, 1)) 16 | assert.True(t, utils.IndexInSlice(v, 2)) 17 | assert.True(t, utils.IndexInSlice(v, 3)) 18 | assert.False(t, utils.IndexInSlice(v, 4)) 19 | } 20 | -------------------------------------------------------------------------------- /utils/chunk_slice_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/utils" 8 | ) 9 | 10 | func TestChunkSlice(t *testing.T) { 11 | t.Parallel() 12 | 13 | in := []int{1, 2, 3, 4, 5, 6, 7, 8} 14 | expected := [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8}} 15 | 16 | out := utils.ChunkSlice(in, 3) 17 | 18 | for i, chunk := range out { 19 | assert.Equal(t, len(expected[i]), len(chunk)) 20 | for j, v := range chunk { 21 | assert.Equal(t, expected[i][j], v) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /utils/map_keys_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/aidenwallis/go-utils/internal/assert" 8 | "github.com/aidenwallis/go-utils/utils" 9 | ) 10 | 11 | func TestMapKeys(t *testing.T) { 12 | t.Parallel() 13 | 14 | out := []int{1, 2, 3} 15 | in := map[int]struct{}{ 16 | 1: {}, 17 | 2: {}, 18 | 3: {}, 19 | } 20 | 21 | v := utils.MapKeys(in) 22 | sort.SliceStable(v, func(i, j int) bool { 23 | return v[i] < v[j] 24 | }) 25 | 26 | assert.Equal(t, 3, len(v)) 27 | for i := range out { 28 | assert.Equal(t, out[i], v[i]) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /utils/slice_find_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/utils" 8 | ) 9 | 10 | func TestSliceFind(t *testing.T) { 11 | t.Parallel() 12 | 13 | item, ok := utils.SliceFind([]string{"a", "b", "c"}, func(s string) bool { 14 | return s == "b" 15 | }) 16 | assert.Equal(t, "b", item) 17 | assert.True(t, ok) 18 | 19 | item, ok = utils.SliceFind([]string{"a", "b", "c"}, func(s string) bool { 20 | return s == "d" 21 | }) 22 | assert.Equal(t, "", item) 23 | assert.False(t, ok) 24 | } 25 | -------------------------------------------------------------------------------- /utils/slice_filter_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/utils" 8 | ) 9 | 10 | func TestSliceFilter(t *testing.T) { 11 | t.Parallel() 12 | 13 | input := []int{ 14 | 0, 15 | 1, 16 | 2, 17 | 3, 18 | 4, 19 | 5, 20 | 6, 21 | } 22 | 23 | output := []int{ 24 | 0, 25 | 2, 26 | 4, 27 | 6, 28 | } 29 | 30 | v := utils.SliceFilter(input, func(i int) bool { 31 | return i%2 == 0 32 | }) 33 | 34 | for i, vv := range v { 35 | assert.Equal(t, output[i], vv) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /bitmask/bitmask_test.go: -------------------------------------------------------------------------------- 1 | package bitmask_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/bitmask" 7 | "github.com/aidenwallis/go-utils/internal/assert" 8 | ) 9 | 10 | func TestBits(t *testing.T) { 11 | t.Parallel() 12 | 13 | const ( 14 | a = 1 << 0 15 | b = 1 << 1 16 | c = 1 << 2 17 | ) 18 | 19 | assert.Equal(t, a|b, bitmask.Add(a, b)) 20 | assert.Equal(t, a, bitmask.Remove(a|b, b)) 21 | assert.True(t, bitmask.Has(a|b, b)) 22 | assert.False(t, bitmask.Has(a|b, c)) 23 | 24 | assert.Equal(t, a|b, bitmask.Toggle(a, b)) 25 | assert.Equal(t, b, bitmask.Toggle(a|b, a)) 26 | } 27 | -------------------------------------------------------------------------------- /utils/map_values_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "sort" 5 | "testing" 6 | 7 | "github.com/aidenwallis/go-utils/internal/assert" 8 | "github.com/aidenwallis/go-utils/utils" 9 | ) 10 | 11 | func TestMapValues(t *testing.T) { 12 | t.Parallel() 13 | 14 | out := []int{1, 2, 3} 15 | in := map[string]int{ 16 | "one": 1, 17 | "two": 2, 18 | "three": 3, 19 | } 20 | 21 | v := utils.MapValues(in) 22 | sort.SliceStable(v, func(i, j int) bool { 23 | return v[i] < v[j] 24 | }) 25 | 26 | assert.Equal(t, 3, len(v)) 27 | for i := range out { 28 | assert.Equal(t, out[i], v[i]) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /factory/chainable_modifier_test.go: -------------------------------------------------------------------------------- 1 | package factory_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/factory" 7 | "github.com/aidenwallis/go-utils/internal/assert" 8 | ) 9 | 10 | func TestChainableModifier(t *testing.T) { 11 | t.Parallel() 12 | 13 | type testValue struct { 14 | Foo bool 15 | Bar bool 16 | } 17 | 18 | chain := factory.ChainableModifier(func() *testValue { 19 | return &testValue{ 20 | Foo: true, 21 | Bar: false, 22 | } 23 | }) 24 | 25 | out := chain(func(v *testValue) { 26 | v.Foo = false 27 | }) 28 | assert.False(t, out.Foo) 29 | assert.False(t, out.Bar) 30 | } 31 | -------------------------------------------------------------------------------- /localid/generator_test.go: -------------------------------------------------------------------------------- 1 | package localid_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/localid" 8 | ) 9 | 10 | func TestLocalID(t *testing.T) { 11 | t.Parallel() 12 | 13 | g := localid.NewGenerator() 14 | 15 | assert.Equal(t, 0, g.ID()) 16 | assert.Equal(t, 1, g.ID()) 17 | assert.Equal(t, 2, g.ID()) 18 | assert.Equal(t, 3, g.ID()) 19 | assert.Equal(t, 4, g.ID()) 20 | 21 | g.ReturnIDs(5, 6, 7, 8, 9) 22 | 23 | assert.Equal(t, 5, g.ID()) 24 | assert.Equal(t, 6, g.ID()) 25 | assert.Equal(t, 7, g.ID()) 26 | assert.Equal(t, 8, g.ID()) 27 | assert.Equal(t, 9, g.ID()) 28 | } 29 | -------------------------------------------------------------------------------- /utils/reverse_slice_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/aidenwallis/go-utils/internal/assert" 8 | "github.com/aidenwallis/go-utils/utils" 9 | ) 10 | 11 | func TestReverseSlice(t *testing.T) { 12 | t.Parallel() 13 | 14 | mustJSON := func(v interface{}) string { 15 | bs, err := json.Marshal(v) 16 | if err != nil { 17 | t.Error("failed to marshal json") 18 | } 19 | return string(bs) 20 | } 21 | 22 | v := []int{1, 2, 3, 4, 5} 23 | assert.Equal(t, mustJSON([]int{1, 2, 3, 4, 5}), mustJSON(v)) 24 | utils.ReverseSlice(v) 25 | assert.Equal(t, mustJSON([]int{5, 4, 3, 2, 1}), mustJSON(v)) 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-utils 2 | 3 | [![codecov](https://codecov.io/gh/aidenwallis/go-utils/branch/main/graph/badge.svg?token=AT2T41NQ7K)](https://codecov.io/gh/aidenwallis/go-utils) [![Go Reference](https://pkg.go.dev/badge/github.com/aidenwallis/go-utils.svg)](https://pkg.go.dev/github.com/aidenwallis/go-utils) 4 | 5 | A set of common Go utils I use throughout my projects, such as [Fossabot](https://fossabot.com). 6 | 7 | I originally created this library to help me break down my large project monorepos into smaller repos, and share more code between them, but decided to open source it, in case some of these utils are useful to others. 8 | 9 | A lot of these utils use [generics](https://go.dev/doc/tutorial/generics) - a new feature introduced in Go 1.18, therefore, this package is only supported on Go 1.18+. -------------------------------------------------------------------------------- /utils/slice_map_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/utils" 8 | ) 9 | 10 | func TestSliceMap(t *testing.T) { 11 | t.Parallel() 12 | 13 | input := []struct { 14 | a int 15 | }{ 16 | {a: 1}, 17 | {a: 2}, 18 | {a: 3}, 19 | } 20 | 21 | output := []struct { 22 | a int 23 | b int 24 | }{ 25 | {a: 1, b: 2}, 26 | {a: 2, b: 3}, 27 | {a: 3, b: 4}, 28 | } 29 | 30 | iterator := func(v struct{ a int }) struct { 31 | a int 32 | b int 33 | } { 34 | return struct { 35 | a int 36 | b int 37 | }{a: v.a, b: v.a + 1} 38 | } 39 | 40 | for i, v := range utils.SliceMap(input, iterator) { 41 | assert.Equal(t, output[i].a, v.a) 42 | assert.Equal(t, output[i].b, v.b) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /utils/unique_slice_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/utils" 8 | ) 9 | 10 | func TestUniqueSlice(t *testing.T) { 11 | t.Parallel() 12 | 13 | t.Run("ints", func(t *testing.T) { 14 | t.Parallel() 15 | 16 | input := []int{1, 2, 3, 4, 5, 1, 2, 2, 6, 6} 17 | output := []int{1, 2, 3, 4, 5, 6} 18 | v := utils.UniqueSlice(input) 19 | for i, vv := range v { 20 | assert.Equal(t, output[i], vv) 21 | } 22 | }) 23 | 24 | t.Run("strings", func(t *testing.T) { 25 | t.Parallel() 26 | 27 | input := []string{"a", "b", "A", "b", "C", "d", "E"} 28 | output := []string{"a", "b", "A", "C", "d", "E"} 29 | v := utils.UniqueSlice(input) 30 | for i, vv := range v { 31 | assert.Equal(t, output[i], vv) 32 | } 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /localid/README.md: -------------------------------------------------------------------------------- 1 | # localid 2 | 3 | Local ID will give you integers on demand that are not currently in use from the generator. This works by keeping an internal counter of the highest ID it's generated, storing "returned" IDs in a queue, and simply generating more as required. 4 | 5 | This means that IDs may be reused from this generator, if you use `ReturnIDs()`. 6 | 7 | The benefit of this approach compared to just constantly incrementing an integer is that your ID space stays relatively small, and works well for long-running applications that need to frequently have a cheap way to identify an object, but don't want to generate unnecessarily large IDs, or deal with potential integer overflows (assuming they return IDs frequently enough). 8 | 9 | This is used in [Fossabot](https://fossabot.com) to uniquely identify tasks within the distributed scheduler processes. 10 | -------------------------------------------------------------------------------- /bitmask/bitmask.go: -------------------------------------------------------------------------------- 1 | package bitmask 2 | 3 | import "golang.org/x/exp/constraints" 4 | 5 | // Bit is a generic type that exports the supported values that can be used in the bitmask. 6 | // Any form of integer is currently supported. 7 | type Bit interface { 8 | constraints.Integer 9 | } 10 | 11 | // Add adds a given bit to the sum. 12 | func Add[T Bit](sum, bit T) T { 13 | sum |= bit 14 | return sum 15 | } 16 | 17 | // Remove removes bit from the sum. 18 | func Remove[T Bit](sum, bit T) T { 19 | sum &= ^bit 20 | return sum 21 | } 22 | 23 | // Has checks whether a given bit exists in sum. 24 | func Has[T Bit](sum, bit T) bool { 25 | return (sum & bit) == bit 26 | } 27 | 28 | // Toggle will either add or remove bit from sum depending on whether it currently exists in the bitmask. 29 | func Toggle[T Bit](sum, bit T) T { 30 | if Has(sum, bit) { 31 | return Remove(sum, bit) 32 | } 33 | return Add(sum, bit) 34 | } 35 | -------------------------------------------------------------------------------- /utils/remove_from_slice_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/utils" 8 | ) 9 | 10 | func TestRemoveFromSlice(t *testing.T) { 11 | t.Parallel() 12 | 13 | testCases := map[string]struct { 14 | in []int 15 | out []int 16 | indexToRemove int 17 | }{ 18 | "normal operation": { 19 | in: []int{0, 1, 2, 3}, 20 | out: []int{0, 1, 3}, 21 | indexToRemove: 2, 22 | }, 23 | 24 | "does not panic when out of bounds": { 25 | in: []int{0, 1, 2}, 26 | out: []int{0, 1, 2}, 27 | indexToRemove: 4, 28 | }, 29 | } 30 | 31 | for name, testCase := range testCases { 32 | testCase := testCase 33 | 34 | t.Run(name, func(t *testing.T) { 35 | t.Parallel() 36 | 37 | result := utils.RemoveFromSlice(testCase.in, testCase.indexToRemove) 38 | assert.Equal(t, len(testCase.out), len(result)) 39 | for i, v := range result { 40 | assert.Equal(t, testCase.out[i], v) 41 | } 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Aiden Wallis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils/move_in_slice_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/utils" 8 | ) 9 | 10 | func TestMoveInSlice(t *testing.T) { 11 | t.Parallel() 12 | 13 | testCases := map[string]struct { 14 | in []int 15 | out []int 16 | oldPosition int 17 | newPosition int 18 | }{ 19 | "normal operation": { 20 | in: []int{0, 1, 2, 3, 4}, 21 | out: []int{0, 3, 1, 2, 4}, 22 | oldPosition: 3, 23 | newPosition: 1, 24 | }, 25 | 26 | "out of bounds in old position": { 27 | in: []int{0, 1, 2, 3, 4}, 28 | out: []int{0, 1, 2, 3, 4}, 29 | oldPosition: 5, 30 | newPosition: 0, 31 | }, 32 | } 33 | 34 | for name, testCase := range testCases { 35 | testCase := testCase 36 | 37 | t.Run(name, func(t *testing.T) { 38 | t.Parallel() 39 | 40 | result := utils.MoveInSlice(testCase.in, testCase.oldPosition, testCase.newPosition) 41 | 42 | assert.Equal(t, len(testCase.out), len(result)) 43 | for i, v := range result { 44 | assert.Equal(t, testCase.out[i], v) 45 | } 46 | }) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /utils/value_at_index_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aidenwallis/go-utils/internal/assert" 7 | "github.com/aidenwallis/go-utils/utils" 8 | "github.com/aidenwallis/go-utils/val" 9 | ) 10 | 11 | func TestValueAtIndex(t *testing.T) { 12 | t.Parallel() 13 | 14 | // handles normal slices 15 | { 16 | v := []int{0, 1, 2} 17 | 18 | { 19 | r, ok := utils.ValueAtIndex(v, 0) 20 | assert.Equal(t, 0, r) 21 | assert.True(t, ok) 22 | } 23 | 24 | { 25 | r, ok := utils.ValueAtIndex(v, 1) 26 | assert.Equal(t, 1, r) 27 | assert.True(t, ok) 28 | } 29 | 30 | { 31 | r, ok := utils.ValueAtIndex(v, 2) 32 | assert.Equal(t, 2, r) 33 | assert.True(t, ok) 34 | } 35 | 36 | { 37 | r, ok := utils.ValueAtIndex(v, 3) 38 | assert.Equal(t, 0, r) 39 | assert.False(t, ok) 40 | } 41 | } 42 | 43 | // handles pointer slices 44 | { 45 | v := []*int{val.Pointer(0)} 46 | 47 | { 48 | r, ok := utils.ValueAtIndex(v, 0) 49 | assert.Equal(t, 0, *r) 50 | assert.True(t, ok) 51 | } 52 | 53 | { 54 | r, ok := utils.ValueAtIndex(v, 1) 55 | assert.Equal(t, nil, r) 56 | assert.False(t, ok) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /localid/generator.go: -------------------------------------------------------------------------------- 1 | package localid 2 | 3 | import ( 4 | "container/list" 5 | "sync" 6 | ) 7 | 8 | // Generator implements the local id generator 9 | type Generator interface { 10 | // ID returns a new ID 11 | ID() int 12 | // ReturnIDs adds new IDs back to the queue. 13 | ReturnIDs(...int) 14 | } 15 | 16 | // GeneratorImpl is the struct that implements Generator 17 | type GeneratorImpl struct { 18 | mu sync.Mutex 19 | lastID int 20 | q *list.List 21 | } 22 | 23 | // NewGenerator creates a new Generator instance 24 | func NewGenerator() Generator { 25 | return &GeneratorImpl{ 26 | lastID: -1, // start at 0 27 | q: list.New(), 28 | } 29 | } 30 | 31 | func (g *GeneratorImpl) ID() int { 32 | g.mu.Lock() 33 | defer g.mu.Unlock() 34 | if id := g.q.Front(); id != nil { 35 | g.q.Remove(id) 36 | // if an ID is available in queue, use that 37 | return id.Value.(int) 38 | } 39 | 40 | // else, generate new DI 41 | g.lastID++ 42 | return g.lastID 43 | } 44 | 45 | // ReturnIDs adds new IDs back to the queue. 46 | func (g *GeneratorImpl) ReturnIDs(items ...int) { 47 | g.mu.Lock() 48 | for _, id := range items { 49 | g.q.PushBack(id) 50 | } 51 | g.mu.Unlock() 52 | } 53 | -------------------------------------------------------------------------------- /utils/insert_in_slice_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/aidenwallis/go-utils/internal/assert" 8 | "github.com/aidenwallis/go-utils/utils" 9 | ) 10 | 11 | func TestInsertInSlice(t *testing.T) { 12 | t.Parallel() 13 | 14 | testCases := map[string]struct { 15 | in []int 16 | out []int 17 | valueToAdd int 18 | index int 19 | }{ 20 | "normal operation": { 21 | in: []int{0, 1, 2, 3, 4}, 22 | out: []int{0, 1, 2000, 2, 3, 4}, 23 | valueToAdd: 2000, 24 | index: 2, 25 | }, 26 | 27 | "out of bounds": { 28 | in: []int{0, 1, 2, 3}, 29 | out: []int{0, 1, 2, 3, 2000}, 30 | valueToAdd: 2000, 31 | index: 2000, 32 | }, 33 | } 34 | 35 | for name, testCase := range testCases { 36 | testCase := testCase 37 | 38 | t.Run(name, func(t *testing.T) { 39 | t.Parallel() 40 | 41 | result := utils.InsertInSlice(testCase.in, testCase.valueToAdd, testCase.index) 42 | log.Println(result) 43 | 44 | assert.Equal(t, len(testCase.out), len(result)) 45 | for i, v := range result { 46 | assert.Equal(t, testCase.out[i], v) 47 | } 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /factory/chainable_modifier.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | // ChainableModifier creates a factory that produces an initial value from initFactory, then lets you change modifiers to modify that value. 4 | // You're then returned the value once all modifiers have ran over it. This can be particularly useful when building mock data in tests. 5 | // 6 | // func example() { 7 | // chain := ChainableModifier(func() *pb.MyRequest { 8 | // return &pb.MyRequest{Foo: true, Bar: false} 9 | // }) 10 | // 11 | // firstRequest := chain() // returns &pb.MyRequest{Foo: true, Bar: false} 12 | // 13 | // secondRequestWithBarTrue := chain(func(v *pb.MyRequest) { 14 | // v.Bar = true 15 | // }) // returns &pb.MyRequest{Foo: true, Bar: true} 16 | // 17 | // thirdRequestWithValuesSwapped := chain(func(v *pb.MyRequest) { 18 | // v.Foo = false 19 | // }, func(v *pb.MyRequest) { 20 | // v.Bar = true 21 | // }) // returns &pb.MyRequest{Foo: false, Bar: true} 22 | // } 23 | func ChainableModifier[T any](initFactory func() T) func(modifiers ...func(T)) T { 24 | return func(modifiers ...func(T)) T { 25 | out := initFactory() 26 | 27 | for _, fn := range modifiers { 28 | fn(out) 29 | } 30 | 31 | return out 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /worker/unbuffered_pool.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import "sync" 4 | 5 | // UnbufferedPool is an interface which implements BufferedPoolImpl, allowing you to mock out the pool for tests. 6 | type UnbufferedPool interface { 7 | // Do enqueues a task and blocks the goroutine until it's enqueued. 8 | Do(func()) 9 | 10 | // Wait blocks the goroutine until all tasks are complete. 11 | Wait() 12 | } 13 | 14 | // compile time assertion 15 | var _ UnbufferedPool = (*UnbufferedPoolImpl)(nil) 16 | 17 | // UnbufferedPoolImpl is an implementation that is compatible with UnbufferedPool. 18 | // 19 | // The main purpose of this tool is to let you ensure all goroutines have been closed before exiting your app, you can 20 | // pass an instance of this around everywhere instead. 21 | type UnbufferedPoolImpl struct { 22 | // WaitGroup tracks how many running/queued tasks there are, we expose Wait() so you can wait until all tasks are complete. 23 | WaitGroup sync.WaitGroup 24 | } 25 | 26 | // NewUnbufferedPool creates a new instance of BufferedPoolImpl 27 | func NewUnbufferedPool() *UnbufferedPoolImpl { 28 | return &UnbufferedPoolImpl{} 29 | } 30 | 31 | // Do increments the wait group and invokes the goroutine, then decrements it. 32 | func (w *UnbufferedPoolImpl) Do(cb func()) { 33 | w.WaitGroup.Add(1) 34 | go func(cb func()) { 35 | cb() 36 | w.WaitGroup.Done() 37 | }(cb) 38 | } 39 | 40 | // Wait blocks the goroutine until all tasks are complete. 41 | func (w *UnbufferedPoolImpl) Wait() { 42 | w.WaitGroup.Wait() 43 | } 44 | -------------------------------------------------------------------------------- /worker/buffered_pool_test.go: -------------------------------------------------------------------------------- 1 | package worker_test 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/aidenwallis/go-utils/internal/assert" 8 | "github.com/aidenwallis/go-utils/worker" 9 | ) 10 | 11 | func TestBufferedPool(t *testing.T) { 12 | t.Parallel() 13 | 14 | t.Run("generic use", func(t *testing.T) { 15 | t.Parallel() 16 | 17 | outputs := []int{} 18 | outputsMutex := sync.Mutex{} 19 | 20 | pool := worker.NewBufferedPool(10, 1).Interface() 21 | 22 | for i := 0; i < 20; i++ { 23 | pool.Do(func() { 24 | outputsMutex.Lock() 25 | outputs = append(outputs, 123) 26 | outputsMutex.Unlock() 27 | }) 28 | } 29 | 30 | pool.WaitAndClose() 31 | 32 | assert.Equal(t, len(outputs), 20) 33 | }) 34 | 35 | t.Run("try to write to filled queue", func(t *testing.T) { 36 | // this is kinda hacky, but essentially we'll block the workers using this channel until we've validated this test works. 37 | ch := make(chan struct{}, 1) 38 | pool := worker.NewBufferedPool(10, 1) 39 | defer pool.Close() 40 | 41 | // 10 workers + 1 in the queue 42 | for i := 0; i < 11; i++ { 43 | pool.Do(func() { 44 | // this will pause the goroutine until we validate TryDo returns false 45 | <-ch 46 | }) 47 | } 48 | 49 | // all goroutines blocked + queue is full, should be false now 50 | assert.False(t, pool.TryDo(func() {})) 51 | close(ch) // closing the channel will exit all blocked workers 52 | 53 | // wait until the prev test is empty, then we should get true next 54 | pool.Wait() 55 | 56 | assert.True(t, pool.TryDo(func() {})) 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 13 | pull-requests: read 14 | 15 | jobs: 16 | lint: 17 | name: lint | ${{ matrix.go_version }} 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | matrix: 22 | go_version: ["1.18", "1.19", "1.20", "1.21"] 23 | 24 | steps: 25 | - uses: actions/setup-go@v4 26 | with: 27 | go-version: ${{ matrix.go_version }} 28 | - uses: actions/checkout@v3 29 | - name: golangci-lint 30 | uses: golangci/golangci-lint-action@v3 31 | with: 32 | only-new-issues: true 33 | 34 | test: 35 | name: test | ${{ matrix.go_version }} 36 | runs-on: ubuntu-latest 37 | 38 | strategy: 39 | matrix: 40 | go_version: ["1.18", "1.19", "1.20"] 41 | 42 | steps: 43 | - name: Setup go ${{ matrix.go_version }} 44 | uses: actions/setup-go@v4 45 | with: 46 | go-version: ${{ matrix.go_version }} 47 | id: go 48 | 49 | - name: Checkout code 50 | uses: actions/checkout@v1 51 | 52 | - name: Make out dir 53 | run: | 54 | mkdir out 55 | 56 | - name: Run tests 57 | run: | 58 | go test -race ./... -coverprofile=out/coverage.txt -covermode=atomic 59 | 60 | - name: Upload coverage 61 | uses: codecov/codecov-action@v2 62 | with: 63 | files: out/coverage.txt 64 | 65 | # Ensures all matrix jobs complete before passing the build 66 | complete: 67 | name: complete 68 | if: ${{ always() }} 69 | needs: [lint, test] 70 | runs-on: ubuntu-latest 71 | steps: 72 | - name: Check that all steps completed 73 | run: | 74 | [ "${{ needs.lint.result }}" != "success" ] && echo "Linting failed." && exit 1; 75 | [ "${{ needs.test.result }}" != "success" ] && echo "Tests failed." && exit 1; 76 | 77 | echo "All steps succeeded!"; 78 | exit 0; 79 | -------------------------------------------------------------------------------- /worker/buffered_pool.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | import "sync" 4 | 5 | // BufferedPool is an interface which implements BufferedPoolImpl, allowing you to mock out the pool for tests. 6 | type BufferedPool interface { 7 | // Do enqueues a task and blocks the goroutine until it's enqueued. 8 | Do(func()) 9 | 10 | // TryDo will attempt to enqueue the task, if the buffer is full, it will return false 11 | TryDo(func()) bool 12 | 13 | // Wait blocks the goroutine until all tasks are complete. 14 | Wait() 15 | 16 | // WaitAndClose closes the worker pool once all tasks are complete, you must not try to enqueue more tasks after calling this. 17 | WaitAndClose() 18 | } 19 | 20 | // compile time assertion 21 | var _ BufferedPool = (*BufferedPoolImpl)(nil) 22 | 23 | // BufferedPoolImpl is an implementation that is compatible with Pool 24 | type BufferedPoolImpl struct { 25 | closeOnce sync.Once 26 | 27 | // Tasks is a buffer of all enqueued tasks to be ran by the workers 28 | Tasks chan func() 29 | 30 | // WaitGroup tracks how many running/queued tasks there are, we expose Wait() so you can wait until all tasks are complete. 31 | WaitGroup sync.WaitGroup 32 | } 33 | 34 | // NewBufferedPool creates a new instance of BufferedPoolImpl 35 | func NewBufferedPool(workers, maxQueue int) *BufferedPoolImpl { 36 | w := &BufferedPoolImpl{ 37 | Tasks: make(chan func(), maxQueue), 38 | } 39 | go w.init(workers) 40 | return w 41 | } 42 | 43 | // Interface is a convenience method which gives you the same instance as an interface. 44 | func (w *BufferedPoolImpl) Interface() BufferedPool { 45 | return w 46 | } 47 | 48 | // Do enqueues a task and blocks the goroutine until it's enqueued. 49 | func (w *BufferedPoolImpl) Do(cb func()) { 50 | w.WaitGroup.Add(1) 51 | w.Tasks <- cb 52 | } 53 | 54 | func (w *BufferedPoolImpl) TryDo(cb func()) bool { 55 | // to prevent a race condition, we optimistically add to the wait group first 56 | w.WaitGroup.Add(1) 57 | 58 | select { 59 | case w.Tasks <- cb: 60 | return true 61 | 62 | default: 63 | // close it out early 64 | w.WaitGroup.Done() 65 | return false 66 | } 67 | } 68 | 69 | // init creates the pool of goroutines 70 | func (w *BufferedPoolImpl) init(workers int) { 71 | for i := 0; i < workers; i++ { 72 | go func() { 73 | for task := range w.Tasks { 74 | task() 75 | 76 | // task is complete, decrement wait group 77 | w.WaitGroup.Done() 78 | } 79 | }() 80 | } 81 | } 82 | 83 | // Wait blocks the goroutine until all tasks are complete. 84 | func (w *BufferedPoolImpl) Wait() { 85 | w.WaitGroup.Wait() 86 | } 87 | 88 | // Close closes the worker pool, you must not try to enqueue more tasks after calling this. 89 | func (w *BufferedPoolImpl) Close() { 90 | w.closeOnce.Do(func() { 91 | close(w.Tasks) 92 | }) 93 | } 94 | 95 | // WaitAndClose closes the worker pool once all tasks are complete, you must not try to enqueue more tasks after calling this. 96 | func (w *BufferedPoolImpl) WaitAndClose() { 97 | w.Wait() 98 | w.Close() 99 | } 100 | --------------------------------------------------------------------------------