// 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 }