check

package module
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Nov 11, 2022 License: Apache-2.0 Imports: 4 Imported by: 0

README

Documentation

Overview

Package check implements an exception-handling system for Go using panic and recover under the hood, with generics enabling a fairly clean API.

Because generics don't offer variadic type parameter packs, package check provides a family of Catch and Must functions for up to four explicitly defined parameter types.

Example

The following example shows a piece of code written in Go's conventional error handling approach on the left diffed with a version using package check on the right.

type Data struct {                            · type Data struct {
  db        *sql.DB                           ·   db        *sql.DB
  selPrices *sql.Stmt                         ·   selPrices *sql.Stmt
}                                             · }
                                              ·
func New(driver, dsn string) (*Data, error) { |  func New(driver, dsn string) (_ *Data, e error) {
  conn, err := sql.Open(driver, dsn)          |    defer check.Handle(&e)
  if err != nil {                             <
    return nil, err                           <
  }                                           <
  return &Data{db: conn}, nil                 |    return &Data{
                                              >      db: check.Must1(sql.Open(driver, dsn)),
                                              >    }, nil
}                                             · }
                                              ·
func (d *Data) GetPrices(sym string) (        · func (d *Data) GetPrices(sym string) (
  open, hi, lo, close float64, _ error,       |   open, hi, lo, close float64, e error,
) {                                           · ) {
                                              >   defer check.Handle(&e)
  if d.selPrices == nil {                     ·   if d.selPrices == nil {
    q, err := d.db.Prepare(                   |     d.selPrices = check.Must1(d.db.Prepare(
      `SELECT o,h,l,c                         ·       `SELECT o,h,l,c
       FROM price                             ·        FROM price
       WHERE sym=?`)                          |        WHERE sym=?`))
    if err != nil {                           <
      return 0, 0, 0, 0, err                  <
    }                                         <
    d.selPrices = q                           <
  }                                           ·   }
  tx, err := d.db.Begin()                     |   tx := check.Must1(d.db.Begin())
  if err != nil {                             <
    return 0, 0, 0, 0, err                    <
  }                                           <
  return getPrices(tx.Stmt(d.selPrices), sym) |   o, h, l, c := getPrices(tx.Stmt(d.selPrices), sym)
                                              >   return o, h, l, c, nil
}                                             · }
                                              ·
func getPrices(stmt *sql.Stmt, sym string) (  · func getPrices(stmt *sql.Stmt, sym string) (
  open, hi, lo, close float64, _ error,       |   open, hi, lo, close float64,
) {                                           · ) {
  q, err := stmt.Query()                      |   q := check.Must1(stmt.Query())
  if err != nil {                             <
    return 0, 0, 0, 0, err                    <
  }                                           <
  colTypes, err := q.ColumnTypes()            |   log.Printf("cols: %#v", check.Must1(q.ColumnTypes()
  if err != nil {                             <
    return 0, 0, 0, 0, err                    <
  }                                           <
  log.Printf("cols: %#v", colTypes)           <
  if q.Next() {                               ·   if q.Next() {
    err := q.Scan(&open, &hi, &lo, &close)    |     check.Must(q.Scan(&open, &hi, &lo, &close))
    if err != nil {                           <
      return 0, 0, 0, 0, err                  <
    }                                         <
    if q.Next() {                             ·     if q.Next() {
      return 0, 0, 0, 0, fmt.Errorf("> 1 resu…|       check.Failf("> 1 result: %q", sym)
    }                                         ·     }
  } else {                                    ·   } else {
    return 0, 0, 0, 0, fmt.Errorf("no result:…|     check.Failf("no result: %q", sym)
  }                                           ·   }
  return                                      ·   return
}                                             · }

The most obvious difference between the two examples is the length of the code. The original weighs in at 39 significant lines of code, while the second is just 29, a reduction of 25%.

A less obvious, but more significant distinction is a reduction from eight internal variables (ignoring input and return parameters) down to just two. This represents a sharp drop in the amount of internal state and a matching reduction in the amount of mental bookkeeping required to comprehend the flow of logic. As an example, the colTypes variable is used four lines after it is defined in the original code, and the experienced reader is predisposed to keep a mental note of it after that, just in case it crops up later in the code. They might even wonder, "Why is it here? Is it just for logging or does the function have some other use for it?" While the reader might be barely (or not even) aware of these thoughts, they will nonetheless clutter the mind as the logic increases in scope and complexity. In the rewritten example, the colTypes variable doesn't exist at all. The expression is used directly, which doesn't trigger any of the above questions, and the reader, instinctively knowing that it won't be referred to again, can simply discard that sliver of information. In fact, they will likely skim past the log.Printf call without even being consciously aware of it.

Several other points are worth noting:

  1. Not every function must trap errors. Note that the unpublished getPrices function uses check.Must/MustN, but doesn't use check.Handle or check.Catch/CatchN. This is perfectly acceptable usage within a package, since the published methods will trap errors before they escape.

  2. MustN and CatchN only go up to 4 parameters. To deal with functions that return more than four return values plus an error, assign their output to local variables the conventional way then call check.Must(err). In practice, one should generally not create functions with more than four return values plus an error. They are usually better redesigned to return a struct.

  3. All instances of returning "don't care" zero values have disappeared in the new code. This is another important way in which package check reduces cognitive load, both on the author and the reader.

Performance considerations

From the profile below, it is clear that error handling using package check is much slower than conventional error handling.

❯ go test -run=^$ -bench=. -benchmem -cpuprofile cpu.success.out
goos: darwin
goarch: arm64
pkg: github.com/anzx/acceleration-tools/envelope/cmd/envelope/internal/check
BenchmarkFailureConventional-8          1000000000           0.3117 ns/op          0 B/op          0 allocs/op
BenchmarkFailureCatch-8                  6531588           180.7 ns/op        16 B/op          1 allocs/op
BenchmarkFailureHandle-8                 8418494           140.8 ns/op        16 B/op          1 allocs/op
BenchmarkFailureHandleTransform-8        8462744           143.1 ns/op        16 B/op          1 allocs/op
BenchmarkSuccessConventional-8          1000000000           0.3106 ns/op          0 B/op          0 allocs/op
BenchmarkSuccessCatch-8                 140923567            8.558 ns/op           0 B/op          0 allocs/op
BenchmarkSuccessHandle-8                240914712            5.008 ns/op           0 B/op          0 allocs/op
BenchmarkSuccessHandleTransform-8       200517948            5.921 ns/op           0 B/op          0 allocs/op
PASS
ok      github.com/anzx/acceleration-tools/envelope/cmd/envelope/internal/check 13.524s

Conventional error handling clocks in at just over 0.3 ns regardless of whether the call succeeds or fails.

In contrast, a successful call to check.Handle is almost 20 times slower and almost 30 times slower when calling check.Catch.

Things are much worse during failures. Failed calls to check.Handle and check.Catch are 500 and 600 times slower, respectively, than conventional error handling.

The clear message from this analysis is to avoid using package check in performance sensitive code. That said, it is worth keeping things in perspective. A 5–8 ns overhead for successful calls is still very fast and would be perfectly acceptable in most contexts. More thought would need to be given to scenarios where errors are common, but even then a failed call still takes a small fraction of the time it takes to perform most forms of I/O.

Index

Constants

This section is empty.

Variables

View Source
var ErrNilError = errors.New("called Fail(nil)")

Functions

func Catch

func Catch(work func(), transforms ...func(e error) error) (e error)

Catch returns err if calling work panics with Error{err}, otherwise it returns nil.

return check.Catch(func() {
	check.Must1(fmt.Println("Hello, World!")
	check.Must1(fmt.Println("¡Hola, Mundo!")
	check.Must1(fmt.Println("你好,世界!")
	check.Must1(fmt.Println("Привет, мир!")
})

func Catch1

func Catch1[T any](work func() T, transforms ...func(e error) error) (t T, e error)

Catch1 returns _, err if calling work panics with Error{err}, otherwise it returns t, nil.

func getTotalWeight(weight, qty string) (float64, error) {
	return Catch1(func() float64 {
		return Must1(strconv.ParseFloat(weight, 64)) *
			float64(Must1(strconv.Atoi(qty)))
	})
}

func Catch2

func Catch2[T1, T2 any](
	work func() (T1, T2),
	transforms ...func(e error) error,
) (t1 T1, t2 T2, e error)

Catch2 returns _, _, err if calling work panics with Error{err}, otherwise it returns t1, t2, nil. See Catch1 for a related example.

func Catch3

func Catch3[T1, T2, T3 any](
	work func() (T1, T2, T3),
	transforms ...func(e error) error,
) (t1 T1, t2 T2, t3 T3, e error)

Catch4 returns _, _, err if calling work panics with Error{err}, otherwise it returns t1, t2, t3, nil. See Catch1 for a related example.

func Catch4

func Catch4[T1, T2, T3, T4 any](
	work func() (T1, T2, T3, T4),
	transforms ...func(e error) error,
) (t1 T1, t2 T2, t3 T3, t4 T4, e error)

Catch4 returns _, _, _, err if calling work panics with Error{err}, otherwise it returns t1, t2, t3, t4 nil. See Catch1 for a related example.

func Fail

func Fail(err error)

Fail panics Error{err} if err is not nil, otherwise it panics with an internal error that isn't treated specially by Handle, Catch[N] and Pass.

func Failf added in v0.5.0

func Failf(format string, args ...any)

Fail panics Error{fmt.Errorf(format, args...)}.

func Handle

func Handle(e *error, transforms ...func(e error) error)

Handle, when deferred, recovers Error{err}. If any transforms are specified, err is transformed via err = transforms[i](err) for each transform in turn. Finally, Handle assigns err to *e unless e is nil, in which case it panics with Error{err}.

func getTotalWeight(weight, qty string) (_ float64, e error) {
	defer Handle(&e, func(e error) error {
		return fmt.Errorf("computing total weight: %w", e)
	})
	return Must1(strconv.ParseFloat(weight, 64)) *
		float64(Must1(strconv.Atoi(qty))), nil
}

func Must

func Must(err error)

Must calls panic(Error{err}) if err is not nil.

func Must1

func Must1[T any](t T, err error) T

Must1 returns t if err is nil, otherwise it calls panic(Error{err}).

price := check.Must1(strconv.ParseFloat(unitPrice, 64)) *
	check.Must1(strconv.ParseFloat(qty, 64))

func Must2

func Must2[T1, T2 any](t1 T1, t2 T2, err error) (T1, T2)

Must2 returns t1, t2 if err is nil, otherwise it calls panic(Error{err}).

// MulDiv's third return value is an error if x = y = 0.
prod, quo := check.Must2(MulDiv(x, y))

func Must3

func Must3[T1, T2, T3 any](t1 T1, t2 T2, t3 T3, err error) (T1, T2, T3)

Must3 returns t1, t2, t3 if err is nil, otherwise it calls panic(Error{err}).

// MulDivRem's fourth return value is an error if x = y = 0.
prod, quo, rem := check.Must3(MulDivRem(x, y))

func Must4

func Must4[T1, T2, T3, T4 any](t1 T1, t2 T2, t3 T3, t4 T4, err error) (T1, T2, T3, T4)

Must4 returns t1, t2, t3, t4 if err is nil, otherwise it calls panic(Error{err}).

// AnalyzeTrades's fifth return value is an error if prices is empty.
open, high, low, close := check.Must4(AnalyzeTrades(prices))

func Pass

func Pass(r any) any

Pass returns r unless it is a check.Error, in which case it re-panics r. Typical usage:

defer func() {
	if r := check.Pass(recover()); r != nil {
		// We only get here if recover() returns something other than
		// check.Error.
	}
}()

func Wrap added in v0.4.0

func Wrap(e *error, skip int, transforms ...func(e error) error)

Wrap behaves like Handle, but additionally wraps any returned error in "github.com/go-errors/errors".Error, which provides access to the stack trace. Use skip to drop uninteresting stack frames.

Types

type Error

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

Error wraps an error. The Must… family of functions use Error to wrap errors in calls to panic, while the Catch… family detect errors wrapped thus.

func (Error) Error

func (e Error) Error() string

Error returns a string representation of e, thus implementing the error interface.

func (Error) Unwrap

func (e Error) Unwrap() error

Unwrap returns the wrapped error as required by the errors packages.

Jump to

Keyboard shortcuts

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