kytsya

package module
v0.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 27, 2023 License: MIT Imports: 5 Imported by: 0

README

🇺🇦kytsya🇺🇦

Logo

Go toolkit!

Kytsya means "kitten" in ukrainian. While cats are the best programmers friends, we decided to call the repo in honour of these beautiful creatures. It's a small, but powerful kit that could be used to change an approach to threading and working with slices.

It contains:

  • Number of controllers for goroutines to use them in a functional, save and readable manner
  • Controllers for run goroutines that should return a result
  • Builders for start a goroutines with a wait group, recovery handler or/and timeout
  • ForEach, ForEachChan, Map, Reduce, Filter functions to handle data in a functional and readable manner

Cases where kytsya will shine:

  • Work hard with goroutines? Kytsya helps to manage that in nice way.
  • Need recovery handlers, but don't want to manually control every case that should be covered with deferred recoveries? Here you could find WithRecover() builder, that makes recovery graceful!
  • Run goroutins that returns results / need to make everything stable and built in one manner? Kytsya will help to do it with ErrorBox or ForEachRunner.
  • Every developer in a big team uses its own threading style? Have a mix of channels, waitgroups, "res" slices? Kytsya gives one approach for everything.
  • Work with data? ForEach, Map, Reduce will definetly what is missing in go's std lib.

Kytsya is not a framework, it's a toolkit that gives an ability, but not forces developers to use the one way. Here is only small part from what it is doing:

  1. Need to run a set of goroutines with recovery handlers and a wait group?
    kytsya.NewBox().
   	 WithRecover().
   	 WithWaitGroup().
   	 AddTask(func() { fmt.Println("🐈") }).
   	 AddTask(func() { fmt.Println("🐈") }).
   	 AddTask(func() { fmt.Println("🐈") }).
   	 Run().Wait()
  1. Need a safe way to run a set of goroutines and properly read some results?
    resCh := NewErrorBox[string]().
   	 WithRecover().
   	 AddTask(func() Result[string] {
   		 return Result[string]{Data: "🐈"}
   	 }).
   	 AddTask(func() Result[string] {
   		 panic("dog detected")
   		 return Result[string]{Data: "🐕"}
   	 }).Run()

Read the output:

    ForChan(resCh, func(val Result[string]) {
   	 fmt.Println(val)
    })

That will print:

{🐈 <nil>}
{ kytsunya: recovered from panic: dog detected from goroutine 22 [running]:
runtime/debug.Stack()
    /opt/homebrew/Cellar/go/1.20.3/libexec/src/runtime/debug/stack.go:24 +0x64
...
}

While kytsya fetching panic, panic message and a stack trace returns as a normal error!

  1. Need to handle every list member in a separate goroutine?
    data := []int{1, 2, 3}
    resCh := NewEachRunner[int, string](data).
   	 Handle(func(val int) Result[string] {
   		 return Result[string]{Data: fmt.Sprint(val)}
   	 })

    ForChan(resCh, func(val Result[string]) {
   	 fmt.Println(val.Data, val.Err)
    })

Every member of the "data" slice will be handled in a separate goroutine ("handler") and results will be returned in the resCh channel that will be closed after all tasks are done.

  1. Need Map, Reduce, ForEach and Filter functions?
    // Range it!
    ForEach([]int{1, 2, 3, 4, 5, 6}, func(i, val int) {
   	 fmt.Printf("index: %d value: %d", i, val)
    })

    // Filter it!
    // output: [2 4 6]
    fmt.Println(Filter([]int{1, 2, 3, 4, 5, 6}, func(i, val int) bool {
   	 return val%2 == 0
    }))

    // Map it!
    resMap := Map([]int{1, 2, 3, 4, 5, 6}, func(i, val int) string {
   	 return strconv.Itoa(val)
    })

    // output: [1 2 3 4 5 6] as an array of string
    fmt.Println(resMap)

    // Reduce it!
    // output: 21
    fmt.Println(Reduce([]int{1, 2, 3, 4, 5, 6}, func(val, acc int) int {
   	 return val + acc
    }))

