golang内置错误处理增强与自定义错误类型插件库errors的使用

Golang内置错误处理增强与自定义错误类型插件库errors的使用

errors gopher

Errors包是Go内置errors包的替代品,它允许您创建14种不同类型的错误,涵盖了大多数用例。其中一些对于Web应用程序来说可能过于具体,但仍然很有用。

功能特性

  1. 支持14种错误类型
  2. 轻松处理用户友好消息
  3. 堆栈跟踪 - 格式化、未格式化、自定义格式
  4. 获取堆栈跟踪的程序计数器
  5. 使用errors.RuntimeFrames(err error)获取堆栈跟踪的runtime.Frames
  6. 所有错误类型都支持HTTP状态码和用户友好消息(包装的消息会被连接)
  7. 所有错误类型都支持GRPC状态码和用户友好消息(包装的消息会被连接)
  8. 生成每种错误类型的辅助函数
  9. 辅助函数用于获取错误类型、错误类型作为整数、检查错误类型是否在链中的任何位置被包装
  10. 支持fmt.Formatter

对于嵌套错误,消息和错误也会遍历整个错误链。

可用的错误类型

  1. TypeInternal - 内部系统错误,例如数据库错误
  2. TypeValidation - 验证错误,例如无效的电子邮件地址
  3. TypeInputBody - 无效的输入数据,例如无效的JSON
  4. TypeDuplicate - 重复内容错误,例如用户已存在(尝试注册新用户时)
  5. TypeUnauthenticated - 未认证错误
  6. TypeUnauthorized - 未授权访问错误
  7. TypeEmpty - 当预期的非空资源为空时
  8. TypeNotFound - 预期资源未找到,例如用户ID未找到
  9. TypeMaximumAttempts - 尝试同一操作超过允许的阈值
  10. TypeSubscriptionExpired - 当用户的"付费"账户已过期时
  11. TypeDownstreamDependencyTimedout - 当下游依赖服务请求超时时
  12. TypeNotImplemented - 当请求的功能由于能力不足而无法完成时
  13. TypeContextTimedout - 当Go上下文超时时
  14. TypeContextCancelled - 当Go上下文被取消时

用户友好消息

在编写API时,我们通常希望用更容易理解的用户友好消息来响应。而不是返回原始错误,只需记录原始错误。

对于所有错误类型都有辅助函数。当需要为现有错误设置友好消息时,有带有后缀’Err’的辅助函数。所有这些辅助函数都接受原始错误和一个字符串。

package main

import (
	"fmt"

	"github.com/naughtygopher/errors"
)

func Bar() error {
	return fmt.Errorf("hello %s", "world!")
}

func Foo() error {
	err := Bar()
	if err != nil {
		return errors.InternalErr(err, "bar is not happy")
	}
	return nil
}

func main() {
	err := Foo()

	fmt.Println("err:", err)
	fmt.Println("\nerr.Error():", err.Error())

	fmt.Printf("\nformatted +v: %+v\n", err)
	fmt.Printf("\nformatted v: %v\n", err)
	fmt.Printf("\nformatted +s: %+s\n", err)
	fmt.Printf("\nformatted s: %s\n", err)

	_, msg, _ := errors.HTTPStatusCodeMessage(err)
	fmt.Println("\nmsg:", msg)
}

输出:

err: bar is not happy

err.Error(): /Users/k.balakumaran/go/src/github.com/naughtygopher/errors/cmd/main.go:16: bar is not happy
hello world!bar is not happy

formatted +v: /Users/k.balakumaran/go/src/github.com/naughtygopher/errors/cmd/main.go:16: bar is not happy
hello world!bar is not happy

formatted v: bar is not happy

formatted +s: bar is not happy: hello world!

formatted s: bar is not happy

msg: bar is not happy

文件路径和行号前缀

Go错误的一个常见烦恼是找出错误的来源,特别是在有嵌套函数调用时。注释通过能够为错误提供上下文消息有很大帮助。例如fmt.Errorf("database query returned error %w", err)。然而在这个包中,Error() string函数(Go错误接口方法)打印带有文件路径和行号前缀的错误。它看起来像../Users/JohnDoe/apps/main.go:50 hello world,其中’hello world’是错误消息。

