Monday, August 16, 2021

Go middlewares for object-oriented programmers

Go language (Golang, http://golang.org) is a very simple procedural programming language of the likes of C. Having been devised in the post-object-orientation age though, it borrows some basic concepts from object-oriented programming but it provides no real support for it. No inheritance, no encapsulation, no polymorphism at all.

That is indeed intemptional, and most of the times it's just OK. It saves programmers the pain of understanding Java class hierearchies a hundred levels deep, the "how come..." moments when reading Python code, or the frustration of deciphering C++11 code. However, some times you really miss some more support for OO constructs. Middleware development in go-kit is one of those times.

Middlewares

In go-kit, a middleware is a function that wraps another function and returns this inner function as result. You may have met them before in other languages under the name of function decorators. For example:

func DoSomething(s string) (int, error) {
return fmt.Println(s)
}

func PrefixingMiddleware(prefix string) func(string)(int, error) {
return func(s string) (int, error) {
fmt.Print(prefix)
return DoSomething(s)
}
}


Middlewares are described in detail in the go-kit documentation here: https://gokit.io/examples/stringsvc.html#middlewares.

This looks harmless, right? I mean, a function complementing or decorating another function, seems just fine. However things get slightly messy when decorating the methods of an interface. For example:

type AnInterface struct {
str string
}

func ( ai AnInterface ) doSomething() (int, error) {
return fmt.Println(ai.str)
}

type PrefixingMiddleware struct {
prefix string
wrapped AnInterface
}

func ( pm PrefixingMiddleware ) doSomething() func()(int, error) {
return func() (int, error) {
fmt.Print(pm.prefix)
return wrapped.doA()
}
}

When there are many methods to decorate, it's easy to get lost and forget where and what you were decorating. I was feeling like that when writing my first REST server using go-kit. So I devised a simple naming trick that helped me "connect" what I was writing with the familiar OO notions carved in my brain.

In the context of an interface, adding middlewares is like extending a base class with a derived class that overloads some or even all of its methods. If I rename some variables, like this:

type AnInterface struct {
str string
}

func ( self AnInterface ) doSomething() (int, error) {
return fmt.Println(self.str)
}

type PrefixingMiddleware struct {
prefix string
super AnInterface
}

func ( self PrefixingMiddleware ) doSomething() func()(int, error) {
return func() (int, error) {
fmt.Print(self.prefix)
return super.doA()
}
}

For OO programmers, that syntax clarifies a few things:
  1. It becomes apparent that  PrefixingMiddleware interface is based on interface  AnInterface
  2. It becomes apparent that  doSomething() is a method of AnInterface that is overloaded by   PrefixingMiddleware
  3. It becomes apparent that PrefixingMiddleware.doSomething() is adding something of itself then recurring to its base interface functionality
  4. Finally, it is clear when the code is accessing elements of the base interface and when it is accessing elements of the derived interface 
However this simple trick does not help the need to write one new middleware method for every method you want to decorate. When there are many methods in an interface this becomes very verbose (check e.g. https://github.com/go-kit/examples/blob/master/profilesvc/middlewares.go).  Middlewares and similar constructs had been easier would Go support interface inheritance and/or some kind of decorator syntax (like e.g. aspects in C++/Java or the @ in Python). That would have left room for other OO sins though, the Golang creators, likely intemptionally, decided to skip.

Conclusion 

I don't know, maybe an expert Go programmer would prefer the first syntax above. However, if you're doing the transition from OO to Go, the second syntax might turn out to be easier to understand.

Using the  self  and  super  names may be useful not only in the context of middlewares, but also when writing any interface implementation if you're familiar with OO.

Go middlewares for object-oriented programmers Go language (Golang, http://golang.org ) is a very simple procedural programming language of ...