Here it is!

See example_test.go for more examples!
We appreciate feedbacks and found issues! Feel free to become a contributor and add here things you miss in go as we do!

Why should we use kytsya?

  • It is giving a way to make an application that work hard with a data to handle everything in more functional way
  • One toolkit for all: big projects struggling from different threading styles of each team member. With kytsya it is not a problem anymore!
  • It is reliable and well-tested
  • Kytsya gives an ability to handle things graceful, but not force - it is not necessary to use whole toolkit
  • The way to do a beautiful things quick and graceful
  • Most of its controller implements laziness - no work until result is requested (but not them all :))

Benchmarking

Benchmarks included in the repo. Here is the results for the ErrTaskRunner (handler for a group of goroutines that returns a results):

goos: darwin
goarch: arm64
pkg: github.com/bkatrenko/kytsya
BenchmarkErrorBox/pure_Go-10         	  524178	      2210 ns/op	     360 B/op	       8 allocs/op
BenchmarkErrorBox/kytsunya-10        	  443697	      2333 ns/op	     512 B/op	      12 allocs/op
PASS
ok  	github.com/bkatrenko/kytsya	3.584s

also:

  • No external dependencies in the kit, pure std golang :)
  • 100% test coverage

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrRecoveredFromPanic = errors.New("kytsunya: recovered from panic")
	ErrTimeout            = errors.New("kytsunya: goroutine timed out")
)
View Source
var (
	ErrWaitWithoutWaitGroup = errors.New("Wait() was called without WithWaitGroup initialization, please, call WithWaitGroup() before")
)

Functions

func Filter

func Filter[T any](input []T, f func(i int, val T) bool) []T

Filter function accept a slice of T(any), and a filter handler that returns a bool value (include?). In case of true, value will be included in the output slice, otherwise will be filtered out.

func ForChan

func ForChan[T any](ch chan T, f func(val T))

ForChan will range through the channel until its closed

func ForEach

func ForEach[T any](data []T, f func(i int, val T))

ForEach do a range through the slice of data, and will execute input handler function on every member of the input slice. For example:

ForEach([]int{1, 2, 3, 4, 5, 6}, func(i, val int) {
	fmt.Printf("index: %d value: %d", i, val)
})

Will print index and value of every member of input slice.

func ForEachErr

func ForEachErr[T any](data []T, f func(i int, val T) error) error
	fmt.Printf("index: %d value: %d", i, val)
	return nil
})

Will print index and value of every member of input slice. In case of error, iteration will be interrupted.

func ForErrChan

func ForErrChan[T any](ch chan T, f func(val T) error) error

ForErrChan will range through the channel entries and handler will receive every value. In case of error returned from the handler, loop will be stopped.

func Map

func Map[T any, V any](data []T, f func(i int, val T) V) []V

output: [1 2 3 4 5 6] as an array of string In this example Map modified an array of integers to an array of string.

func MapErr

func MapErr[T any, V any](data []T, f func(i int, val T) (V, error)) ([]V, error)

MapErr works similar to map with out exception: handler function could return an error. In such case, iteration will be stopped, and function will return empty result with an error.

func Reduce

func Reduce[T any, V any](input []T, f func(val T, acc V) V) V

Reduce receive a slice of values, and return a single value as a result. For example:

fmt.Println(Reduce([]int{1, 2, 3, 4, 5, 6}, func(val, acc int) int {
	return val + acc
}))

Will print a sum of all integers in the slice.

Types

type EachRunner

type EachRunner[T any, V any] struct {
	// contains filtered or unexported fields
}

EachRunner could be used in case of needing to handle every list member in different goroutine.

func NewEachRunner

func NewEachRunner[T any, V any](data []T) *EachRunner[T, V]

NewEachRunner returns an instance of each runner - entity that handle a slice of data with handler, where each member handled in a different goroutine. T represents input data type. V represents output data type.

