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



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

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个处理程序,所以重复的代码看起来并不是真正可维护的。

//stackoverflow不允许在细节上有太多代码,所以…这里有细节,那里有细节,甚至更多细节…//

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

假设您具有以下类型和功能:

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)
})
}

这里有很多方法可以避免重复,例如,您可以使用decorator模式,在那里您可以定义如何解码/编码以及其他不包括业务逻辑的步骤。

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

另一个是go-kit包(你可以在github上查看它),但我建议你检查一下如何组成装饰器的想法,而不是安装库,这可能会对你的实现来说是一种过度。

通常REST API只有/guest端点,只有一个处理程序来决定基于HTTP方法做什么:

  • 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 %sn", 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)
}

最新更新