peake100/pears-go

View on GitHub
README.md

Summary

Maintainability
Test Coverage
<h1 align="center">Pears</h1>
<p align="center">
    <img height=200 align="center" src="https://raw.githubusercontent.com/peake100/pears-go/main/zdocs/source/_static/logo.svg"/>
</p>
<p align="center">Harvest Go Errors with Ease</p>
<p align="center">
    <a href="https://dev.azure.com/peake100/Peake100/_build?definitionId=10"><img src="https://dev.azure.com/peake100/Peake100/_apis/build/status/pears-go?repoName=peake100%2Fpears-go&branchName=dev" alt="click to see build pipeline"></a>
    <a href="https://dev.azure.com/peake100/Peake100/_build?definitionId=10"><img src="https://img.shields.io/azure-devops/tests/peake100/Peake100/10/dev?compact_message" alt="click to see build pipeline"></a>
    <a href="https://dev.azure.com/peake100/Peake100/_build?definitionId=10"><img src="https://img.shields.io/azure-devops/coverage/peake100/Peake100/10/dev?compact_message" alt="click to see build pipeline"></a>
</p>
<p align="center">
    <a href="https://goreportcard.com/report/github.com/illuscio-dev/islelib-go"><img src="https://goreportcard.com/badge/github.com/illuscio-dev/islelib-go" alt="click to see report card"></a>
    <a href="https://codeclimate.com/github/peake100/pears-go/maintainability"><img src="https://api.codeclimate.com/v1/badges/eb73ef0f82b6bd72d7a2/maintainability" /></a>
</p>
<p align="center">
    <a href="https://github.com/peake100/pears-go"><img src="https://img.shields.io/github/go-mod/go-version/peake100/pears-go" alt="Repo"></a>
    <a href="https://pkg.go.dev/github.com/peake100/pears-go?readme=expanded#section-documentation"><img src="https://pkg.go.dev/badge/github.com/peake100/pears-go?readme=expanded#section-documentation.svg" alt="Go Reference"></a>
</p>

Introduction
------------

Pears helps reduce the boilerplate and ensure correctness for common error-handling 
scenarios:

- Panic recovery

- Abort and error collection from concurrent workers.

Demo
----

**Catch a Panic**

```go
package main

import (
    "errors"
    "fmt"
    "github.com/peake100/pears-go/pkg/pears"
    "io"
)

func main() {
    // We can use CatchPanic to catch ay panics that occur in an operation:
    err := pears.CatchPanic(func() (innerErr error) {
        // We are going to throw an io.EOF.
        panic(io.EOF)
    })

    // Our error will report that it is from a recovered panic.
    fmt.Println("Error:", err)

    // We can test whether this error is a the result of a panic by using errors.As.
    panicErr := pears.PanicError{}
    if errors.As(err, &panicErr) {
        fmt.Println("error is recovered panic")
        // do something if this was a panic
    }

    // PanicError implements xerrors.Wrapper, so we can use errors.Is and errors.As
    // to get at any inner errors.
    if errors.Is(err, io.EOF) {
        fmt.Println("error is io.EOF")
    }

    // Output:
    // Error: panic recovered: EOF
    // error is recovered panic
    // error is io.EOF
}
```

**Gather Errors From Multiple Workers**

