在 Go 中制作 REST 处理程序的有效方法(无需重复代码)?

Efficient way to make REST handlers in Go (without repeating code)?

目前我有太多处理程序的重复代码:

type GuestMux struct {
  http.ServeMux
}

func main() {
    guestMux := NewGuestMux()
    http.ListenAndServe(":3001", guestMux)
}

func NewGuestMux() *GuestMux {
    var guestMux = &GuestMux{}
    guestMux.HandleFunc("/guest/createguest", createGuestHandler)
    guestMux.HandleFunc("/guest/updateguest", updateGuestHandler)
    guestMux.HandleFunc("/guest/getguest", getGuestHandler)

    return guestMux
}

func createGuestHandler(w http.ResponseWriter, r *http.Request) {
  var createGuestReq CreateGuestRequest
  reqBody, err := ioutil.ReadAll(r.Body)
  if err != nil {
    log.Println(err)
    w.WriteHeader(http.StatusInternalServerError)
    return
  }
  err = json.Unmarshal(reqBody, &createGuestReq)
  if err != nil {
    log.Println(err)
    w.WriteHeader(http.StatusBadRequest)
    return
  }
  resp, err := CreateGuest(&createGuestReq)
  if err != nil {
    log.Println(err)
    w.WriteHeader(http.StatusInternalServerError)
    return
  }
  w.Header().Set("Content-Type", "application/json")
  json.NewEncoder(w).Encode(resp)
}

func updateGuestHandler(w http.ResponseWriter, r *http.Request) {
  var updateGuestReq UpdateGuestRequest
  reqBody, err := ioutil.ReadAll(r.Body)
  if err != nil {
    log.Println(err)
    w.WriteHeader(http.StatusInternalServerError)
    return
  }
  err = json.Unmarshal(reqBody, &updateGuestReq)
  if err != nil {
    log.Println(err)
    w.WriteHeader(http.StatusBadRequest)
    return
  }
  resp, err := UpdateGuest(&updateGuestReq)
  if err != nil {
    log.Println(err)
    w.WriteHeader(http.StatusInternalServerError)
    return
  }
  w.Header().Set("Content-Type", "application/json")
  json.NewEncoder(w).Encode(resp)
}

func getGuestHandler(w http.ResponseWriter, r *http.Request) {
  // almost the same as above two handlers, just different method to call and 
    // its parameter type
    ...
}

有没有更好的方法来编写处理程序 createGuestHandlerupdateGuestHandlergetGuestHandler 而不是重复类似的代码块三次。我想我可以使用 interface 但我不确定如何写。我有大约 20 个处理程序,所以重复的代码似乎不太容易维护。

//Whosebug 不允许在细节上使用过多代码的问题,所以...这里有细节,那里有细节,甚至更多细节...//

您可以将通用逻辑移至单独的函数,并将每个处理程序中特定的所有内容传递给它。

假设您有这些类型和函数:

type CreateGuestRequest struct{}
type UpdateGuestRequest struct{}
type CreateGuestResponse struct{}
type UpdateGuestResponse struct{}

func CreateGuest(v *CreateGuestRequest) (resp *CreateGuestResponse, err error) {
    return nil, nil
}

func UpdateGuest(v *UpdateGuestRequest) (resp *UpdateGuestResponse, err error) {
    return nil, nil
}

允许使用泛型

如果允许使用泛型,您可以从处理程序中提取 所有 代码:

