├── go.mod ├── cmd └── example │ └── main.go ├── .gitignore ├── LICENSE ├── README.md ├── must.go └── must_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mcesar/must 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /cmd/example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/mcesar/must" 8 | ) 9 | 10 | func main() { 11 | fmt.Println(f()) 12 | } 13 | 14 | func f() (err error) { 15 | defer must.Handle(&err) 16 | f := must.Do(os.Open("file")) 17 | defer f.Close() 18 | // ... 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Marcos César de Oliveira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # must 2 | Generic error handling with panic, recover, and defer. Requires Go 1.18 or later. 3 | 4 | Usage: 5 | ```go 6 | // main.go 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | 13 | "github.com/mcesar/must" 14 | ) 15 | 16 | func main() { 17 | fmt.Println(f()) 18 | } 19 | 20 | func f() (err error) { 21 | defer must.Handle(&err) 22 | f := must.Do(os.Open("file")) 23 | defer f.Close() 24 | // ... 25 | return nil 26 | } 27 | 28 | ``` 29 | To run: 30 | ```sh 31 | $ go run main.go 32 | ``` 33 | 34 | Benchmarks: 35 | ```sh 36 | $ go test -bench=. 37 | goos: darwin 38 | goarch: arm64 39 | pkg: github.com/mcesar/must 40 | BenchmarkMustErrorHandlingWithoutDelay-8 9594230 114.1 ns/op 41 | BenchmarkRegularErrorHandlingWithoutDelay-8 73931268 15.71 ns/op 42 | BenchmarkMustErrorHandlingWith10msDelay-8 100 11777532 ns/op 43 | BenchmarkRegularErrorHandlingWith10msDelay-8 100 11590843 ns/op 44 | ``` 45 | 46 | ## Documentation 47 | 48 | **func Do** 49 | ```go 50 | func Do[A any](a A, err error) A 51 | ``` 52 | Do returns a or panics if err != nil 53 | 54 | **func Do0** 55 | ```go 56 | func Do0(err error) 57 | ``` 58 | Do panics if err != nil 59 | 60 | **func Do2** 61 | ```go 62 | func Do2[A, B any](a A, b B, err error) 63 | ``` 64 | Do2 returns a and b or panics if err != nil 65 | 66 | **func Handle** 67 | ```go 68 | func Handle(err *error) 69 | ``` 70 | Handle sets err to recovered value if it is an error 71 | 72 | **func Handlef** 73 | ```go 74 | func Handlef(err *error, str string) 75 | ``` 76 | Handlef sets err to recovered value if it is an error, 77 | wrapped according to the formatting string specified 78 | -------------------------------------------------------------------------------- /must.go: -------------------------------------------------------------------------------- 1 | // Package must provides generic error handling with panic, recover, and defer. 2 | // Usage: 3 | // import "github.com/mcesar/must" 4 | // func f() (err error) { 5 | // must.Handle(&err) 6 | // f := must.Do(os.Open("file")) 7 | // defer f.close() 8 | // // ... 9 | // } 10 | package must 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | ) 16 | 17 | type wrappedError struct{ error } 18 | 19 | func (w wrappedError) Unwrap() error { return w.error } 20 | 21 | // Do returns a or panics if err != nil 22 | func Do[A any](a A, err error) A { 23 | if err != nil { 24 | panic(wrappedError{err}) 25 | } 26 | return a 27 | } 28 | 29 | // Do0 panics if err != nil 30 | func Do0(err error) { 31 | if err != nil { 32 | panic(wrappedError{err}) 33 | } 34 | } 35 | 36 | // Do2 returns a and b or panics if err != nil 37 | func Do2[A, B any](a A, b B, err error) (A, B) { 38 | if err != nil { 39 | panic(wrappedError{err}) 40 | } 41 | return a, b 42 | } 43 | 44 | // Handle sets dest to recovered value if it is an error emitted inside of a Do call 45 | func Handle(dest *error) { 46 | e := recover() 47 | handle(dest, e) 48 | } 49 | 50 | func handle(dest *error, e interface{}) { 51 | if e == nil { 52 | return 53 | } 54 | var errTyped wrappedError 55 | if eError, ok := e.(error); ok && errors.As(eError, &errTyped) { 56 | if dest != nil { 57 | *dest = errTyped.error 58 | } 59 | return 60 | } 61 | panic(e) 62 | } 63 | 64 | // Handlef sets err to recovered value if it is an error, wrapped according to 65 | // the formatting string specified 66 | func Handlef(dest *error, str string) { 67 | e := recover() 68 | handle(dest, e) 69 | if dest != nil && *dest != nil { 70 | *dest = fmt.Errorf(str, *dest) 71 | } 72 | } 73 | 74 | // HandleFunc recovers error and passes it to the handler function 75 | func HandleFunc(f func(err error)) { 76 | var err error 77 | e := recover() 78 | handle(&err, e) 79 | f(err) 80 | } 81 | -------------------------------------------------------------------------------- /must_test.go: -------------------------------------------------------------------------------- 1 | package must 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "runtime" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | type testErrorEmitFunc func(a, delay int) (x int, err error) 13 | 14 | func getFunctionName(i interface{}) string { 15 | return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() 16 | } 17 | 18 | func TestDo(t *testing.T) { 19 | for _, test := range []struct { 20 | name string 21 | x int 22 | err bool 23 | }{ 24 | { 25 | "no error", 26 | 0, 27 | false, 28 | }, 29 | { 30 | "error", 31 | -1, 32 | true, 33 | }, 34 | { 35 | "error", 36 | 1, 37 | true, 38 | }, 39 | } { 40 | for _, f := range []testErrorEmitFunc{withMustErrorHandling, withMustErrorHandlingV2, withMustErrorHandlingV3} { 41 | t.Run(test.name+fmt.Sprintf("(%s)", getFunctionName(f)), func(t *testing.T) { 42 | x, err := f(test.x, 0) 43 | if !test.err && x != test.x { 44 | t.Errorf("expected %v, got %v", test.x, x) 45 | } 46 | if (err != nil) != test.err { 47 | t.Errorf("expected %v, got %v", test.err, err) 48 | } 49 | t.Logf("%+v", err) 50 | }) 51 | } 52 | } 53 | } 54 | 55 | func BenchmarkMustErrorHandlingWithoutDelay(b *testing.B) { 56 | for n := 0; n < b.N; n++ { 57 | withMustErrorHandling(n, 0) 58 | } 59 | } 60 | 61 | func BenchmarkRegularErrorHandlingWithoutDelay(b *testing.B) { 62 | for n := 0; n < b.N; n++ { 63 | withRegularErrorHandling(n, 0) 64 | } 65 | } 66 | 67 | func BenchmarkMustErrorHandlingWith10msDelay(b *testing.B) { 68 | for n := 0; n < b.N; n++ { 69 | withMustErrorHandling(n, 10) 70 | } 71 | } 72 | 73 | func BenchmarkRegularErrorHandlingWith10msDelay(b *testing.B) { 74 | for n := 0; n < b.N; n++ { 75 | withRegularErrorHandling(n, 10) 76 | } 77 | } 78 | 79 | func withMustErrorHandling(a, delay int) (x int, err error) { 80 | defer Handle(&err) 81 | time.Sleep(time.Duration(delay) * time.Millisecond) 82 | x = Do(nonNegativeOnly(a)) 83 | x = Do(nonPositiveOnly(a)) 84 | return x, nil 85 | } 86 | 87 | func withMustErrorHandlingV2(a, delay int) (x int, rErr error) { 88 | defer Handlef(&rErr, "error: %w") 89 | time.Sleep(time.Duration(delay) * time.Millisecond) 90 | x = Do(nonNegativeOnly(a)) 91 | x = Do(nonPositiveOnly(a)) 92 | return x, nil 93 | } 94 | 95 | func withMustErrorHandlingV3(a, delay int) (x int, rErr error) { 96 | defer HandleFunc(func(err error) { 97 | if err != nil { 98 | rErr = fmt.Errorf("formatted: %+v", err) 99 | } 100 | }) 101 | time.Sleep(time.Duration(delay) * time.Millisecond) 102 | x = Do(nonNegativeOnly(a)) 103 | x = Do(nonPositiveOnly(a)) 104 | return x, nil 105 | } 106 | 107 | func withRegularErrorHandling(a, delay int) (x int, err error) { 108 | time.Sleep(time.Duration(delay) * time.Millisecond) 109 | x, err = nonNegativeOnly(a) 110 | if err != nil { 111 | return 0, err 112 | } 113 | x, err = nonPositiveOnly(a) 114 | if err != nil { 115 | return 0, err 116 | } 117 | return x, nil 118 | } 119 | 120 | func nonNegativeOnly(x int) (int, error) { 121 | if x < 0 { 122 | return 0, errors.New("must be positive") 123 | } 124 | return x, nil 125 | } 126 | 127 | func nonPositiveOnly(x int) (int, error) { 128 | if x > 0 { 129 | return 0, errors.New("must be negative") 130 | } 131 | return x, nil 132 | } 133 | 134 | func TestDo_RethrowsPanic(t *testing.T) { 135 | err := func() (rErr error) { 136 | defer func() { 137 | e := recover() 138 | if e == nil { 139 | t.Errorf("panic must be rethrown") 140 | } 141 | }() 142 | defer Handle(&rErr) 143 | Do0(func() error { 144 | panic(fmt.Errorf("some non-wrapped error")) 145 | }()) 146 | return nil 147 | }() 148 | if err != nil { 149 | t.Errorf("panic was interpreted as wrapped error") 150 | } 151 | } 152 | --------------------------------------------------------------------------------