func (*EachRunner[T, V]) Handle

func (er *EachRunner[T, V]) Handle(handler func(val T) Result[V]) chan Result[V]

Handle(f) accepts a functional handler for every member on the input list. Handler will be spawned in a separate goroutine and will receive on of input list entry.

func (*EachRunner[T, V]) WithRecover

func (er *EachRunner[T, V]) WithRecover() *EachRunner[T, V]

WithRecover adds recovery handler to each spawned goroutine.

type ErrRunner

type ErrRunner[T any] struct {
	// contains filtered or unexported fields
}
	fmt.Println(<-res)
 In this case goroutine also using WithTimeout functionality.

func Erroutine

func Erroutine[T any]() *ErrRunner[T]

Erroutine represents a goroutine that returns a result of execution as a value (in case of Wait() was called) or channel with one value in case of WaitAsync() was called. T(any) param represents an output result type.

func (*ErrRunner[T]) Spawn

func (r *ErrRunner[T]) Spawn(f func() Result[T]) *ErrRunner[T]

Spawn accept handler/worker function as an argument and start the execution immediately.

func (*ErrRunner[T]) Wait

func (r *ErrRunner[T]) Wait() Result[T]

Wait waits until spawned goroutine return a result of the execution or timed out.

func (*ErrRunner[T]) WaitAsync

func (r *ErrRunner[T]) WaitAsync() chan Result[T]

WaitAsync returns channel that sends a value as a result of the execution, or timeout error.

func (*ErrRunner[T]) WithRecover

func (r *ErrRunner[T]) WithRecover() *ErrRunner[T]

WithRecover add a recovery handler to the Erroutine.

func (*ErrRunner[T]) WithTimeout

func (r *ErrRunner[T]) WithTimeout(timeout time.Duration) *ErrRunner[T]

WithTimeout adds a timeout of execution to the erroutine. In case of timeout, error message will be moved to Result{Err: "error here"}. Error type is: ErrTimeout = errors.New("kytsya: goroutine timed out")

type ErrTaskRunner

type ErrTaskRunner[T any] struct {
	// contains filtered or unexported fields
}

ErrTaskRunner is a structure for run a group of async tasks that should return a result or error, as run & collect result from some number of network calls. For example:

resCh := NewErrorBox[string]().
WithRecover().
AddTask(func() Result[string] {
	return Result[string]{Data: "1"}
}).
AddTask(func() Result[string] {
	return Result[string]{Data: "2"}
}).
AddTask(func() Result[string] {
	return Result[string]{Err: errors.New("Houston, we have a problem")}
}).
AddTask(func() Result[string] {
	panic("aaaaa")
}).
Run()
// ResCh will be closed after all tasks are done!
for v := range resCh {
	fmt.Println(v)
}

In this case, WithRecover() returns error to a handler-channel as a Result{Err: val} from function that called a panic.

func NewErrorBox

func NewErrorBox[T any]() *ErrTaskRunner[T]

NewErrorBox is a constructor for task runner. We call it *Box cause 🐈🐈🐈 are in love with boxes!

func (*ErrTaskRunner[T]) AddTask

func (tr *ErrTaskRunner[T]) AddTask(f func() Result[T]) *ErrTaskRunner[T]

AddTask accept a generic function that will contain result or error structure.

func (*ErrTaskRunner[T]) Run

func (tr *ErrTaskRunner[T]) Run() chan Result[T]

Run spawns all tasks and return a chan Result[T] to collect all.

func (*ErrTaskRunner[T]) WithRecover

func (tr *ErrTaskRunner[T]) WithRecover() *ErrTaskRunner[T]

WithRecover adds a recovery handler for every task. Panic message and a stacktrace will be returned as Result{Err: "error message/panic trace"}

type Result

type Result[T any] struct {
	Data T
	Err  error
}

Result[T] define kind of "OneOf": Generic result of the execution or error message. Error message will be also returned in case of panic or timeout.

type Runner

