基于Golang的恐慌处理框架QIWI探讨

基于Golang的恐慌处理框架QIWI探讨 作为一名后端开发人员,我最近在公司里一直在深入研究 NestJS。其中一个让我着迷的特性是能够抛出像 throw new AppBadRequest() 这样的异常来处理 HTTP 消息。能够在代码中中断整个 HTTP 请求并返回 HTTP 消息,这真是太棒了。

然而,当我再次用 Go 语言编码时,我一直很怀念这个功能。我不知道是否已经存在这样的框架,但我已经构建了自己的框架,可以参考这个示例项目:GitHub - dalton02/Qiwi-Base-Backend: 使用 Qiwi 框架和 Prisma 的后端服务基础结构

实际上,它包含一个我正在做的大学博客项目。该框架使用反射器实现了一个 JSON 验证器,并且有一些很酷的功能,比如针对 JWT 的自定义保护路由和公共路由。

之后,我考虑将其扩展得更具可定制性,如果你们能给我一些建议,那就太好了。整个框架都在 “requester” 文件夹中。

示例实现

func InsertPost(db *sql.DB, postagem postagensDto.NovaPostagem, response http.ResponseWriter) int {
    var lastInsertID int
    err := db.QueryRow("INSERT INTO postagem (tipo, titulo, conteudo, usuario_id, tags) VALUES ($1, $2, $3, $4, $5) RETURNING id",
        postagem.Tipo, postagem.Titulo, postagem.Conteudo, postagem.UsuarioId, pq.Array(postagem.Tags)).Scan(&lastInsertID)
    
    if err != nil {
        httpkit.AppConflict("A conflict has occurred: "+err.Error(), response)
        // If this function is called in high order functions, the panic will go all the way back to the original http.handleFunc
    }
    return int(lastInsertID)
}

与 NestJS 的对比

import { Injectable, ConflictException } from '@nestjs/common';

@Injectable()
export class PostService {
    async insertPost(postagem: NovaPostagem): Promise<number> {
        try {
            const result = await this.postRepository.insert(postagem);
            return result.identifiers[0].id; // Returning the ID of the last post
        } catch (error) {
            throw new ConflictException('A conflict has occurred: ' + error.message);
        }
    }
}

注意:如果代码库中有些内容是葡萄牙语,请见谅。


更多关于基于Golang的恐慌处理框架QIWI探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html

18 回复

实际上我确实没看过那个,哈哈。我一直在学习 Go 中更复杂的东西,忘了去研究基础内容。谢谢大家。

更多关于基于Golang的恐慌处理框架QIWI探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


正如他所说,并且正如最近所讨论的,Golang的错误处理非常有趣,如果你习惯了这种代码风格,你会爱上它的。

抱歉,我的措辞可能过于直白,也许是翻译的问题。我想表达的意思是,做某些事情太简单了(比如?我想不出更合适的词)。

如果让你觉得这很蠢,我表示抱歉。这在我正在开发的公司项目中是可行的,并且对我来说非常地道,我只是想分享一下,看看是否有人喜欢这种方法,伙计,放轻松…

你是独自负责这个项目,还是这是你们团队的解决方案?这个设计是否经过讨论?除了节省代码行数之外,你能给出任何充分的理由来说明为什么应该使用它而不是错误处理吗?

这看起来实际上是在 Go 语言中重新创建 try/catch 机制,这直接违背了其核心原则之一:将错误视为值。如果我想抛出异常,我会去写 Python 代码。经过这么多年使用 Go,我希望编写 err != nil 并从函数中获取这个 err 作为最后一个返回值。

这根本不会让代码变得清晰。所以你只是在库的某个地方引发恐慌然后恢复?因为我看到的只是一堆令人困惑的导出函数(甚至不是方法),它们不简单地返回错误,而是引发恐慌。这看起来并不好,感觉不对,就是不对。当有人说他测试了边界情况时,那意味着仍然有无数种方法可以破坏一切。

我认为“愚蠢”这个词在这里既粗鲁又过于强烈。我同意这不符合惯用法,并且我喜欢 Go 将错误视为值的处理方式。但有很多框架/语言/语言设计者更喜欢其他范式。我并不认为强行将这些范式塞进 Go 是件好事(就像我们将错误视为值,这正是让这门语言与众不同的特点之一!),但请尽量不要对新社区成员无礼。

