1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
|
// 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
}
|