Golang默认HTTP函数有Bug吗? - Rachev


所有 Go 程序员都会很早就了解标准 HTTP 处理程序函数:

func hello(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello, World!"))
}

它简单易记。同时,它足够低级,不会以任何可能的方式限制或模糊开发人员——典型的 Go。

有什么问题?
让我们构建一个稍微复杂一点的示例,其中流程在返回客户端之前必须经过几个可能容易出错的操作。

func aMoreAdvancedHandler(w http.ResponseWriter, r *http.Request) {
    resOne, err := potentiallyDangerousOpOne()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    resTwo, err := potentiallyDangerousOpTwo(resOne)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }

    resThree, err := potentiallyDangerousOpThree(resTwo)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Write([]byte("Success: " + resThree))
}

在我看来,这里有一个大问题。你实际上并没有返回任何东西。不管结果如何,你只是把它写到了响应者writer。这可能会导致像上面的例子那样的难以察觉和难以追踪的错误。

resTwo, err := potentiallyDangerousOpTwo(resOne)
if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    // Did we forget to return here?
}

虽然我们处理了这个错误,但我们并没有在错误处理之后立即return。
根据定义,我们并没有被强制这样做,所以编译器也不会报错。
这可能会导致这样一种情况:尽管出现了错误,我们仍然告诉客户一切正常--要么就是其他形式的不一致的行为。

缺少的空返回有时被称为 "裸 "返回。它出现在所有C-风格的语言中,所以它不是Go语法的问题,但我很难证明使用裸返回是合理的。
但是,只要我能够,我尽量会让函数总是返回一个值,或者如果它在堆栈中的位置足够高(例如main),就会使它退出整个应用程序。
不幸的是,这种习惯并不是其他人的想法,这也是我提出这一点的原因。
目前存在众多的Go的HTTP框架,但最流行的框架之一--Gin似乎也在遵循同样的风格。


func (h *Handlers) getAlbumGin(c gin.Context) {
    id, ok := c.Params.Get("id")

    if !ok {
        c.Error(fmt.Errorf(
"get album: missing id"))
        return
    }

    album, err := h.db.FetchAlbum(id)
    if err != nil {
        c.Error(fmt.Errorf(
"fetch album with id %s: %w", id, err))
        
// Same thing here. Oh, man ‍
    }

    cover, err := h.db.FetchAlbumCover(id)
    if err != nil {
        c.Error(fmt.Errorf(
"fetch cover for album with id %s: %w", id, err))
        return
    }

    c.JSON(http.StatusOK, gin.H{
"album": album, "cover": cover})
}

解决方案
为了比较,这是我最喜欢的Echo处理同一主题的方式。

func (h *Handlers) getAlbum(c echo.Context) error {
    // NOTE the explicit err return value here 

    id := c.QueryParam(
"id")
    if id ==
"" {
        return fmt.Errorf(
"get album: missing id")
    }

    album, err := h.db.FetchAlbum(id)
    if err != nil {
        return fmt.Errorf(
"fetch album with id %s: %w", id, err)
    }

    cover, err := h.db.FetchAlbumCover(id)
    if err != nil {
        return fmt.Errorf(
"fetch cover for album with id %s: %w", id, err)
    }

    return c.JSON(http.StatusOK, echo.Map{
"album": album, "cover": cover})
}

通过强制显式返回错误,Echo 将意外忘记返回错误的可能性降到最低。
除非开发人员明确地忽略该错误,否则他们将不得不对其进行处理;否则,由于未使用的变量,编译器将无法编译。
最简单的方法是return一些东西,这将向 Echo 发出信号,向 HTTP 响应编写器写入适当的消息和状态代码。

我们可以在没有框架的情况下做同样的事情吗?当然,这很容易做到,但正如您在 Echo 示例中所见,它需要对标准 HTTP 处理程序函数进行轻微更改。这将要求我们将编写器抽象出来,而是以常规函数的方式返回值。

type MyResponseWriter struct {
    w http.ResponseWriter
}

func (rw *MyResponseWriter) WriteString(str string) error {
    _, err := rw.w.Write([]byte(str))
    return err
}

// You can potentiall extend MyResponseWriter with indefinite helper methods


type MyHandlerFunc func(w *MyResponseWriter, r *http.Request) error

// This is how a new handler would look like
func someHandler(w *MyResponseWriter, r *http.Request) error {
    res, err := potentiallyDangerousOp()
    if err != nil {
        return fmt.Errorf(
"someHandler: %w", err)
    }

    return w.WriteString(res)
}

// And this is what we will use to bridge the gap with the existing http package
func MakeHTTPHandler(fn MyHandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        rw := MyResponseWriter{w: w}
        err := fn(&rw, r)
        if err != nil {
            
// Note that this is the only place where we actually use
            
// the original writer's error reporting capabilities
            http.Error(w, err.Error(), http.StatusInternalServerError)
        }
    }
}