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

huangapple go评论71阅读模式
英文:

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) {
  // 几乎与上面两个处理程序相同,只是调用的方法和参数类型不同
    ...
}

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

//stackoverflow不允许包含太多代码的问题,所以...这里有细节,那里有细节,还有更多细节...//

英文:

Currently I have too much repeated code for the handlers:

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

Is there any nicer way to write the handlers createGuestHandler, updateGuestHandler and getGuestHandler instead of repeating similar code blocks three times. I guess I can use interface but am not sure how to write that. I have about 20 handlers so the repeating code does not seem really maintainable.

//stackoverflow does not allow question with too much code over details so... details here, details there, even more details...//

答案1

得分: 4

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

假设你有以下类型和函数:

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)
    })
}
英文:

You can move the common logic to a separate function, and pass everything to it that is specific in each handler.

Let's assume you have these types and functions:

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
}

With generics allowed

If generics are allowed, you can factor all code out of handlers:

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

As you can see, all handler implementations are just a single line! We can even get rid of the handler functions now, as we can create a handler from a logic function (like CreateGuest(), UpdateGuest()).

This is how it would look like:

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

And using it:

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

Without generics

This solution does not use generics (and works with old Go versions too).

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

答案2

得分: 0

这里有很多避免重复的方法,例如,你可以使用装饰器模式,其中你可以定义如何解码/编码以及其他不涉及业务逻辑的步骤。

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

另一种是go-kit包(你可以在GitHub上查看),但我建议你先了解如何组合装饰器的思想,而不是直接安装该库,因为对于你的实现来说可能有点过度设计。

英文:

There are many ways to avoid repetition here, for example, you could use a decorator pattern, where you can define how to decode/encode and other steps that doesn't include your business logic.

You can check two interesting approaches:
One is from Mat: https://pace.dev/blog/2018/05/09/how-I-write-http-services-after-eight-years.html

The other one is the go-kit package (you can check it out on github), but I recommend you to checkout the idea on how to compose decorators instead of installing the library, could be an overkill for your implematation.

答案3

得分: 0

通常,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()应该执行什么操作)

英文:

Typically REST APIs have just /guest endpoint with single handler that decides what to do based on HTTP method:

  • POST to create
  • GET to retrieve
  • PUT to update the entire record
  • PATCH to update certain fields

You can look at r.Method inside your handler and decide what code to run based on that.

If you are bound to interface shown in your question you can e.g. wrap handler to an anonymous function with expected interface and make it accept an additional argument to decide what to do like this:

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

(where CREATE and UPDATE are some sort of flags that tell guestHandler() what it should do)

答案4

得分: 0

我有这些实用函数: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, &updateGuestReq)
	if err != nil {
		respondJson(w, nil, err)
		return
	}
    data, err := UpdateGuest(&updateGuestReq)
	respondJson(w, data, err)

}
英文:

I have these utility functions : decodeJsonBody, respondJson that I use to simplify response, without adding too much complexity. I wrap it in the Response struct for sending client side error details.

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)

}

答案5

得分: 0

我建议你去看一下go-kit。它主要用于使用六边形架构创建服务。它提供了许多实用函数,可以避免重复的代码,专注于业务逻辑。

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

示例也很容易跟随。

英文:

I suggest to have a look to go-kit.
It's mainly designed to create services using Hexagonal architecture. It brings a lot of utility functions to avoid repeated code and focus on the business logic.

It has a lot of functionality that may not need but since it's a toolkit (and not a complete framework) you're free to use only the parts that you need.

Examples are also easy to follow.

huangapple
  • 本文由 发表于 2022年5月11日 14:59:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/72196676.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定