func handle[Req any, Resp any](w http.ResponseWriter, r *http.Request, logicFunc func(dst Req) (Resp, error)) {
    var dst Req
    if err := json.NewDecoder(r.Body).Decode(&dst); err != nil {
        log.Printf("Decoding body failed: %v", err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    resp, err := logicFunc(dst)
    if err != nil {
        log.Println(err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    if err := json.NewEncoder(w).Encode(resp); err != nil {
        log.Printf("Encoding response failed: %v", err)
    }
}

func createGuestHandler(w http.ResponseWriter, r *http.Request) {
    handle(w, r, CreateGuest)
}

func updateGuestHandler(w http.ResponseWriter, r *http.Request) {
    handle(w, r, UpdateGuest)
}

如您所见,所有处理程序实现都只是一行!我们现在甚至可以摆脱处理函数,因为我们可以从逻辑函数创建处理函数(如 CreateGuest()UpdateGuest())。

这是它的样子:

func createHandler[Req any, Resp any](logicFunc func(dst Req) (Resp, error)) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var dst Req
        if err := json.NewDecoder(r.Body).Decode(&dst); err != nil {
            log.Printf("Decoding body failed: %v", err)
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
        resp, err := logicFunc(dst)
        if err != nil {
            log.Println(err)
            w.WriteHeader(http.StatusInternalServerError)
            return
        }
        w.Header().Set("Content-Type", "application/json")
        if err := json.NewEncoder(w).Encode(resp); err != nil {
            log.Printf("Encoding response failed: %v", err)
        }
    }
}

并使用它:

func NewGuestMux() *GuestMux {
    var guestMux = &GuestMux{}
    guestMux.HandleFunc("/guest/createguest", createHandler(CreateGuest))
    guestMux.HandleFunc("/guest/updateguest", createHandler(UpdateGuest))

    return guestMux
}

没有泛型

此解决方案不使用泛型(也适用于旧的 Go 版本)。

func handle(w http.ResponseWriter, r *http.Request, dst interface{}, logicFunc func() (interface{}, error)) {
    if err := json.NewDecoder(r.Body).Decode(dst); err != nil {
        log.Printf("Decoding body failed: %v", err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    resp, err := logicFunc()
    if err != nil {
        log.Println(err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    if err := json.NewEncoder(w).Encode(resp); err != nil {
        log.Printf("Encoding response failed: %v", err)
    }
}

func createGuestHandler(w http.ResponseWriter, r *http.Request) {
    var createGuestReq CreateGuestRequest
    handle(w, r, &createGuestReq, func() (interface{}, error) {
        return CreateGuest(&createGuestReq)
    })
}

func updateGuestHandler(w http.ResponseWriter, r *http.Request) {
    var updateGuestReq UpdateGuestRequest
    handle(w, r, &updateGuestReq, func() (interface{}, error) {
        return UpdateGuest(&updateGuestReq)
    })
}

这里有很多方法可以避免重复,例如,您可以使用装饰器模式,您可以在其中定义如何 decode/encode 以及其他不包含您的业务逻辑的步骤。

您可以检查两种有趣的方法: 一个来自 Mat:https://pace.dev/blog/2018/05/09/how-I-write-http-services-after-eight-years.html

另一个是 go-kit 包(你可以在 github 上查看),但我建议你查看关于如何编写装饰器的想法而不是安装库,可以对您的实施来说太过分了。

通常 REST API 只有 /guest 端点和单个处理程序,根据 HTTP method:

决定要做什么
  • POST 创建
  • GET 检索
  • PUT更新整条记录
  • PATCH 更新某些字段

您可以在您的处理程序中查看 r.Method 并根据此决定要 运行 的代码。

如果您绑定到问题中显示的界面,您可以例如将处理程序包装到具有预期接口的匿名函数,并使其接受一个额外的参数来决定要做什么:

guestMux.HandleFunc("/guest/createguest", func(w http.ResponseWriter, r *http.Request) {
      guestHandler(r, w, CREATE)
})
guestMux.HandleFunc("/guest/updateguest", func(w http.ResponseWriter, r *http.Request) {
      guestHandler(r, w, UPDATE)
})
...

(其中 CREATE 和 UPDATE 是某种标志,告诉 guestHandler() 它应该做什么)

我有这些实用函数:decodeJsonBodyrespondJson,我用它们来简化响应,而不增加太多复杂性。我将其包装在 Response 结构中,用于发送客户端错误详细信息。

type Response struct {
    Data   interface{} `json:"data"`
    Errors interface{} `json:"errors"`
}

func respondJson(w http.ResponseWriter, data interface{}, err error) {
    w.Header().Set("Content-Type", "application/json")
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        err = json.NewEncoder(w).Encode(Response{
            Errors: err.Error(),
        })
        return
    }
    err = json.NewEncoder(w).Encode(Response{
        Data: data,
    })
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        log.Printf("http handler failed to convert response to json %s\n", err)
    }
}

func decodeJsonBody(r *http.Request, v interface{}) error {
    decoder := json.NewDecoder(r.Body)
    return decoder.Decode(v)
}

func updateGuestHandler(w http.ResponseWriter, r *http.Request) {
    var updateGuestReq UpdateGuestRequest
    err := decodeJsonBody(r, &updeateGuestReq)
    if err != nil {
        respondJson(w, nil, err)
        return
    }
    data, err := UpdateGuest(&updateGuestReq)
    respondJson(w, data, err)

}

建议去go-kit看看。 它主要设计用于使用六边形架构创建服务。它带来了很多实用功能来避免重复代码并专注于业务逻辑。

它有很多可能不需要的功能,但由于它是一个工具包(而不是一个完整的框架),您可以自由地只使用您需要的部分。

示例也很容易理解。