refract(gerror): add ITextArgs interface and its implements, mainly for i18n that needs text and args separately (#4597)

This pull request refactors the error handling code to improve support
for error text formatting with arguments, making it easier to retrieve
both the error message template and its arguments (useful for i18n and
structured error handling). It introduces the new `ITextArgs` interface,
updates error constructors to store format strings and arguments
separately, and adds methods to retrieve them. Several usages and tests
are updated to reflect these changes.

### Error formatting and argument support

* Introduced the `ITextArgs` interface to allow errors to expose their
text template and arguments separately, supporting advanced use cases
like internationalization (`errors/gerror/gerror.go`).
* Updated the `Error` struct to include an `args` field for error
arguments, and added methods `TextWithArgs()`, `Text()`, and `Args()` to
retrieve formatted error text, the template, and arguments respectively
(`errors/gerror/gerror_error.go`).
[[1]](diffhunk://#diff-b56b52e546735b8196ec3e8bd25c0b007ac134e2f13b116ee3abcb2f92c3bdd9R23)
[[2]](diffhunk://#diff-b56b52e546735b8196ec3e8bd25c0b007ac134e2f13b116ee3abcb2f92c3bdd9L121-R145)
* Changed all error creation and wrapping functions (e.g., `Newf`,
`Wrapf`, `NewCodef`, etc.) to store the format string and arguments
separately, rather than pre-formatting the error text
(`errors/gerror/gerror_api.go`, `errors/gerror/gerror_api_code.go`).
[[1]](diffhunk://#diff-847475c1de42114004c50163aa2f34a4095e05122b4c2993aa3df4e5923e83cbL24-R27)
[[2]](diffhunk://#diff-847475c1de42114004c50163aa2f34a4095e05122b4c2993aa3df4e5923e83cbL43-R48)
[[3]](diffhunk://#diff-847475c1de42114004c50163aa2f34a4095e05122b4c2993aa3df4e5923e83cbL77-R78)
[[4]](diffhunk://#diff-31ee6b1493f4b206c060a98818226b1b78102c91b5ae22e34ed4d1bb4a38c185L25-R29)
[[5]](diffhunk://#diff-31ee6b1493f4b206c060a98818226b1b78102c91b5ae22e34ed4d1bb4a38c185L44-R50)
[[6]](diffhunk://#diff-31ee6b1493f4b206c060a98818226b1b78102c91b5ae22e34ed4d1bb4a38c185L77-R79)
[[7]](diffhunk://#diff-31ee6b1493f4b206c060a98818226b1b78102c91b5ae22e34ed4d1bb4a38c185L107-R110)
* Updated the `Option` struct and related constructor to handle error
arguments (`errors/gerror/gerror_api_option.go`).
[[1]](diffhunk://#diff-4b458af6df9a0d8289303cf408b082ed472360b286cdc5a556c8fe7541973caaR16)
[[2]](diffhunk://#diff-4b458af6df9a0d8289303cf408b082ed472360b286cdc5a556c8fe7541973caaR26)

### Code and test improvements

* Updated formatting and equality checks to use the new methods for
retrieving formatted error text and arguments, ensuring consistent
behavior (`errors/gerror/gerror_error.go`,
`errors/gerror/gerror_error_format.go`).
[[1]](diffhunk://#diff-b56b52e546735b8196ec3e8bd25c0b007ac134e2f13b116ee3abcb2f92c3bdd9L45-R46)
[[2]](diffhunk://#diff-fa801ef307f6c6fdda49fe9853593de29eda5b4d3712ea5bf9ed39de6e6859ebL26-R26)
* Improved unit tests to verify the new interface and argument handling,
including tests for the `ITextArgs` interface
(`errors/gerror/gerror_z_unit_test.go`).
* Minor code cleanup, such as removing unused imports and updating
comments for clarity (`errors/gerror/gerror_api.go`,
`errors/gerror/gerror_api_code.go`,
`errors/gerror/gerror_error_json.go`).
[[1]](diffhunk://#diff-847475c1de42114004c50163aa2f34a4095e05122b4c2993aa3df4e5923e83cbL10-L11)
[[2]](diffhunk://#diff-31ee6b1493f4b206c060a98818226b1b78102c91b5ae22e34ed4d1bb4a38c185L10)
[[3]](diffhunk://#diff-3e4ba207e242eb338f31f1091466374e8e72754a8969d92724bfb5c6b88f25edL15-R15)

These changes make error handling more flexible and maintainable,
especially for scenarios where error messages need to be localized or
programmatically inspected.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
John Guo
2026-01-09 10:48:43 +08:00
committed by GitHub
parent c5778127b1
commit db9f47d942
8 changed files with 82 additions and 24 deletions

View File

@ -17,40 +17,48 @@ import (
// IEqual is the interface for Equal feature.
type IEqual interface {
Error() string
error
Equal(target error) bool
}
// ICode is the interface for Code feature.
type ICode interface {
Error() string
error
Code() gcode.Code
}
// IStack is the interface for Stack feature.
type IStack interface {
Error() string
error
Stack() string
}
// ICause is the interface for Cause feature.
type ICause interface {
Error() string
error
Cause() error
}
// ICurrent is the interface for Current feature.
type ICurrent interface {
Error() string
error
Current() error
}
// IUnwrap is the interface for Unwrap feature.
type IUnwrap interface {
Error() string
error
Unwrap() error
}
// ITextArgs is the interface for Text and Args features.
// This interface is mainly used for i18n features, that needs text and args separately.
type ITextArgs interface {
error
Text() string
Args() []any
}
const (
// commaSeparatorSpace is the comma separator with space.
commaSeparatorSpace = ", "

View File

@ -7,8 +7,6 @@
package gerror
import (
"fmt"
"github.com/gogf/gf/v2/errors/gcode"
)
@ -25,7 +23,8 @@ func New(text string) error {
func Newf(format string, args ...any) error {
return &Error{
stack: callers(),
text: fmt.Sprintf(format, args...),
text: format,
args: args,
code: gcode.CodeNil,
}
}
@ -45,7 +44,8 @@ func NewSkip(skip int, text string) error {
func NewSkipf(skip int, format string, args ...any) error {
return &Error{
stack: callers(skip),
text: fmt.Sprintf(format, args...),
text: format,
args: args,
code: gcode.CodeNil,
}
}
@ -74,7 +74,8 @@ func Wrapf(err error, format string, args ...any) error {
return &Error{
error: err,
stack: callers(),
text: fmt.Sprintf(format, args...),
text: format,
args: args,
code: Code(err),
}
}
@ -104,7 +105,8 @@ func WrapSkipf(skip int, err error, format string, args ...any) error {
return &Error{
error: err,
stack: callers(skip),
text: fmt.Sprintf(format, args...),
text: format,
args: args,
code: Code(err),
}
}

View File

@ -7,7 +7,6 @@
package gerror
import (
"fmt"
"strings"
"github.com/gogf/gf/v2/errors/gcode"
@ -26,7 +25,8 @@ func NewCode(code gcode.Code, text ...string) error {
func NewCodef(code gcode.Code, format string, args ...any) error {
return &Error{
stack: callers(),
text: fmt.Sprintf(format, args...),
text: format,
args: args,
code: code,
}
}
@ -46,7 +46,8 @@ func NewCodeSkip(code gcode.Code, skip int, text ...string) error {
func NewCodeSkipf(code gcode.Code, skip int, format string, args ...any) error {
return &Error{
stack: callers(skip),
text: fmt.Sprintf(format, args...),
text: format,
args: args,
code: code,
}
}
@ -74,7 +75,8 @@ func WrapCodef(code gcode.Code, err error, format string, args ...any) error {
return &Error{
error: err,
stack: callers(),
text: fmt.Sprintf(format, args...),
text: format,
args: args,
code: code,
}
}
@ -104,7 +106,8 @@ func WrapCodeSkipf(code gcode.Code, skip int, err error, format string, args ...
return &Error{
error: err,
stack: callers(skip),
text: fmt.Sprintf(format, args...),
text: format,
args: args,
code: code,
}
}

View File

@ -13,6 +13,7 @@ type Option struct {
Error error // Wrapped error if any.
Stack bool // Whether recording stack information into error.
Text string // Error text, which is created by New* functions.
Args []any // Error arguments for formatted error text.
Code gcode.Code // Error code if necessary.
}
@ -22,6 +23,7 @@ func NewWithOption(option Option) error {
err := &Error{
error: option.Error,
text: option.Text,
args: option.Args,
code: option.Code,
}
if option.Stack {

View File

@ -20,6 +20,7 @@ type Error struct {
error error // Wrapped error.
stack stack // Stack array, which records the stack information when this error is created or wrapped.
text string // Custom Error text when Error is created, might be empty when its code is not nil.
args []any // Custom arguments for formatting the error text.
code gcode.Code // Error code if necessary.
}
@ -42,7 +43,7 @@ func (err *Error) Error() string {
if err == nil {
return ""
}
errStr := err.text
errStr := err.TextWithArgs()
if errStr == "" && err.code != nil {
errStr = err.code.Message()
}
@ -76,7 +77,7 @@ func (err *Error) Cause() error {
// return loop
//
// To be compatible with Case of https://github.com/pkg/errors.
return errors.New(loop.text)
return errors.New(loop.TextWithArgs())
}
}
return nil
@ -92,6 +93,7 @@ func (err *Error) Current() error {
error: nil,
stack: err.stack,
text: err.text,
args: err.args,
code: err.code,
}
}
@ -118,8 +120,26 @@ func (err *Error) Equal(target error) bool {
return false
}
// Text should be the same.
if err.text != fmt.Sprintf(`%-s`, target) {
if err.TextWithArgs() != fmt.Sprintf(`%-s`, target) {
return false
}
return true
}
// TextWithArgs returns the formatted error text with its arguments.
func (err *Error) TextWithArgs() string {
if len(err.args) > 0 {
return fmt.Sprintf(err.text, err.args...)
}
return err.text
}
// Text returns the error text of current error.
func (err *Error) Text() string {
return err.text
}
// Args returns the error arguments of current error.
func (err *Error) Args() []any {
return err.args
}

View File

@ -23,7 +23,7 @@ func (err *Error) Format(s fmt.State, verb rune) {
switch {
case s.Flag('-'):
if err.text != "" {
_, _ = io.WriteString(s, err.text)
_, _ = io.WriteString(s, err.TextWithArgs())
} else {
_, _ = io.WriteString(s, err.Error())
}

View File

@ -10,8 +10,8 @@ import (
"encoding/json"
)
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
// Note that do not use pointer as its receiver here.
func (err Error) MarshalJSON() ([]byte, error) {
// MarshalJSON implements the interface json.Marshaler for Error.
// It serializes the error using its string representation.
func (err *Error) MarshalJSON() ([]byte, error) {
return json.Marshal(err.Error())
}

View File

@ -804,3 +804,26 @@ func Test_WrapCodeSkip_MultipleTexts(t *testing.T) {
t.Assert(err.Error(), "text1, text2: inner")
})
}
func Test_TextArgs(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
err := gerror.New("text")
textArgs := err.(gerror.ITextArgs)
t.Assert(textArgs.Text(), "text")
t.Assert(textArgs.Args(), nil)
})
gtest.C(t, func(t *gtest.T) {
err := gerror.Newf("text: %s", "arg1")
textArgs := err.(gerror.ITextArgs)
t.Assert(textArgs.Text(), "text: %s")
t.Assert(textArgs.Args(), []any{"arg1"})
})
gtest.C(t, func(t *gtest.T) {
err1 := errors.New("text")
err2 := gerror.Wrapf(err1, "wrap: %s", "arg1")
textArgs := err2.(gerror.ITextArgs)
t.Assert(textArgs.Error(), "wrap: arg1: text")
t.Assert(textArgs.Text(), "wrap: %s")
t.Assert(textArgs.Args(), []any{"arg1"})
})
}