有些人并非以英语为母语,有时他们的表达可能听起来有些粗鲁,但这背后并无恶意。

除此之外,我并不认为错误处理已经成为某种范式。它是语言设计的一部分,并且可能存在最佳实践。这不仅仅适用于Go语言。同时,当人们在这个论坛上发帖寻求反馈时,他们应该准备好接受反馈。我希望看到他们为什么这样做以及为什么它比其他方法更好的清晰理由。否则,我将根据我的经验和知识提出反驳。

显然,我们无法阻止开发者做出愚蠢的行为。据我观察,Go语言的设计如此简洁迷人,以至于让一些人产生了奇怪的想法。

当他们认为最佳实践是错误的时候,他们不会觉得自己的思路有问题,除非他们遭遇了糟糕的后果。

panic 仅在 defer 中被捕获并执行一些逻辑,这导致在编写会 panic 但未被捕获的代码时发生泄漏。

让调用者知道将会发生一个错误,远比隐式地 panic 要好得多。

errEmail、errLogin、errSomethingElse 这些不都是错误接口吗? 通常的处理方式如下:

data, err := accountservice.CreateAccount(login,email,password)
if err!=nil{
   if errors.Is(err,errEmail){
        // do errEmail ...
   }
   if errors.Is(err,errLogin){
        // do errLogin ...
   }
   if errors.Is(err,errSomethingElse){
        // do errSomethingElse ...
   }
}

这只是一个简单的例子,具体的错误处理可能会在更高层级被调用。

我是说,我真的很喜欢 Go 处理错误的方式。我甚至在 TypeScript 中也使用了一些 Go 的语法,我知道你们讨厌使用 panic,但我已经测试了所有的边界情况,并且以每秒大约 10,000 个请求的速度进行了测试,看看它是否会影响性能。我只是觉得我采用的这种专门用于发送 HTTP 消息的方法还挺不错的。我只用这种方法来向客户端发送早期消息。如果你看到我创建的结构体,每个 panic 都会被恢复并记录日志。我甚至不是说它比原来的方式更好;我只是展示一种很酷的替代方案,它能让代码在某些方面变得清晰且简短。

某种程度上是这样,但为了遵循 Go 语言的惯例,我可以在控制器中直接返回 HTTP 消息,并保持 panic 恢复机制仅用于返回通常的“内部服务器错误”。唯一有点麻烦的是,在我的服务层,我可能需要返回多个错误变量来发送不同的 HTTP 消息。例如,在一个尝试为你创建新账户的服务中,我需要返回不同的错误来对应:“邮箱已存在于数据库中”、“登录名已存在于数据库中”等情况。我的意思是,我可能会返回类似这样的东西:

data, errEmail, errLogin, errSomethingElse  := accountservice.CreateAccount(login,email,password)

但我想这就是方法,如果其他人要使用它的话。除此之外,我还没有发现 panic 本身会导致内存泄漏之类的问题。感谢你的反馈。

我曾经历过糟糕的情况,由于到处发生 panic 以及 defer 遗漏某些内容导致泄漏,调试变得异常困难。通过良好的错误传递,调用方和提供方可以很好地协同工作。

此外,调用方不喜欢每个函数调用都隐藏着 panic 这一事实。但如果你不明确说明将会发生什么,那么调试就会面临巨大障碍,更不用说可能泄漏的风险了。

http.handler 的 recover 被设计为一种保险措施,以防发生意外的 panic 导致主要服务中断。当这种保险措施被主动触发时,是非常不寻常的事情。如果你只是想要调用栈,你应该调用相关函数而不是使用 panic。

最后,如果我之前说的话冒犯了你,我表示歉意。

但我也认为,如果不遵循最佳实践,就不是一种好的学习方式。或者说,“吃一堑长一智”才会让人注意到一些问题。

探讨一种更地道的实现方法,仅供参考:

// 将所有错误包装在包含状态码的 HTTPError 中
type HTTPError struct {
    err error
    status_code int
}

func BadRequest(err error) *HTTPError {
    return &HTTPError{err, http.StatusBadRequest}
}
func Conflict ...
func Forbidden ...
func NotFound ...
...

func SomeAPIEndpoint(w http.ResponseWriter, r *http.Request) *HTTPError {
    err := DBOperationWrapper()
    if err != nil {
        return err
    }
    ...
    if err != nil {
        return BadRequest(errors.New("Bad data received"))
    }
    return nil
}

func DBOperationWrapper() *HTTPError {
    ...
    if conflict_occurred {
        return Conflict(errors.New("A conflict has occurred"))
    }
    ...
    return nil
}

最近对此有一些讨论,我就不再赘述了,但总的来说,不要使用 panic。查看此主题的回复以获取更多信息:

I implemented Rust’s unwrap() in Go, feedback asked

我几个月前开始用 Go 编码,现在我开始对这个语言感到得心应手,于是我编译了一些实用函数并上传到了 Github。其中之一是对 Rust 的 unwrap() 的重新实现,旨在提供更便捷的错误处理。以下是包含所有解释和示例的文档:g package - github.com/n-mou/yagul/g - Go Packages 但长话短说:

import (
    "os"
    "github.com/n-mou/yagul/itertools"
)

// 这个:
fileInfo := g.Unwrap(os.Stat("."))

// 是 e…

等等,什么?为什么?Go 中的 error 是一个接口。它拥有 Error() string 方法,同样支持包装/解包。好吧,如果你想发送不同的状态码,这没问题,而且相当明显。但你可以将请求处理的逻辑拆分,例如,将与错误请求相关的错误返回的函数放在一个地方,将内部错误相关的放在另一个地方。或者你可以使用 errors 包来获得更大的灵活性。例如:

package handler

import (
    "errors"
    "fmt"
    "net/http"
)

// 根据需要定义任意数量的自定义错误...
// 它可以是一个简单的字符串,甚至是一个更复杂的结构体。
var (
    ErrMailExists    = errors.New("email already exists")
    ErrLoginExists   = errors.New("login alrady exists")
    ErrSomethingElse = errors.New("something else")
)

func Handler(w http.ResponseWriter, r *http.Request) {
    if err := createUser(r); err != nil {
        code := http.StatusBadRequest

        // 根据你的逻辑,可以是一个 switch 语句、
        // 一次性检查或任何你偏好的方式...
        if errors.Is(err, ErrSomethingElse) {
            code = http.StatusInternalServerError
        }

        http.Error(w, err.Error(), code)
        return
    }

    w.WriteHeader(http.StatusOK)
}

func createUser(r *http.Request) error {
    var login, email string
    // 从请求中提取值...

    // 执行你需要的任何逻辑,返回你想要的任何错误。
    if inDb(login) {
        return fmt.Errorf("login %q: %w", login, ErrLoginExists)
    }

    if inDb(email) {
        return fmt.Errorf("email %q: %w", email, ErrMailExists)
    }

    if err := addToDb(login, email); err != nil {
        return fmt.Errorf("cannot add %s/%s into db: %w", login, email, err)
    }

    return nil
}

func inDb(s string) bool {
    // 检查是否存在...
    return false
}

func addToDb(login, email string) error {
    // 你可以在这里使用数据库连接,并在出现错误时包装错误...
    return ErrSomethingElse
}

关于QIWI恐慌处理框架的专业分析

从你的描述来看,QIWI框架实现了一种类似NestJS的异常处理机制,这在Go社区中确实是一个有趣的探索方向。让我从专业角度分析这种实现方式。

核心机制分析

你的框架通过panic()recover()实现了异常传播机制,这类似于NestJS的异常过滤器。以下是典型的实现模式:

// 中间件中的全局恢复机制
func PanicRecoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                // 处理不同类型的panic
                switch e := err.(type) {
                case *AppBadRequest:
                    w.WriteHeader(http.StatusBadRequest)
                    json.NewEncoder(w).Encode(e)
                case *AppConflict:
                    w.WriteHeader(http.StatusConflict)
                    json.NewEncoder(w).Encode(e)
                case *AppUnauthorized:
                    w.WriteHeader(http.StatusUnauthorized)
                    json.NewEncoder(w).Encode(e)
                default:
                    // 未知panic,返回500错误
                    w.WriteHeader(http.StatusInternalServerError)
                    json.NewEncoder(w).Encode(map[string]string{
                        "error": "Internal server error",
                    })
                }
            }
        }()
        next.ServeHTTP(w, r)
    })
}

异常类型定义示例

// 定义标准化的应用异常
type AppError struct {
    StatusCode int    `json:"statusCode"`
    Message    string `json:"message"`
    Error      string `json:"error"`
    Timestamp  string `json:"timestamp"`
}

func NewAppError(statusCode int, message string) *AppError {
    return &AppError{
        StatusCode: statusCode,
        Message:    message,
        Error:      http.StatusText(statusCode),
        Timestamp:  time.Now().Format(time.RFC3339),
    }
}

// 具体异常类型
func AppBadRequest(message string) {
    panic(NewAppError(http.StatusBadRequest, message))
}

func AppConflict(message string) {
    panic(NewAppError(http.StatusConflict, message))
}

func AppUnauthorized(message string) {
    panic(NewAppError(http.StatusUnauthorized, message))
}

验证器集成示例

// 基于反射的JSON验证器
type Validator struct{}

func (v *Validator) Validate(schema interface{}, data []byte) error {
    // 使用反射验证数据结构
    target := reflect.New(reflect.TypeOf(schema)).Interface()
    
    if err := json.Unmarshal(data, target); err != nil {
        return fmt.Errorf("validation failed: %w", err)
    }
    
    // 自定义验证逻辑
    if err := validateStruct(target); err != nil {
        return err
    }
    
    return nil
}

// 在处理器中使用
func CreatePostHandler(w http.ResponseWriter, r *http.Request) {
    var post PostDTO
    validator := &Validator{}
    
    body, _ := io.ReadAll(r.Body)
    if err := validator.Validate(PostDTO{}, body); err != nil {
        AppBadRequest(err.Error())
    }
    
    // 处理业务逻辑
    // ...
}

路由保护机制

// JWT保护装饰器
func Protected(handler http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            AppUnauthorized("Missing authorization token")
        }
        
        // 验证JWT
        claims, err := ValidateJWT(token)
        if err != nil {
            AppUnauthorized("Invalid token")
        }
        
        // 将claims存入上下文
        ctx := context.WithValue(r.Context(), "user", claims)
        handler(w, r.WithContext(ctx))
    }
}

// 使用示例
router.HandleFunc("/posts", Protected(createPostHandler)).Methods("POST")

数据库层集成

// 数据库操作包装器
type DBExecutor struct {
    db *sql.DB
}

func (e *DBExecutor) ExecWithErrorHandling(query string, args ...interface{}) (sql.Result, error) {
    result, err := e.db.Exec(query, args...)
    if err != nil {
        // 根据错误类型抛出相应异常
        if strings.Contains(err.Error(), "duplicate") {
            AppConflict("Duplicate entry: " + err.Error())
        } else if strings.Contains(err.Error(), "foreign key") {
            AppBadRequest("Foreign key violation: " + err.Error())
        } else {
            AppInternalError("Database error: " + err.Error())
        }
    }
    return result, nil
}

性能考虑

使用panic()/recover()机制需要注意性能影响。在Go中,panic比普通的错误返回开销更大。对于高性能场景,建议:

// 性能优化的错误处理
func InsertPostOptimized(db *sql.DB, post PostDTO) (int, error) {
    var id int
    err := db.QueryRow(query, post.Tipo, post.Titulo).Scan(&id)
    
    if err != nil {
        // 返回错误而不是panic,让上层决定如何处理
        return 0, &AppError{
            StatusCode: http.StatusConflict,
            Message:    "Insert failed: " + err.Error(),
        }
    }
    return id, nil
}

中间件链示例

// 完整的中间件链配置
func NewRouter() *mux.Router {
    router := mux.NewRouter()
    
    // 应用中间件链
    router.Use(PanicRecoveryMiddleware)
    router.Use(LoggingMiddleware)
    router.Use(CORSMiddleware)
    
    // 路由定义
    api := router.PathPrefix("/api").Subrouter()
    api.Use(AuthMiddleware)
    
    api.HandleFunc("/posts", createPostHandler).Methods("POST")
    api.HandleFunc("/posts/{id}", getPostHandler).Methods("GET")
    
    return router
}

这种基于panic的异常处理框架在Go中确实提供了类似NestJS的开发体验,但需要注意Go语言的哲学更倾向于显式的错误处理。框架的实现需要在开发便利性和语言习惯之间找到平衡点。

回到顶部