type Runner struct {
	// contains filtered or unexported fields
}
defer func(){
	if err := recover(); err != nil {
		fmt.Println(err)
	}
}()

With kytsya it's possible to generalize such operation in one defined manner: kytsya.Goroutine().WithRecover().Spawn(func() {fmt.Println("🐈🐈🐈")}).Wait() Wait group also built-in.

func Goroutine

func Goroutine() *Runner

Goroutine creates a new goroutine runner. Example: kytsunya2.Goroutine().WithRecover().WithWaitGroup().Spawn(func() {fmt.Println("🐈🐈🐈")}).Wait()

func (*Runner) Spawn

func (r *Runner) Spawn(f func()) Waiter

Spawn start a new goroutine, accept function that will be executed in the newly created routine.

func (*Runner) WithRecover

func (r *Runner) WithRecover() *Runner

WithRecover will add defer recover() function to the executor that will recover panics and will print the stack trace into stdout.

func (*Runner) WithWaitGroup

func (r *Runner) WithWaitGroup() *Runner

WithWaitGroup add a wait group into the executor (Wait() could be called, and will block until created goroutine will return).

type RunnerFunc

type RunnerFunc interface {
	Run() Waiter
}

type TaskFiller

type TaskFiller interface {
	AddTask(f func()) TaskFiller
	Run() Waiter
	AfterAll(f func()) RunnerFunc
}

type TaskRunner

type TaskRunner struct {
	// contains filtered or unexported fields
}

TaskRunner represents an executor for a group of goroutines. Could be useful in case of needing to run a controlled group of goroutines with one handler point, panic-handlers of wait group. Constructor function is NewBox 'cause boxes is best friends of cats! For example:

NewBox().WithWaitGroup(). AddTask(func() {}). AddTask(func() {}). AddTask(func() {}). AfterAll(func() {}). Run().Wait()

WithWaitGroup().WithRecover() and AfterAll(f) is not necessary calls, added here just to show the possibilities.

func NewBox

func NewBox() *TaskRunner

NewBox creates new runner that controls a set of running goroutines that returns no values. It has functionality to: 1. Recover all panics with WithRecover() 2. Add Wait group to run with WithWaitGroup() 3. Add new task to execution with AddTask(f()) 4. Run the set with Run() 5. Wait till all goroutines are done with Wait() 6. Add a function that will be executed after all goroutines will done with AfterAll(f())

func (*TaskRunner) AddTask

func (tr *TaskRunner) AddTask(f func()) TaskFiller

AddTask accept a new task for async execution.

func (*TaskRunner) AfterAll

func (tr *TaskRunner) AfterAll(f func()) RunnerFunc

AfterAll accept a handler that will be executed after all tasks. In general case it could be used for a range of tasks: - Close any kinds of connections - Logs the results of measure the time of executions - close channels - etc. up to user.

WARNING: The nature of AfterAll is async. It means, that it will wait unit WaitGroup unblock and will be executed asynchronously. If it is necessary to wait until AfterFunc will exit, use any possible sync mechanism.

func (*TaskRunner) Run

func (tr *TaskRunner) Run() Waiter

Run spawns all tasks in a loop.

func (*TaskRunner) Wait

func (tr *TaskRunner) Wait()

Wait blocks until all tasks will done, call a panic in case of "WithWaitGroup()" was no called.

func (*TaskRunner) WithRecover

func (tr *TaskRunner) WithRecover() *TaskRunner

WithRecover add a recovery handler to a task funner. Handler will be assigned to every goroutine and in case of panic will recover and print a stacktrace into stdout.

func (*TaskRunner) WithWaitGroup

func (tr *TaskRunner) WithWaitGroup() *TaskRunner

WithWaitGroup add a WaitGroup to an executions and makes possible to call Wait() to wait until all tasks will done.

type Waiter

type Waiter interface {
	Wait()
}

Waiter/RunnerFunc/TaskFiller is a group of interfaces that prevents misuse of TaskRunner.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL