Must support Custom error handler. (#752)

* Custom error type.

* remove  "github.com/pkg/errors" depend

* disable some Test Parallel

* fix test code in go 1.18

* golangcli-lint warning fix

---------

Co-authored-by: Samuel Berthe <dev@samuel-berthe.fr>
This commit is contained in:
RelicOfTesla
2026-01-09 01:07:00 +08:00
committed by GitHub
parent a2553e57bb
commit eb9719c674
2 changed files with 123 additions and 13 deletions
+9 -9
View File
@@ -30,8 +30,8 @@ func messageFromMsgAndArgs(msgAndArgs ...any) string {
return "" return ""
} }
// must panics if err is error or false. // MustChecker panics if err is error or false.
func must(err any, messageArgs ...any) { var MustChecker = func(err any, messageArgs ...any) {
if err == nil { if err == nil {
return return
} }
@@ -63,14 +63,14 @@ func must(err any, messageArgs ...any) {
// and panics if err is error or false. // and panics if err is error or false.
// Play: https://go.dev/play/p/fOqtX5HudtN // Play: https://go.dev/play/p/fOqtX5HudtN
func Must[T any](val T, err any, messageArgs ...any) T { func Must[T any](val T, err any, messageArgs ...any) T {
must(err, messageArgs...) MustChecker(err, messageArgs...)
return val return val
} }
// Must0 has the same behavior as Must, but callback returns no variable. // Must0 has the same behavior as Must, but callback returns no variable.
// Play: https://go.dev/play/p/TMoWrRp3DyC // Play: https://go.dev/play/p/TMoWrRp3DyC
func Must0(err any, messageArgs ...any) { func Must0(err any, messageArgs ...any) {
must(err, messageArgs...) MustChecker(err, messageArgs...)
} }
// Must1 is an alias to Must. // Must1 is an alias to Must.
@@ -82,35 +82,35 @@ func Must1[T any](val T, err any, messageArgs ...any) T {
// Must2 has the same behavior as Must, but callback returns 2 variables. // Must2 has the same behavior as Must, but callback returns 2 variables.
// Play: https://go.dev/play/p/TMoWrRp3DyC // Play: https://go.dev/play/p/TMoWrRp3DyC
func Must2[T1, T2 any](val1 T1, val2 T2, err any, messageArgs ...any) (T1, T2) { func Must2[T1, T2 any](val1 T1, val2 T2, err any, messageArgs ...any) (T1, T2) {
must(err, messageArgs...) MustChecker(err, messageArgs...)
return val1, val2 return val1, val2
} }
// Must3 has the same behavior as Must, but callback returns 3 variables. // Must3 has the same behavior as Must, but callback returns 3 variables.
// Play: https://go.dev/play/p/TMoWrRp3DyC // Play: https://go.dev/play/p/TMoWrRp3DyC
func Must3[T1, T2, T3 any](val1 T1, val2 T2, val3 T3, err any, messageArgs ...any) (T1, T2, T3) { func Must3[T1, T2, T3 any](val1 T1, val2 T2, val3 T3, err any, messageArgs ...any) (T1, T2, T3) {
must(err, messageArgs...) MustChecker(err, messageArgs...)
return val1, val2, val3 return val1, val2, val3
} }
// Must4 has the same behavior as Must, but callback returns 4 variables. // Must4 has the same behavior as Must, but callback returns 4 variables.
// Play: https://go.dev/play/p/TMoWrRp3DyC // Play: https://go.dev/play/p/TMoWrRp3DyC
func Must4[T1, T2, T3, T4 any](val1 T1, val2 T2, val3 T3, val4 T4, err any, messageArgs ...any) (T1, T2, T3, T4) { func Must4[T1, T2, T3, T4 any](val1 T1, val2 T2, val3 T3, val4 T4, err any, messageArgs ...any) (T1, T2, T3, T4) {
must(err, messageArgs...) MustChecker(err, messageArgs...)
return val1, val2, val3, val4 return val1, val2, val3, val4
} }
// Must5 has the same behavior as Must, but callback returns 5 variables. // Must5 has the same behavior as Must, but callback returns 5 variables.
// Play: https://go.dev/play/p/TMoWrRp3DyC // Play: https://go.dev/play/p/TMoWrRp3DyC
func Must5[T1, T2, T3, T4, T5 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, err any, messageArgs ...any) (T1, T2, T3, T4, T5) { func Must5[T1, T2, T3, T4, T5 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, err any, messageArgs ...any) (T1, T2, T3, T4, T5) {
must(err, messageArgs...) MustChecker(err, messageArgs...)
return val1, val2, val3, val4, val5 return val1, val2, val3, val4, val5
} }
// Must6 has the same behavior as Must, but callback returns 6 variables. // Must6 has the same behavior as Must, but callback returns 6 variables.
// Play: https://go.dev/play/p/TMoWrRp3DyC // Play: https://go.dev/play/p/TMoWrRp3DyC
func Must6[T1, T2, T3, T4, T5, T6 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, val6 T6, err any, messageArgs ...any) (T1, T2, T3, T4, T5, T6) { func Must6[T1, T2, T3, T4, T5, T6 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, val6 T6, err any, messageArgs ...any) (T1, T2, T3, T4, T5, T6) {
must(err, messageArgs...) MustChecker(err, messageArgs...)
return val1, val2, val3, val4, val5, val6 return val1, val2, val3, val4, val5, val6
} }
+114 -4
View File
@@ -3,6 +3,11 @@ package lo
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"net/url"
"reflect"
"runtime/debug"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -22,8 +27,8 @@ func TestValidate(t *testing.T) {
is.NoError(result2) is.NoError(result2)
} }
func TestMust(t *testing.T) { func TestMust(t *testing.T) { //nolint:paralleltest
t.Parallel() // t.Parallel()
is := assert.New(t) is := assert.New(t)
is.Equal("foo", Must("foo", nil)) is.Equal("foo", Must("foo", nil))
@@ -63,8 +68,8 @@ func TestMust(t *testing.T) {
}) })
} }
func TestMustX(t *testing.T) { func TestMustX(t *testing.T) { //nolint:paralleltest
t.Parallel() // t.Parallel()
is := assert.New(t) is := assert.New(t)
{ {
@@ -254,6 +259,111 @@ func TestMustX(t *testing.T) {
} }
} }
func mustCheckerWithStack(err any, messageArgs ...any) {
if err == nil {
return
}
switch e := err.(type) {
case bool:
if !e {
message := messageFromMsgAndArgs(messageArgs...)
if message == "" {
message = "not ok"
}
// panic(stackErrors.New(message))
panic(errorsJoin(errors.New(message), errors.New(string(debug.Stack()))))
}
case error:
message := messageFromMsgAndArgs(messageArgs...)
if message != "" {
// panic(stackErrors.Wrap(e, message))
panic(errorsJoin(e, errors.New(message), errors.New(string(debug.Stack()))))
}
// panic(stackErrors.WithStack(e))
panic(errorsJoin(e, errors.New(string(debug.Stack()))))
default:
// panic(stackErrors.New("must: invalid err type '" + reflect.TypeOf(err).Name() + "', should either be a bool or an error"))
panic(errorsJoin(errors.New("must: invalid err type '"+reflect.TypeOf(err).Name()+"', should either be a bool or an error"),
errors.New(string(debug.Stack()))))
}
}
// errorsJoin: var errorsJoin = errors.Join // only go 1.20+, not in go 1.18 .
func errorsJoin(es ...error) joinErrors { return joinErrors(es) }
type joinErrors []error
func (es joinErrors) Is(target error) bool {
for _, e := range es {
if errors.Is(e, target) {
return true
}
}
return error(es) == target
}
func (es joinErrors) Error() string {
sb := strings.Builder{}
for _, e := range es {
sb.WriteString(e.Error())
sb.WriteRune('\n')
}
return sb.String()
}
func (es joinErrors) As(t any) bool {
for _, e := range es {
if errors.As(e, t) {
return true
}
}
return false
}
func TestMustUserCustomHandler(t *testing.T) { //nolint:paralleltest
oldMustChecker := MustChecker
MustChecker = mustCheckerWithStack
defer func() {
MustChecker = oldMustChecker
}()
t.Run("wrap stack", func(t *testing.T) { //nolint:paralleltest
err, ok := TryWithErrorValue(func() error {
Must("foo", errors.New("wrap callstack"))
return nil
})
assert.False(t, ok)
fullErrStr := fmt.Sprintf("%+v", err)
assert.Contains(t, fullErrStr, "/errors_test.go:", fullErrStr)
})
t.Run("wrap as", func(t *testing.T) { //nolint:paralleltest
e, ok := TryWithErrorValue(func() error {
Must("foo", errorsJoin(io.EOF, &url.Error{
Op: "test op",
URL: "test url",
Err: io.ErrUnexpectedEOF,
}))
return nil
})
assert.False(t, ok)
err, ok := e.(error)
assert.True(t, ok)
errURL, ok := ErrorsAs[*url.Error](err)
assert.True(t, ok)
assert.NotNil(t, errURL)
if errURL != nil {
assert.Equal(t, "test url", errURL.URL)
assert.Equal(t, "test op", errURL.Op)
assert.ErrorIs(t, err, io.EOF)
assert.ErrorIs(t, err, io.ErrUnexpectedEOF)
}
})
}
func TestTry(t *testing.T) { func TestTry(t *testing.T) {
t.Parallel() t.Parallel()
is := assert.New(t) is := assert.New(t)