|
- // Package warnings implements error handling with non-fatal errors (warnings).
- //
- // A recurring pattern in Go programming is the following:
- //
- // func myfunc(params) error {
- // if err := doSomething(...); err != nil {
- // return err
- // }
- // if err := doSomethingElse(...); err != nil {
- // return err
- // }
- // if ok := doAnotherThing(...); !ok {
- // return errors.New("my error")
- // }
- // ...
- // return nil
- // }
- //
- // This pattern allows interrupting the flow on any received error. But what if
- // there are errors that should be noted but still not fatal, for which the flow
- // should not be interrupted? Implementing such logic at each if statement would
- // make the code complex and the flow much harder to follow.
- //
- // Package warnings provides the Collector type and a clean and simple pattern
- // for achieving such logic. The Collector takes care of deciding when to break
- // the flow and when to continue, collecting any non-fatal errors (warnings)
- // along the way. The only requirement is that fatal and non-fatal errors can be
- // distinguished programmatically; that is a function such as
- //
- // IsFatal(error) bool
- //
- // must be implemented. The following is an example of what the above snippet
- // could look like using the warnings package:
- //
- // import "gopkg.in/warnings.v0"
- //
- // func isFatal(err error) bool {
- // _, ok := err.(WarningType)
- // return !ok
- // }
- //
- // func myfunc(params) error {
- // c := warnings.NewCollector(isFatal)
- // c.FatalWithWarnings = true
- // if err := c.Collect(doSomething()); err != nil {
- // return err
- // }
- // if err := c.Collect(doSomethingElse(...)); err != nil {
- // return err
- // }
- // if ok := doAnotherThing(...); !ok {
- // if err := c.Collect(errors.New("my error")); err != nil {
- // return err
- // }
- // }
- // ...
- // return c.Done()
- // }
- //
- // For an example of a non-trivial code base using this library, see
- // gopkg.in/gcfg.v1
- //
- // Rules for using warnings
- //
- // - ensure that warnings are programmatically distinguishable from fatal
- // errors (i.e. implement an isFatal function and any necessary error types)
- // - ensure that there is a single Collector instance for a call of each
- // exported function
- // - ensure that all errors (fatal or warning) are fed through Collect
- // - ensure that every time an error is returned, it is one returned by a
- // Collector (from Collect or Done)
- // - ensure that Collect is never called after Done
- //
- // TODO
- //
- // - optionally limit the number of warnings (e.g. stop after 20 warnings) (?)
- // - consider interaction with contexts
- // - go vet-style invocations verifier
- // - semi-automatic code converter
- //
- package warnings // import "gopkg.in/warnings.v0"
-
- import (
- "bytes"
- "fmt"
- )
-
- // List holds a collection of warnings and optionally one fatal error.
- type List struct {
- Warnings []error
- Fatal error
- }
-
- // Error implements the error interface.
- func (l List) Error() string {
- b := bytes.NewBuffer(nil)
- if l.Fatal != nil {
- fmt.Fprintln(b, "fatal:")
- fmt.Fprintln(b, l.Fatal)
- }
- switch len(l.Warnings) {
- case 0:
- // nop
- case 1:
- fmt.Fprintln(b, "warning:")
- default:
- fmt.Fprintln(b, "warnings:")
- }
- for _, err := range l.Warnings {
- fmt.Fprintln(b, err)
- }
- return b.String()
- }
-
- // A Collector collects errors up to the first fatal error.
- type Collector struct {
- // IsFatal distinguishes between warnings and fatal errors.
- IsFatal func(error) bool
- // FatalWithWarnings set to true means that a fatal error is returned as
- // a List together with all warnings so far. The default behavior is to
- // only return the fatal error and discard any warnings that have been
- // collected.
- FatalWithWarnings bool
-
- l List
- done bool
- }
-
- // NewCollector returns a new Collector; it uses isFatal to distinguish between
- // warnings and fatal errors.
- func NewCollector(isFatal func(error) bool) *Collector {
- return &Collector{IsFatal: isFatal}
- }
-
- // Collect collects a single error (warning or fatal). It returns nil if
- // collection can continue (only warnings so far), or otherwise the errors
- // collected. Collect mustn't be called after the first fatal error or after
- // Done has been called.
- func (c *Collector) Collect(err error) error {
- if c.done {
- panic("warnings.Collector already done")
- }
- if err == nil {
- return nil
- }
- if c.IsFatal(err) {
- c.done = true
- c.l.Fatal = err
- } else {
- c.l.Warnings = append(c.l.Warnings, err)
- }
- if c.l.Fatal != nil {
- return c.erorr()
- }
- return nil
- }
-
- // Done ends collection and returns the collected error(s).
- func (c *Collector) Done() error {
- c.done = true
- return c.erorr()
- }
-
- func (c *Collector) erorr() error {
- if !c.FatalWithWarnings && c.l.Fatal != nil {
- return c.l.Fatal
- }
- if c.l.Fatal == nil && len(c.l.Warnings) == 0 {
- return nil
- }
- // Note that a single warning is also returned as a List. This is to make it
- // easier to determine fatal-ness of the returned error.
- return c.l
- }
-
- // FatalOnly returns the fatal error, if any, **in an error returned by a
- // Collector**. It returns nil if and only if err is nil or err is a List
- // with err.Fatal == nil.
- func FatalOnly(err error) error {
- l, ok := err.(List)
- if !ok {
- return err
- }
- return l.Fatal
- }
-
- // WarningsOnly returns the warnings **in an error returned by a Collector**.
- func WarningsOnly(err error) []error {
- l, ok := err.(List)
- if !ok {
- return nil
- }
- return l.Warnings
- }
|