Go Generics Example
Go 1.18 will be adding Go’s version of generics pretty soon. I wanted to get a feel for how I might use them. Read on for a concrete example.
I wanted to get some hands on experience with generics to understand what would or wouldn’t work so I built a little web app that has a separate component for serialization and the actual handler.
Typically when I write an http handler in Go, the general pattern is:
func (x X) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
var i inputType
if err := json.NewDecoder(r.Body).Decode(&i); err != nil {
rw.WriteHeader(400)
// maybe log here
return
}
var o outputType
// meat of the function is here, populates o
if err := json.NewEncoder(rw).Encode(o); err !- nil {
rw.WriteHeader(500) // probably too late
// definitely log here
return
}
}
The code above is almost exclusively boilerplate to handle serialization. I write up an example of how to translate this to generics.
Before I show the code let me describe some (not all) of the foundational bits of generics in Go.
First, and most basic, is that any
becomes an alias for interface{}
. This is pure sugar
but makes the code much less noisy for a pretty common case.
Next, we have the opportunity to include parameter lists in our type definitions. The gist is you can do something like:
type Reducer[T any] interface {
reduce func (a, b T) T
}
The square brackets are where you put one or more new parameters in your type. The parameters
end up used in the body of the type (or function or method or whatever.) I used any
above
but you can use other interfaces there too.
I started from the bottom up, basically making sure that the go1.18beta1
would
compile at each step.
First, let’s define what our handler interface. In prose we might say that this receives any type for input as I and returns any kind of output as O.
type Handler[I, O any] interface {
Handle(context.Context, I) (O, error)
}
At the same time I wrote my silly concrete version. Note that the input and output are both simple strings.
type titleHandler struct {}
func (h titleHandler) Handle (ctx context.Context, i string) (string, error) {
return strings.Title(i), nil
}
Next up, a serializer interface. If we wrote interface{}
instead of any
this type would work with older versions of Go.
type serde interface {
deserialize(*http.Request, any) error
serialize(any, http.ResponseWriter) error
}
I could imagine serializers being parameterized (ie have an [I, O any]
) but
didn’t have any good examples where it would actually make the code any better.
Here are my concrete examples:
type jserde struct{}
func (s jserde) deserialize(r *http.Request, i any) error {
d := json.NewDecoder(r.Body)
defer r.Body.Close()
return d.Decode(i)
}
func (s jserde) serialize(o any, rw http.ResponseWriter) error {
e := json.NewEncoder(rw)
return e.Encode(o)
}
type xserde struct{}
func (s xserde) deserialize(r *http.Request, i any) error {
d := xml.NewDecoder(r.Body)
defer r.Body.Close()
return d.Decode(i)
}
func (s xserde) serialize(o any, rw http.ResponseWriter) error {
e := xml.NewEncoder(rw)
return e.Encode(o)
}
Of course you could split these into two values (a serializer and a deserializer) so you could take JSON in and respond with XML, but that would be uncouth, so we’ll leave everything as is.
And for the meat of the post, let’s define a generic type that bundles these together:
type LMHTTP[I, O any] struct {
serde serde
handler Handler[I, O]
}
func (h LMHTTP[I, O]) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
var i I
if err := h.serde.deserialize(r, &i); err != nil {
fmt.Fprintf(os.Stderr, "deser: %s\n", err)
rw.WriteHeader(400)
}
o, err := h.handler.Handle(r.Context(), i)
if err != nil {
fmt.Fprintf(os.Stderr, "handle: %s\n", err)
rw.WriteHeader(500)
}
if err := h.serde.serialize(o, rw); err != nil {
fmt.Fprintf(os.Stderr, "ser: %s\n", err)
rw.WriteHeader(500)
}
}
The main things to notice above are:
- We put the type parameters on the type itself (
[I, O any]
). - The method gets the names of the parameters (
[I, O]
) and it’s important that the order is the same. - The method can use the parameter names as if they were types (
var i I
, for example.)
Finally, let’s use all the types together:
func main() {
mux := http.NewServeMux()
mux.Handle("/json", LMHTTP[string, string]{
serde: jserde{},
handler: titleHandler{},
})
mux.Handle("/xml", LMHTTP[string, string]{
serde: xserde{},
handler: titleHandler{},
})
fmt.Println(http.ListenAndServe(":8083", mux))
}
With the above I was able to run the code and interact with it:
$ curl http://localhost:8083/xml --request POST --data '<string>king of the world</string>'
<string>King Of The World</string>
$ curl http://localhost:8083/json --request POST --data '"king of the world"'
"King Of The World"
🔗 Why
I think generics to allow switching from JSON to XML is pretty silly. It’d be
convenient to drop in if it were warranted, but I prefer APIs to be limited and
straightforward. If I were doing this in anger I’d probably hardcode the json
serialization (or whatever) directly into the LMHTTP.Handle
method.
The real boon is that the business logic of our code (the handler) is straightforwardly defined in terms of concrete types. Here we’re using just a string in and a string out, but it works just as well with a struct in and a struct out.
I do think that writing the generics above are much more complicated and
confusing than typical Go, but I could imagine having a minimalist framework
built around the pattern above. It could pick apart errors returned from the
handler to map to errors other than simply 500
.
Of course middleware can now be added at either the outer layer (the traditional way to add middleware in Go) or the inner layer (wrapping the handler.) Both have uses, for example timing the outer layer will be more accurate, but the inner layer might let you have metrics that include parts of the request body, for example.
🔗 Middleware
Speaking of middleware, middleware is where generics really shine. Fundamentally, generics allow our middleware to not obfuscate our types behind an interface. Normally if you were to layer a bunch of middleware together in Go, once one of the middleware says: “my input param must implement interface X” and then passes that parameter to another middleware in the chain, that next layer gets… well an X. The concrete implementation is now hidden.
I wrote some middleware with this post in mind, parameterizing on the output value, but you could just as easily do this with other values you pass along. First, a middleware that times the request:
type setDuration interface {
setDuration(time.Duration)
}
type timerMW[I any, O setDuration] struct{
inner Handler[I, O]
}
func (h timerMW[I, O]) Handle(ctx context.Context, i I) (O, error) {
t0 := time.Now()
o, err := h.inner.Handle(ctx, i)
o.setDuration(time.Now().Sub(t0))
return o, err
}
Note that we use the setDuration
interface above, instead of any
like we’ve
been using before. This means that we can call the setDuration
method on o
.
But critically, o
is not a value of type setDuration
, like it would have been
in previous versions of Go. o
is parameterized and has the type we request when
we (later) instantiate the middleware value.
Next, lets add another middleware, presumably to emit some metrics:
type metricsEmitter interface {
emitMetrics()
}
type metricsMW[I any, O metricsEmitter] struct{
inner Handler[I, O]
}
func (h metricsMW[I, O]) Handle(ctx context.Context, i I) (O, error) {
o, err := h.inner.Handle(ctx, i)
o.emitMetrics()
return o, err
}
Let’s define a type that implements these interfaces and use it in our application handler:
type titleOut struct {
Out string
d time.Duration
}
func (s *titleOut) setDuration(d time.Duration) { s.d = d }
func (s *titleOut) emitMetrics() {
fmt.Println("request time", s.d)
}
type titleHandler struct {}
func (h titleHandler) Handle (ctx context.Context, i string) (*titleOut, error) {
return &titleOut{Out: strings.Title(i)}, nil
}
Finally, here’s an example of putting it all together:
func main() {
mux := http.NewServeMux()
mux.Handle("/json", LMHTTP[string, *titleOut]{
serde: jserde{},
handler: metricsMW[string, *titleOut]{timerMW[string, *titleOut]{titleHandler{}}},
})
mux.Handle("/xml", LMHTTP[string, *titleOut]{
serde: xserde{},
handler: metricsMW[string, *titleOut]{timerMW[string, *titleOut]{titleHandler{}}},
})
fmt.Println(http.ListenAndServe(":8083", mux))
}
In some of the lower level uses of generics (like a function that finds the minimum value in a slice) the type inference lets callers not even realize they are using generics:
func min[P constraints.Ordered](ps []P) P {
ret := ps[0]
for _, p := range ps[1:] {
if p < ret {
ret = p
}
}
return ret
}
func main() {
fmt.Println(min([]int{24, -2, 42, 0}))
}
But when doing more complex code like above I have found the type inference lacking. Go’s type inference has improved over time so maybe with generics that will happen too.
I am confident that generics will end up overused, at least for a while, in Go. We’ll have code that’s harder to understand than it has to be both due to a slightly more complex syntax but more much because of an added abstraction layer.
On the other hand, the use case for this post (middleware and web frameworks) is a real limitation in Go. I look forward to have nicely reusable middleware and web frameworks. Today I just eschew most existing middleware, but this added functionality will hopefully change that.
If you are interested in more posts like this, follow me on twitter.
🔗 Appendix A: Full Version of Code without Middleware
Don’t forget that for this to work you need to be using Go 1.18 or higher (beta is here) and you need to declare go 1.18 in your go.mod.
package main
import (
"os"
"net/http"
"strings"
"context"
"fmt"
"encoding/json"
"encoding/xml"
)
func main() {
mux := http.NewServeMux()
mux.Handle("/json", LMHTTP[string, string]{
serde: jserde{},
handler: titleHandler{},
})
mux.Handle("/xml", LMHTTP[string, string]{
serde: xserde{},
handler: titleHandler{},
})
fmt.Println(http.ListenAndServe(":8083", mux))
}
type LMHTTP[I, O any] struct {
serde serde
handler Handler[I, O]
}
func (h LMHTTP[I, O]) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
var i I
if err := h.serde.deserialize(r, &i); err != nil {
fmt.Fprintf(os.Stderr, "deser: %s\n", err)
rw.WriteHeader(500)
}
o, err := h.handler.Handle(r.Context(), i)
if err != nil {
fmt.Fprintf(os.Stderr, "handle: %s\n", err)
rw.WriteHeader(500)
}
if err := h.serde.serialize(o, rw); err != nil {
fmt.Fprintf(os.Stderr, "ser: %s\n", err)
rw.WriteHeader(500)
}
}
type titleHandler struct {}
func (h titleHandler) Handle (ctx context.Context, i string) (string, error) {
return strings.Title(i), nil
}
type Handler[I, O any] interface {
Handle(context.Context, I) (O, error)
}
type serde interface {
deserialize(*http.Request, any) error
serialize(any, http.ResponseWriter) error
}
type jserde struct{}
func (s jserde) deserialize(r *http.Request, i any) error {
d := json.NewDecoder(r.Body)
defer r.Body.Close()
return d.Decode(i)
}
func (s jserde) serialize(o any, rw http.ResponseWriter) error {
e := json.NewEncoder(rw)
return e.Encode(o)
}
type xserde struct{}
func (s xserde) deserialize(r *http.Request, i any) error {
d := xml.NewDecoder(r.Body)
defer r.Body.Close()
return d.Decode(i)
}
func (s xserde) serialize(o any, rw http.ResponseWriter) error {
e := xml.NewEncoder(rw)
return e.Encode(o)
}
🔗 Appendix B: Full Version of Code with Middleware
package main
import (
"os"
"time"
"net/http"
"strings"
"context"
"fmt"
"encoding/json"
"encoding/xml"
)
func main() {
mux := http.NewServeMux()
mux.Handle("/json", LMHTTP[string, *titleOut]{
serde: jserde{},
handler: metricsMW[string, *titleOut]{timerMW[string, *titleOut]{titleHandler{}}},
})
mux.Handle("/xml", LMHTTP[string, *titleOut]{
serde: xserde{},
handler: metricsMW[string, *titleOut]{timerMW[string, *titleOut]{titleHandler{}}},
})
fmt.Println(http.ListenAndServe(":8083", mux))
}
type LMHTTP[I, O any] struct {
serde serde
handler Handler[I, O]
}
func (h LMHTTP[I, O]) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
var i I
if err := h.serde.deserialize(r, &i); err != nil {
fmt.Fprintf(os.Stderr, "deser: %s\n", err)
rw.WriteHeader(500)
}
o, err := h.handler.Handle(r.Context(), i)
if err != nil {
fmt.Fprintf(os.Stderr, "handle: %s\n", err)
rw.WriteHeader(500)
}
if err := h.serde.serialize(o, rw); err != nil {
fmt.Fprintf(os.Stderr, "ser: %s\n", err)
rw.WriteHeader(500)
}
}
type titleOut struct {
Out string
d time.Duration
}
func (s *titleOut) setDuration(d time.Duration) { s.d = d }
func (s *titleOut) emitMetrics() {
fmt.Println("request duration", s.d)
}
type titleHandler struct {}
func (h titleHandler) Handle (ctx context.Context, i string) (*titleOut, error) {
return &titleOut{Out: strings.Title(i)}, nil
}
type Handler[I, O any] interface {
Handle(context.Context, I) (O, error)
}
type setDuration interface {
setDuration(time.Duration)
}
type timerMW[I any, O setDuration] struct{
inner Handler[I, O]
}
func (h timerMW[I, O]) Handle(ctx context.Context, i I) (O, error) {
t0 := time.Now()
o, err := h.inner.Handle(ctx, i)
o.setDuration(time.Now().Sub(t0))
return o, err
}
type metricsEmitter interface {
emitMetrics()
}
type metricsMW[I any, O metricsEmitter] struct{
inner Handler[I, O]
}
func (h metricsMW[I, O]) Handle(ctx context.Context, i I) (O, error) {
o, err := h.inner.Handle(ctx, i)
o.emitMetrics()
return o, err
}
type serde interface {
deserialize(*http.Request, any) error
serialize(any, http.ResponseWriter) error
}
type jserde struct{}
func (s jserde) deserialize(r *http.Request, i any) error {
d := json.NewDecoder(r.Body)
defer r.Body.Close()
return d.Decode(i)
}
func (s jserde) serialize(o any, rw http.ResponseWriter) error {
e := json.NewEncoder(rw)
return e.Encode(o)
}
type xserde struct{}
func (s xserde) deserialize(r *http.Request, i any) error {
d := xml.NewDecoder(r.Body)
defer r.Body.Close()
return d.Decode(i)
}
func (s xserde) serialize(o any, rw http.ResponseWriter) error {
e := xml.NewEncoder(rw)
return e.Encode(o)
}
If you're interested in being notified when new posts are published, you can subscribe here; you'll get an email once a week at the most.