Few days ago Go 1.18beta1 was released and with it the first official generics support. I was embarrassed with standard Go’s HTTP handler functions for quite a while. If you are not familiar with Go, you probably wonder why, but if you are familiar, I believe you know. For example, implementing a RESTful API using idiomatic Go requires lot of code repetition in order to JSON decode request bodies and JSON encode response bodies. This problem was possible to solve in some way but never in such elegant way like using generics.
First, let’s take a look how Go’s idiomatic HTTP handler functions look like: func(res http.ResponseWriter, req *http.Request)
res
is just an interface providing several methods to set response and req
is a concrete implementation of Go’s HTTP request. In order to get path or query parameters, you have to play around with req, but let’s forget now about that. Even worse is that in your RESTful API you don’t get concrete decoded JSON request body as concrete type, but instead you have to parse it every time. Then at the end of the request you have do encode your data to JSON response body again. So, lot’s of encoding and decoding which can’t be hidden in an easy way, at least not without generics. It’s not the point to hide these steps, but to focus to business logic and not to irrelevant tasks like this. When I say hide it’s not about magic, but about having a framework which does this for you so you can focus to the business login. If you write lot of API’s in Go, you probably realize this without my explanation. It’s so boring to handle JSON encoding/decoding and possible errors. Let’s focus to the real stuff.
In order to overcome this, I made
gap
today, small package which provides a wrapper over custom gap’s HTTP handler functions providing idiomatic HTTP functions at the end. So, it can be used with Go’s standard library HTTP implementation. gap’s handler functions accept custom request with concrete type and also custom response with concrete data type. Here is the signature: func(*gap.Request[I]) *gap.Response[O]
. Generic type I
here is the type of the JSON request body and O
is the type of the data returned within the gap.Response
. Besides the data, this custom response contains also HTTP status code and errors that may have ocurred. So, your custom handler would become concrete request type as input and you may return concrete response type as output. And the render function of gap
would take care to send the JSON response to the client.
But, let’s jump into coding, then it will be more clear what is this about. Here is a gap’s handler example:
|
|
Because our helloHandler is GET endpoint, our concrete request body will be just struct{}
, so no body. However, our response will contain data of hello type. What does it mean? Here is the gap.Response
type:
|
|
So, *gap.Response[hello]
in the snippet above means that we are returning *gap.Response
type with data field of type hello
, which is private type in this case. In order to use our helloHandler
as standard Go’s HTTP handler, it’s enough just to wrap it using gap.Wrap
. Besides the custom HTTP handler function, gap.Wrap
requires a logger with
lax.Logger
interface, but this is not relevant to this article, let’s focus on the API. Here is how wrapping looks like:
|
|
Our handler function will be triggered on /hello
path, will receive no body (struct{}
) and will send gap.Response
with gap.Response.Data
of type hello
(concrete private type in this case, but can be exported as well). Both JSON request body decoding and JSON response body encoding will be done by gap
package.
For further documentation how to use this package, please check this test and this example test . At the end I would like to mention that this is project is in a very early stage and the API may suffer lot of modifications in the future. So, gap should not be used in production until the first release, but it can show the power of Go’s generics and give you ideas how to improve Go’s RESTful API’s further. Feel free to contribute to the project.