pears offers a ``Group`` type which takes some inspirations from 
[https://pkg.go.dev/golang.org/x/sync/errgroup](errgroup.Group), with some key 
differences:

- All errors are collected, not just the first. Each is wrapped in an OpError and 
  then collected into a GroupErrors. These types offer a number of ways to inspect
  and resolve errors in concurrent situations.

- Launched operations can be named using GoNamed for more robust error inspection and
  handling.

- A context is required, and is passed to all child functions, allowing for higher
  readability of where a context comes from.

- Group must be created with a constructor function: NewGroup.

```go
package main

import (
    "context"
    "errors"
    "fmt"
    "github.com/peake100/pears-go/pkg/pears"
    "io"
    "time"
)

func main() {
    group := pears.NewGroup(
        context.Background(), // this context will be used as the parent to al
        // operation contexts
    )

    for i := 0; i < 10; i++ {
        // Each routine will be identified as 'worker [workerNum]'. We do not need to
        // use the 'go' keyword here. op will be launched as a routine, but some
        // internal bookkeeping needs to occur before the op can be launched.
        workerNum := i
        group.GoNamed(fmt.Sprint("worker", workerNum), func(ctx context.Context) error {
            // We'll use a timer to stand in for some long-running worker.
            timer := time.NewTimer(5 * time.Second)
            select {
            case <-ctx.Done():
                fmt.Printf("operation %v received abort request\n", workerNum)
            return ctx.Err()
            case <-timer.C:
                fmt.Printf("operation %v completed successfully\n", workerNum)
            return nil
            }
        })
    }

    // Lastly we'll launch a routine that returns an error, which will cancel the
    // contexts of every op launched above.
    group.GoNamed("faulty operation", func(ctx context.Context) error {
        // This faulty operation will return an io.EOF
        return io.EOF
    })

    // Now we join the group, which blocks until all routines launched above return.
    // If any operations returned an error, we will get one here.
    err := group.Wait()

    // report our error.
    fmt.Println("\nERROR:", err)

    // errors.Is() and errors.As() can inspect what caused our operations to fail.
    // Because pears.BatchMatchFirst is our error-matching mode, only the FIRST
    // encountered error will pass errors.Is() or errors.As().
    //
    // For us that should be io.EOF.
    if errors.Is(err, io.EOF) {
        fmt.Println("error is io.EOF")
    }

    // Even though the other operations returned context.Canceled, we will NOT
    // pass the following check since it was not the FIRST error returned. This is nice
    // for checking against an error that started a cascade.
    //
    // If our match mode had been set to pears.BatchMatchAny, this check would also
    // pass
    if errors.Is(err, context.Canceled) {
        fmt.Println("error is context.Cancelled")
    }

    // We can extract a pears.OpError to get more information about the first error.
    opErr := pears.OpError{}
    if !errors.As(err, &opErr) {
    panic("expected opErr")
  }

    fmt.Println("batch failure caused by operation:", opErr.OpName)

    // We can also extract a GroupErrors to inspect all of our errors more closely:
    groupErrs := pears.GroupErrors{}
    if !errors.As(err, &groupErrs) {
    panic("expected BatchErrors")
  }

    // Let's inspect ALL of the errors we got back. We'll see that the context
    // cancellation errors were returned, but because of our Batch error matching mode,
    // are being kept from surfacing through errors.Is() and errors.As().
    fmt.Println("\nALL ERRORS:")
    for _, thisErr := range groupErrs.Errs {
        fmt.Println(thisErr)
    }
  
    // Unordered Output:
    //
    // operation 9 received abort request
    // operation 8 received abort request
    // operation 1 received abort request
    // operation 3 received abort request
    // operation 6 received abort request
    // operation 4 received abort request
    // operation 0 received abort request
    // operation 7 received abort request
    // operation 5 received abort request
    // operation 2 received abort request
    //
    // ERROR: 11 errors returned. first: error during 'faulty operation': EOF
    // error is io.EOF
    // batch failure caused by operation: faulty operation
    //
    // ALL ERRORS:
    // error during 'faulty operation': EOF
    // error during 'worker1': context canceled
    // error during 'worker5': context canceled
    // error during 'worker7': context canceled
    // error during 'worker0': context canceled
    // error during 'worker4': context canceled
    // error during 'worker6': context canceled
    // error during 'worker8': context canceled
    // error during 'worker3': context canceled
    // error during 'worker2': context canceled
    // error during 'worker9': context canceled
}
```

Goals
-----

- Expose simple APIs for dealing with common error-handling situations.

- Support error inspection through errors.Is and errors.As,

Non-Goals
---------

- Creating complex error frameworks. Pears does not want to re-invent the wheel and 
  seeks only to reduce the boilerplate of leveraging Go's built-in error system.
  
- Solving niche problems. This package seeks to help only the most-broad error cases.
  Features like HTTP or gRPC error-code and serialization systems are beyond the scope 
  of this package.

## Getting Started
For API documentation:
[read the docs](https://pkg.go.dev/github.com/peake100/pears-go?readme=expanded#section-documentation).

For library development guide, 
[read the docs](https://illuscio-dev.github.io/islelib-go/).

### Prerequisites

Golang 1.6+

## Authors

* **Billy Peake** - *Initial work*

## Attributions

<div>Logo made by <a href="https://www.freepik.com" title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a></div>