HTTP/GRPC状态码和消息

函数errors.HTTPStatusCodeMessage(error) (int, string, bool)errors.GRPCStatusCodeMessage(error) (int, string, bool)返回HTTP/GRPC状态码、消息和布尔值。如果错误是此包中的*Error类型,则布尔值为true。如果错误是嵌套的,它会解包并返回单个连接的消息。

如何使用?

除了在"用户友好消息"部分解释的函数外,下面提供了更多示例。

package main

import (
	"fmt"
	"net/http"
	"time"

	"github.com/naughtygopher/errors"
	"github.com/naughtygopher/webgo/v6"
	"github.com/naughtygopher/webgo/v6/middleware/accesslog"
)

func bar() error {
	return fmt.Errorf("%s %s", "sinking", "bar")
}

func bar2() error {
	err := bar()
	if err != nil {
		return errors.InternalErr(err, "bar2 was deceived by bar1 :(")
	}
	return nil
}

func foo() error {
	err := bar2()
	if err != nil {
		return errors.InternalErr(err, "we lost bar2!")
	}
	return nil
}

func handler(w http.ResponseWriter, r *http.Request) {
	err := foo()
	if err != nil {
		// 在服务器上记录错误以便故障排除
		fmt.Println(err.Error())
		// 使用友好消息响应请求
		status, msg, _ := errors.HTTPStatusCodeMessage(err)
		webgo.SendError(w, msg, status)
		return
	}

	webgo.R200(w, "yay!")
}

func routes() []*webgo.Route {
	return []*webgo.Route{
		{
			Name:    "home",
			Method:  http.MethodGet,
			Pattern: "/",
			Handlers: []http.HandlerFunc{
				handler,
			},
		},
	}
}

func main() {
	router := webgo.NewRouter(&webgo.Config{
		Host:         "",
		Port:         "8080",
		ReadTimeout:  15 * time.Second,
		WriteTimeout: 60 * time.Second,
	}, routes()...)

	router.UseOnSpecialHandlers(accesslog.AccessLog)
	router.Use(accesslog.AccessLog)
	router.Start()
}

使用webgo来说明函数errors.HTTPStatusCodeMessage的用法。它返回适当的http状态码、存储的用户友好消息和布尔值。如果返回的错误是*Error类型,则布尔值为true。

一旦应用程序运行,您可以通过在浏览器中打开http://localhost:8080来检查响应。或者在终端上:

$ curl http://localhost:8080
{"errors":"we lost bar2!. bar2 was deceived by bar1 :(","status":500} // 输出

fmt.Println(err.Error())在stdout上生成的输出将是:

/Users/username/go/src/errorscheck/main.go:28 /Users/username/go/src/errorscheck/main.go:20 sinking bar

基准测试 [2025-07-03]

Macbook Air 13-inch, M3, 2024, 内存: 24 GB

$ go version
go version go1.24.4 darwin/arm64

$ go test -benchmem -bench .
goos: darwin
goarch: arm64
pkg: github.com/naughtygopher/errors
cpu: Apple M3
Benchmark_Internal-8                            	 3650916	       321.7 ns/op	    1104 B/op	       2 allocs/op
Benchmark_Internalf-8                           	 3155463	       378.9 ns/op	    1128 B/op	       3 allocs/op
Benchmark_InternalErr-8                         	 3866085	       312.2 ns/op	    1104 B/op	       2 allocs/op
Benchmark_InternalGetError-8                    	 1983544	       608.0 ns/op	    1576 B/op	       6 allocs/op
Benchmark_InternalGetErrorWithNestedError-8     	 2419369	       497.8 ns/op	    1592 B/op	       6 allocs/op
Benchmark_InternalGetMessage-8                  	 3815074	       316.1 ns/op	    1104 B/op	       2 allocs/op
Benchmark_InternalGetMessageWithNestedError-8   	 3470449	       342.2 ns/op	    1128 B/op	       3 allocs/op
Benchmark_HTTPStatusCodeMessage-8               	40540940	        29.12 ns/op	      16 B/op	       1 allocs/op
BenchmarkHasType-8                              	100000000	        11.44 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	github.com/naughtygopher/errors	13.805s

贡献

