Golang 错误处理和日志

25-05-11 编程 #code #golang

Go 语言的错误处理和日志规范。

error handling

error 变量或者类型

package myerrors

import "errors"
// Err 前缀
var ErrNotFound = errors.New("not found")

// 或者自定义一个错误类型 实现 Error() 方法用于 error wrap
// 一般是 Error 结尾
type ValidationError struct {
    Field string
    Msg   string
}

func (v *ValidationError) Error() string {
    return "validation failed: field=" + v.Field + ", msg=" + v.Msg
}

error 包装

// 使用全局 error 变量
func Svc1() error {
    return myerrors.ErrNotFound
}
// 或者使用上面自定义错误类型
func Svc1() error {
    return &myerrors.ValidationError{
        Field: "email",
        Msg:   "invalid format",
    }
    
}
// wrap error
func CallSvc1() {
    err := Svc1()
    if err != nil {
        // 包装原始错误,保留错误链
        return fmt.Errorf("B failed: %w", err)
    }
    return nil
}

errors.Is

import (
    "errors"
    "fmt"
    "myapp/myerrors"
)

func main() {
    err := CallSvc1()
    if err != nil {
        // 这里会一直 unwrap 到匹配或者最后
        if errors.Is(err, myerrors.ErrNotFound) {
            fmt.Println("handle not found error specifically")
        } else {
            fmt.Println("generic error:", err)
        }
    }
}

errors.Join

func cleanup() error {
    var errs []error

    if err := closeFile(); err != nil {
        errs = append(errs, err)
    }
    if err := closeDB(); err != nil {
        errs = append(errs, err)
    }
    if err := closeNetwork(); err != nil {
        errs = append(errs, err)
    }

    return errors.Join(errs...) // 返回合并后的错误
}

// 可以使用 errors.Is
func main() {
    err := doSomething()

    fmt.Println(errors.Is(err, ErrA)) // true
    fmt.Println(errors.Is(err, ErrB)) // true
}

errors.As

type MyError struct {
    Code int
    Msg  string
}

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

func doSomething() error {
    return fmt.Errorf("wrapper: %w", &MyError{Code: 404, Msg: "not found"})
}

func main() {
    err := doSomething()

    var myErr *MyError
    if errors.As(err, &myErr) {
        fmt.Println("Matched MyError, code:", myErr.Code)
    } else {
        fmt.Println("Not a MyError")
    }
}

errors.As(err, &myErr)将 err 转换成 myErr 类型并赋值给 myErr,如果成功,则可以使用 myErr,否则 myErr 为 nil,执行 else 逻辑。

if err styles

// 这是最常见、最推荐的写法,清晰简洁。
if err != nil {
    return err
}

// result 和 err 作用域只在 if 内部
if result, err := doSomething(); err != nil {
    return err
}

logging

go 1.21 有了 slog 之后,日志可以统一使用 slog 即可。下面是一个包含 ctx 实现链路日志的示例

// 日志初始化
import (
	"log/slog"
	"os"
)

func initLogger() *slog.Logger {
	handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	})
	logger := slog.New(handler)
	slog.SetDefault(logger)
	return logger
}

// 注入 trace_id
import (
	"context"
	"github.com/google/uuid"
	"log/slog"
	"net/http"
)

func RequestContextMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		reqID := uuid.New().String()

		logger := slog.With("request_id", reqID)
		ctx := slog.NewContext(r.Context(), logger)

		// 将 context 传给下一个 handler
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// 在业务逻辑中使用 slogger
func handleLogin(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()
	logger := slog.FromContext(ctx)

	logger.Info("user login", slog.String("user", "alice"))

	// or shorter:
	slog.InfoContext(ctx, "auth success", slog.String("user", "alice"))
}

其他 logging 资料: