From d4080f782e4af5e080027ac8eb1dd472ef82b783 Mon Sep 17 00:00:00 2001 From: Spiegel Date: Thu, 22 Jun 2023 20:47:22 +0900 Subject: [PATCH 1/2] Bump up external packages --- zapobject/go.mod | 6 +++--- zapobject/go.sum | 17 ++++++----------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/zapobject/go.mod b/zapobject/go.mod index 3c0955b..484201a 100644 --- a/zapobject/go.mod +++ b/zapobject/go.mod @@ -3,11 +3,11 @@ module github.com/goark/errs/zapobject go 1.20 require ( - github.com/goark/errs v1.2.2 + github.com/goark/errs v1.2.3 go.uber.org/zap v1.24.0 ) require ( - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect ) diff --git a/zapobject/go.sum b/zapobject/go.sum index 7418502..4fc10e9 100644 --- a/zapobject/go.sum +++ b/zapobject/go.sum @@ -1,20 +1,15 @@ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/goark/errs v1.2.2 h1:UrMZZJL0WaOzaO+ErSV+nz/k/+bmW2wUiFe5V7pUeEo= -github.com/goark/errs v1.2.2/go.mod h1:ZsQucxaDFVfSB8I99j4bxkDRfNOrlKINwg72QMuRWKw= +github.com/goark/errs v1.2.3 h1:ATChtJki69yLftoXzvp1PnrSXXFJcLW3mDRExdu+Gu4= +github.com/goark/errs v1.2.3/go.mod h1:ZsQucxaDFVfSB8I99j4bxkDRfNOrlKINwg72QMuRWKw= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From 1ff424440e6fe828edd61dae47090f7cd482b04a Mon Sep 17 00:00:00 2001 From: Spiegel Date: Sun, 25 Jun 2023 20:20:34 +0900 Subject: [PATCH 2/2] Add Errors type for handling multiple errors --- README.md | 18 +++- errlist.go | 170 ++++++++++++++++++++++++++++++++++++++ example_test.go | 36 +++++++- sample/sample5.go | 18 +++- zapobject/example_test.go | 21 +++++ zapobject/zapobject.go | 4 +- 6 files changed, 258 insertions(+), 9 deletions(-) create mode 100644 errlist.go diff --git a/README.md b/README.md index 6d84b92..9965549 100644 --- a/README.md +++ b/README.md @@ -151,18 +151,30 @@ import ( "errors" "fmt" "io" - "os" + "sync" "github.com/goark/errs" ) func generateMultiError() error { - return errs.Wrap(errors.Join(os.ErrInvalid, io.EOF)) + errlist := &errs.Errors{} + var wg sync.WaitGroup + for i := 1; i <= 2; i++ { + i := i + wg.Add(1) + go func() { + defer wg.Done() + errlist.Add(fmt.Errorf("error %d", i)) + }() + } + wg.Wait() + errlist.Add(io.EOF) + return errlist.ErrorOrNil() } func main() { err := generateMultiError() - fmt.Printf("%+v\n", err) // {"Type":"*errs.Error","Err":{"Type":"*errors.joinError","Msg":"invalid argument\nEOF","Cause":[{"Type":"*errors.errorString","Msg":"invalid argument"},{"Type":"*errors.errorString","Msg":"EOF"}]},"Context":{"function":"main.generateMultiError"}} + fmt.Printf("%+v\n", err) // {"Type":"*errs.Errors","Errs":[{"Type":"*errors.errorString","Msg":"error 2"},{"Type":"*errors.errorString","Msg":"error 1"},{"Type":"*errors.errorString","Msg":"EOF"}]} fmt.Println(errors.Is(err, io.EOF)) // true } ``` diff --git a/errlist.go b/errlist.go new file mode 100644 index 0000000..814c60c --- /dev/null +++ b/errlist.go @@ -0,0 +1,170 @@ +package errs + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" + "sync" +) + +// Errors is multiple error instance. +type Errors struct { + mu sync.RWMutex + Errs []error +} + +// Join function returns Errors instance. +func Join(errlist ...error) error { + if len(errlist) == 0 { + return nil + } + ct := 0 + for _, err := range errlist { + if err != nil { + ct++ + } + } + if ct == 0 { + return nil + } + el := &Errors{Errs: make([]error, 0, ct)} + for _, err := range errlist { + if err != nil { + el.Errs = append(el.Errs, err) + ct++ + } + } + return el +} + +// Add method adds errors to Errors. +func (es *Errors) Add(errlist ...error) { + if es == nil { + return + } + es.mu.Lock() + defer es.mu.Unlock() + es.Errs = append(es.Errs, errlist...) +} + +// ErrorOrNil method returns this as a error type. +func (es *Errors) ErrorOrNil() error { + if es == nil || len(es.Errs) == 0 { + return nil + } + return es +} + +// Error method returns error message. +// This method is a implementation of error interface. +func (es *Errors) Error() string { + if es == nil || len(es.Errs) == 0 { + return nilAngleString + } + es.mu.RLock() + defer es.mu.RUnlock() + var b []byte + for i, err := range es.Errs { + if i > 0 { + b = append(b, '\n') + } + b = append(b, err.Error()...) + } + return string(b) +} + +// String method returns error message. +// This method is a implementation of fmt.Stringer interface. +func (es *Errors) String() string { + return es.Error() +} + +// GoString method returns serialize string of Errors. +// This method is a implementation of fmt.GoStringer interface. +func (es *Errors) GoString() string { + if es == nil || len(es.Errs) == 0 { + return nilAngleString + } + es.mu.RLock() + defer es.mu.RUnlock() + return fmt.Sprintf("%T{Errs:%#v}", es, es.Errs) +} + +// MarshalJSON method returns serialize string of Errors with JSON format. +// This method is implementation of json.Marshaler interface. +func (es *Errors) MarshalJSON() ([]byte, error) { + return []byte(es.EncodeJSON()), nil +} + +// Format method returns formatted string of Errors instance. +// This method is a implementation of fmt.Formatter interface. +func (es *Errors) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case s.Flag('#'): + _, _ = strings.NewReader(es.GoString()).WriteTo(s) + case s.Flag('+'): + _, _ = strings.NewReader(es.EncodeJSON()).WriteTo(s) + default: + _, _ = strings.NewReader(es.Error()).WriteTo(s) + } + case 's': + _, _ = strings.NewReader(es.String()).WriteTo(s) + default: + fmt.Fprintf(s, `%%!%c(%s)`, verb, es.GoString()) + } +} + +// EncodeJSON method returns serialize string of Errors with JSON format. +func (es *Errors) EncodeJSON() string { + if es == nil { + return "null" + } + es.mu.RLock() + defer es.mu.RUnlock() + elms := []string{} + elms = append(elms, strings.Join([]string{`"Type":`, strconv.Quote(reflect.TypeOf(es).String())}, "")) + if len(es.Errs) > 0 { + elms2 := []string{} + for _, err := range es.Errs { + msgBuf := &bytes.Buffer{} + json.HTMLEscape(msgBuf, []byte(EncodeJSON(err))) + elms2 = append(elms2, msgBuf.String()) + } + elms = append(elms, strings.Join([]string{`"Errs":[`, strings.Join(elms2, ","), "]"}, "")) + } + return strings.Join([]string{"{", strings.Join(elms, ","), "}"}, "") +} + +// Unwrap method returns error list in Errors instance. +// This method is used in errors.Unwrap function. +func (es *Errors) Unwrap() []error { + if es == nil { + return nil + } + es.mu.RLock() + defer es.mu.RUnlock() + if len(es.Errs) == 0 { + return nil + } + return es.Errs +} + +/* Copyright 2023 Spiegel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/example_test.go b/example_test.go index 52d35b6..fefbd74 100644 --- a/example_test.go +++ b/example_test.go @@ -1,8 +1,10 @@ package errs_test import ( + "errors" "fmt" "os" + "sync" "github.com/goark/errs" ) @@ -36,7 +38,39 @@ func ExampleEncodeJSON() { // {"Type":"*fs.PathError","Msg":"open not-exist.txt: no such file or directory","Cause":{"Type":"syscall.Errno","Msg":"no such file or directory"}} } -/* Copyright 2019-2021 Spiegel +func ExampleJoin() { + err := errs.Join(errors.New("error 1"), errors.New("error 2")) + fmt.Println(err) + errlist, ok := err.(*errs.Errors) + if !ok { + return + } + errlist.Add(errors.New("error 3")) + fmt.Printf("%+v\n", errlist.ErrorOrNil()) + // Output: + // error 1 + // error 2 + // {"Type":"*errs.Errors","Errs":[{"Type":"*errors.errorString","Msg":"error 1"},{"Type":"*errors.errorString","Msg":"error 2"},{"Type":"*errors.errorString","Msg":"error 3"}]} +} + +func ExampleErrors() { + errlist := &errs.Errors{} + var wg sync.WaitGroup + for i := 1; i <= 100000; i++ { + i := i + wg.Add(1) + go func() { + defer wg.Done() + errlist.Add(fmt.Errorf("error %d", i)) + }() + } + wg.Wait() + fmt.Println("error ount =", len(errlist.Unwrap())) + // Output: + // error ount = 100000 +} + +/* Copyright 2019-2023 Spiegel * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/sample/sample5.go b/sample/sample5.go index 2c89ca6..9c8babd 100644 --- a/sample/sample5.go +++ b/sample/sample5.go @@ -7,17 +7,29 @@ import ( "errors" "fmt" "io" - "os" + "sync" "github.com/goark/errs" ) func generateMultiError() error { - return errs.Wrap(errors.Join(os.ErrInvalid, io.EOF)) + errlist := &errs.Errors{} + var wg sync.WaitGroup + for i := 1; i <= 2; i++ { + i := i + wg.Add(1) + go func() { + defer wg.Done() + errlist.Add(fmt.Errorf("error %d", i)) + }() + } + wg.Wait() + errlist.Add(io.EOF) + return errlist.ErrorOrNil() } func main() { err := generateMultiError() - fmt.Printf("%+v\n", err) // {"Type":"*errs.Error","Err":{"Type":"*errors.joinError","Msg":"invalid argument\nEOF","Cause":[{"Type":"*errors.errorString","Msg":"invalid argument"},{"Type":"*errors.errorString","Msg":"EOF"}]},"Context":{"function":"main.generateMultiError"}} + fmt.Printf("%+v\n", err) // {"Type":"*errs.Errors","Errs":[{"Type":"*errors.errorString","Msg":"error 2"},{"Type":"*errors.errorString","Msg":"error 1"},{"Type":"*errors.errorString","Msg":"EOF"}]} fmt.Println(errors.Is(err, io.EOF)) // true } diff --git a/zapobject/example_test.go b/zapobject/example_test.go index 5ab4e79..3209a3f 100644 --- a/zapobject/example_test.go +++ b/zapobject/example_test.go @@ -1,7 +1,9 @@ package zapobject_test import ( + "fmt" "os" + "sync" "github.com/goark/errs" "github.com/goark/errs/zapobject" @@ -22,6 +24,21 @@ func checkFileOpen(path string) error { return nil } +func generateMultiError() error { + errlist := &errs.Errors{} + var wg sync.WaitGroup + for i := 1; i <= 2; i++ { + i := i + wg.Add(1) + go func() { + defer wg.Done() + errlist.Add(fmt.Errorf("error %d", i)) + }() + } + wg.Wait() + return errlist.ErrorOrNil() +} + func Example() { logger := zap.NewExample() defer logger.Sync() @@ -29,6 +46,10 @@ func Example() { if err := checkFileOpen("not-exist.txt"); err != nil { logger.Error("err", zap.Object("error", zapobject.New(err))) } + if err := generateMultiError(); err != nil { + logger.Error("err", zap.Object("error", zapobject.New(err))) + } // Output: // {"level":"error","msg":"err","error":{"type":"*errs.Error","msg":"file open error: open not-exist.txt: no such file or directory","error":{"type":"*errors.errorString","msg":"file open error"},"cause":{"type":"*fs.PathError","msg":"open not-exist.txt: no such file or directory","cause":{"type":"syscall.Errno","msg":"no such file or directory"}},"context":{"function":"github.com/goark/errs/zapobject_test.checkFileOpen","path":"not-exist.txt"}}} + // {"level":"error","msg":"err","error":{"type":"*errs.Errors","msg":"error 2\nerror 1","causes":[{"type":"*errors.errorString","msg":"error 2"},{"type":"*errors.errorString","msg":"error 1"}]}} } diff --git a/zapobject/zapobject.go b/zapobject/zapobject.go index 8bbabca..184cb4d 100644 --- a/zapobject/zapobject.go +++ b/zapobject/zapobject.go @@ -56,8 +56,8 @@ func (e ErrObject) MarshalLogObject(enc zapcore.ObjectEncoder) error { return enc.AddObject("cause", New(errList[0])) } elist := make([]ErrObject, 0, len(errList)) - for i, e := range errList { - elist[i] = New(e) + for _, e := range errList { + elist = append(elist, New(e)) } zap.Objects("causes", elist).AddTo(enc) }