更多的错误类型、自定义、功能、多错误;欢迎PR和问题!

Gopher

这里使用的gopher是使用Gopherize.me创建的。像我们的gopher女士一样对Go错误表示一些爱!


更多关于golang内置错误处理增强与自定义错误类型插件库errors的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang内置错误处理增强与自定义错误类型插件库errors的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Golang 错误处理增强与自定义错误类型

Go 语言内置的错误处理机制相对简单,但通过标准库 errors 和第三方库如 pkg/errors,我们可以实现更强大的错误处理功能。下面我将介绍 Go 的错误处理增强方法和自定义错误类型的使用。

内置错误处理基础

Go 使用简单的 error 接口作为错误处理的基础:

type error interface {
    Error() string
}

基本用法:

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Result:", result)
}

自定义错误类型

我们可以创建更丰富的错误类型:

type MyError struct {
    Code    int
    Message string
    Op      string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("%s: code=%d, msg=%s", e.Op, e.Code, e.Message)
}

func process() error {
    return &MyError{
        Code:    500,
        Message: "internal server error",
        Op:      "process",
    }
}

func main() {
    err := process()
    if err != nil {
        fmt.Println("Error:", err)
        // 类型断言获取更多信息
        if me, ok := err.(*MyError); ok {
            fmt.Printf("Details: Code=%d, Op=%s\n", me.Code, me.Op)
        }
    }
}

使用 pkg/errors 增强错误处理

pkg/errors 提供了堆栈跟踪和错误包装功能:

import (
    "github.com/pkg/errors"
)

func readFile(path string) ([]byte, error) {
    data, err := ioutil.ReadFile(path)
    if err != nil {
        return nil, errors.Wrap(err, "read failed")
    }
    return data, nil
}

func main() {
    _, err := readFile("/nonexistent/file")
    if err != nil {
        // 打印带堆栈的错误信息
        fmt.Printf("%+v\n", err)
        
        // 获取原始错误
        original := errors.Cause(err)
        fmt.Println("Original error:", original)
    }
}

Go 1.13+ 的错误增强

Go 1.13 引入了错误包装和检查的新方法:

func processFile() error {
    if err := readConfig(); err != nil {
        return fmt.Errorf("config error: %w", err)
    }
    return nil
}

func main() {
    err := processFile()
    if err != nil {
        // 检查特定错误类型
        var configErr *ConfigError
        if errors.As(err, &configErr) {
            fmt.Println("Config error:", configErr)
        }
        
        // 解包错误链
        unwrapped := errors.Unwrap(err)
        fmt.Println("Unwrapped error:", unwrapped)
    }
}

最佳实践

  1. 错误定义:在包级别定义可导出的错误变量

    var (
        ErrNotFound = errors.New("not found")
        ErrInvalidInput = errors.New("invalid input")
    )
    
  2. 错误包装:使用 %w 包装底层错误,保留原始错误信息

  3. 错误检查:使用 errors.Iserrors.As 代替直接比较

  4. 添加上下文:在错误传递过程中添加有意义的上下文信息

  5. 日志记录:在错误处理的最上层记录完整的错误信息

完整示例

package main

import (
    "errors"
    "fmt"
    "os"
)

// 自定义错误类型
type ApiError struct {
    Status  int
    Message string
}

func (e *ApiError) Error() string {
    return fmt.Sprintf("API error %d: %s", e.Status, e.Message)
}

func fetchData(id string) (string, error) {
    if id == "" {
        return "", &ApiError{Status: 400, Message: "invalid ID"}
    }
    
    if id == "404" {
        return "", fmt.Errorf("data fetch: %w", os.ErrNotExist)
    }
    
    return "data for " + id, nil
}

func main() {
    data, err := fetchData("404")
    if err != nil {
        // 检查特定错误类型
        var apiErr *ApiError
        if errors.As(err, &apiErr) {
            fmt.Printf("API Error: Status=%d\n", apiErr.Status)
        }
        
        // 检查解包后的错误
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("Data not found")
        }
        
        fmt.Printf("Full error: %v\n", err)
        return
    }
    
    fmt.Println("Data:", data)
}

通过以上方法,你可以构建更健壮、更易于调试的 Go 应用程序错误处理系统。

回到顶部