Doing Well by Doing Bad: Writing Bad Code with Go Part 2

A Satirical Take on Programming in Go

In my last blog post, I explained how to be an evil Go programmer. Evil comes in many forms, but in programming, evil consists of intentionally making code harder to understand and maintain. Evil programs ignore language idioms in favor of techniques that provide short-term benefits in exchange for long-term problems. As a quick review, the evil Go tricks we covered include:

  • Poorly named and organized packages.
  • Incorrectly organized interfaces.
  • Passing pointers to variables into functions to populate their values.
  • Using panics instead of errors.
  • Using init functions and blank imports to set up dependencies.
  • Loading configuration files using init functions.
  • Using frameworks instead of libraries.

Big Ball of Evil

What does it look like if we put all of our evil tools together? We’d have a framework that used lots of configuration files, populated fields of structs using pointers, defined interfaces to describe the types that are being published, relied on magically-run code, and panicked whenever there was a problem.

And I built it. If you go to https://github.com/evil-go, you will see Fall, a dependency injection framework designed to bring you all of the bad things you could possibly want. I’ve paired Fall with a tiny web framework called Outboy that follows the same principles.

Now, you might wonder how bad it could really be. Well, let’s take a look. Let’s walk through a simple Go program written using best practices that exposes a single http endpoint, and then rewrite the program using Fall and Outboy.

Best Practices

Our program is contained in a single package called greet, that has all of the basic functionality to implement our single endpoint. Because this is a sample, we have a DAO that works entirely in memory, with three fields to hold the different values we will return. There’s also a method that fakes our database call and returns the proper greeting, depending on the input to the method.

package greet
type Dao struct {
DefaultMessage string
BobMessage string
JuliaMessage string
}
func (sdi Dao) GreetingForName(name string) (string, error) {
switch name {
case "Bob":
return sdi.BobMessage, nil
case "Julia":
return sdi.JuliaMessage, nil
default:
return sdi.DefaultMessage, nil
}
}

The business logic is next, and in order to implement our business logic, we define a struct to hold the output from the business logic, a GreetingFinder interface to describe what the business logic is looking for in a data lookup layer, and the struct to hold the business logic itself, with a field for the GreetingFinder. The actual logic is simple, it just calls the GreetingFinder, and handles any errors that might happen.

type Response struct {
Message string
}
type GreetingFinder interface {
GreetingForName(name string) (string, error)
}
type Service struct {
GreetingFinder GreetingFinder
}
func (ssi Service) Greeting(name string)(Response, error) {
msg, err := ssi.GreetingFinder.GreetingForName(name)
if err != nil {
return Response{}, err
}
return Response{Message: msg}, nil
}

Next comes the web layer, and for that part, we define a Greeter interface that provides all the business logic we need, and a struct to contain our http handler that is configured with a Greeter. Then we have the method to implement the http.Handler interface, which takes apart the http request, calls the greeter, handles any errors, and returns back the results.

type Greeter interface {
Greeting(name string) (Response, error)
}
type Controller struct {
Greeter Greeter
}
func (mc Controller) ServeHTTP(rw http.ResponseWriter,
req *http.Request) {
result, err := mc.Greeter.Greeting(
req.URL.Query().Get("name"))
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
rw.Write([]byte(err.Error()))
return
}
rw.Write([]byte(result.Message))
}

That’s the end of the greet package. Next, we take a look at how a good Go developer would write a main to use this package. In the main package, we define a struct called Config that contains the properties that we need in order to run. The main function then does three things.

  • First, it calls a loadProperties function, which uses a simple config library (which can be found at https://github.com/evil-go/good-sample/blob/master/config/config.go) to load the properties from a properties file and puts them into an instance of config. If config loading fails, the main function reports an error and exits.
  • Second, the main function wires up the components in the greet package, explicitly assigning them values from the config and setting up how one component depends on another.
  • Third, it calls a small server library (which can be found at https://github.com/evil-go/good-sample/blob/master/server/server.go) and passes in an endpoint description of the path to be handled, the http verb that should be listened for, and the http.Handler to process the request. Calling the server library starts the web service, and our application is complete.
package main
type Config struct {
DefaultMessage string
BobMessage string
JuliaMessage string
Path string
}
func main() {
c, err := loadProperties()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
      dao := greet.Dao{
DefaultMessage: c.DefaultMessage,
BobMessage: c.BobMessage,
JuliaMessage: c.JuliaMessage,
}
svc := greet.Service{GreetingFinder: dao}
controller := greet.Controller{Greeter: svc}
      err = server.Start(server.Endpoint{c.Path, http.MethodGet, controller})
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}

This example is pretty short, but it shows how good Go is written; some things are a little verbose, but it’s clear what’s going on. We glue together small libraries that are explicitly configured to work together. Nothing is hidden; anyone could pick up this code, understand how the parts all fit together, and swap in new parts when needed.

The Bad Place

Now we’re going to look at the Fall and Outboy version. The first thing we do is break apart the greet package into multiple packages, each one containing one layer in the application. Here’s the DAO package. It imports Fall, our dependency injection framework, and because we are being evil and defining interfaces on the wrong side of the relationship, we define an interface called GreetDao. Notice that we’ve removed any reference to an error; if something fails, we’re just going to panic. So far, we’ve got bad packaging, bad interfaces, and bad errors; we’re off to a great start.

Our struct from the good example is here, slightly renamed. The fields now have struct tags; they are used to tell Fall to assign a value that’s registered with Fall to the field. We also now have an init function for our package, which is where we really start piling on the evil. In the package’s init function, we call Fall twice:

  • Once to register a properties file that supplies values for the struct tags.
  • Once to register a pointer to an instance of the struct so Fall can populate those fields for us and make our DAO available for other code to use.
package dao
import (
"github.com/evil-go/fall"
)
type GreetDao interface {
GreetingForName(name string) string
}
type greetDaoImpl struct {
DefaultMessage string `value:"message.default"`
BobMessage string `value:"message.bob"`
JuliaMessage string `value:"message.julia"`
}
func (gdi greetDaoImpl) GreetingForName(name string) string {
switch name {
case "Bob":
return gdi.BobMessage
case "Julia":
return gdi.JuliaMessage
default:
return gdi.DefaultMessage
}
}
func init() {
fall.RegisterPropertiesFile("dao.properties")
fall.Register(&greetDaoImpl{})
}

Let’s look at the service package for our code. It imports the DAO package, because it needs to access the interface defined there. The service package also imports a model package that we haven’t seen yet, but we’ll store all of our data types there. And we import Fall, because like all good frameworks, it forces itself everywhere. We also define an interface for the service to expose to the web layer, again with no error returned.

The implementation of our service now has a struct tag with wire. A field tagged with wire has its dependency automatically assigned when the struct is registered with Fall. In our tiny sample, it’s clear what is going to be assigned to this field, but in a bigger program, the only thing you’ll know is that something, somewhere, implemented that GreetDao interface and was registered with Fall. You have no control over the behavior of the dependency.

Next is the method for our service, which has been modified slightly to get the GreetResponse struct from the model package and which removes any error handling. Finally, we have the package’s init function which registers an instance of the service with Fall.

package service
import (
"github.com/evil-go/fall"
"github.com/evil-go/evil-sample/dao"
"github.com/evil-go/evil-sample/model"
)
type GreetService interface {
Greeting(string) model.GreetResponse
}
type greetServiceImpl struct {
Dao dao.GreetDao `wire:""`
}
func (ssi greetServiceImpl) Greeting(name string)     model.GreetResponse {
return model.GreetResponse{Message: ssi.Dao.GreetingForName(name)}
}
func init() {
fall.Register(&greetServiceImpl{})
}

Now let’s take a look at that model package. There’s not much to see here, just that the model is separated from the code that creates it, for no reason other than layering.

package model
type GreetResponse struct {
Message string
}

Then we have our web front end in the web package. In our web package, we import both Fall and Outboy, and we also import the service package that the web package depends on. Because frameworks only play nicely together when they integrate behind the scenes, Fall has some special-case code to make sure that it and Outboy work together. We also modify a struct to be the controller for our web app. It has two fields:

  • The first is wired by Fall with an implementation of the GreetService interface from the service package.
  • The second is the path for our single web endpoint. It’s assigned the value of a property that’s loaded from the property file registered in the package’s init function.

Our http Handler is now renamed to GetHello and is error handling free. We also have an Init method, not to be confused with the init function. Init is a magic method that’s invoked on structs registered with Fall after all of their fields are populated. In Init, we call Outboy to register our controller and its endpoint at the path that was assigned by Fall. Looking at the code, you’ll see the path and the handler, but the HTTP verb isn’t specified. In Outboy, the method’s name is used to define what verb a handler responds to. Since our method is named GetHello, it responds to GET requests. If you don’t know the rules, you can’t understand what kinds of requests it responds to. Evil, right?

Finally, we call the init function to register the property file and the controller with Fall.

package web
import (
"github.com/evil-go/fall"
"github.com/evil-go/outboy"
"github.com/evil-go/evil-sample/service"
"net/http"
)
type GreetController struct {
Service service.GreetService `wire:""`
Path string `value:"controller.path.hello"`
}
func (mc GreetController) GetHello(rw http.ResponseWriter, req *http.Request) {
result := mc.Service.Greeting(req.URL.Query().Get("name"))
rw.Write([]byte(result.Message))
}
func (mc GreetController) Init() {
outboy.Register(mc, map[string]string{
"GetHello": mc.Path,
})
}
func init() {
fall.RegisterPropertiesFile("web.properties")
fall.Register(&GreetController{})
}

The only thing left to show is how we kick off a program. In the main package, we use blank imports to register outboy and the web package, and the main function calls fall.Start() to kick off the whole application.

package main
import (
_ "github.com/evil-go/evil-sample/web"
"github.com/evil-go/fall"
_ "github.com/evil-go/outboy"
)
func main() {
fall.Start()
}

Pulling Back the Curtain

And there we go, a complete program written using all of our evil Go tools. This is terrible. It magically obscures how the parts of the program fit together, making it far too hard to understand what’s going on.

And yet, you have to admit, there’s something seductive about writing code with Fall and Outboy. For a tiny program, you could even argue that it’s an improvement. Look at how easy it is to specify configuration information! I can wire up dependencies with almost no code! I registered a handler for a method just by using its name! And without any error handling, everything looks so clean!

And that’s the way evil works, at first glance, it’s really appealing. But as your program changes and grows, all of that magic starts getting in the way, making it harder to figure out what’s going on. It’s only when you are fully in the grip of evil do you look around and see that you’re trapped.

For Java developers, this all might sound slightly familiar. These are the techniques found in many popular Java frameworks. As I mentioned earlier, I’ve used Java for over 20 years, dating back to Java 1.0.2 in 1996. In many cases, Java developers were the first to face the problems of writing large-scale enterprise software in the internet era. I remember when servlets, EJBs, Spring, and Hibernate were new. The solutions that were adopted by Java developers made sense at the time, but over the years, these techniques are showing their age. Newer languages like Go are designed to address the pain points that were discovered when older techniques were used. However, as Java developers start to investigate Go and write code using it, they should be aware that attempts to replicate patterns from Java will produce suboptimal results.

Go was designed for programming in the large, for projects that span hundreds of developers and dozens of teams. But in order for Go to do that, it needs to be used in the way it works best. We can choose to be evil, or we can choose to be good. If we choose evil, we can encourage new Go developers to transplant their style and techniques before they discovered Go. Or, we can choose good. Part of our job as Go developers is to mentor these new Gophers and help them understand the reasons behind our best practices.

The only downside of doing good is that you’ll have to find another way to express your inner evil.

Have you considered driving under the speed limit in the fast lane?

Missed Part 1? Read it here.


DISCLOSURE STATEMENT: These opinions are those of the author. Unless noted otherwise in this post, Capital One is not affiliated with, nor is it endorsed by, any of the companies mentioned. All trademarks and other intellectual property used or displayed are the ownership of their respective owners. This article is © 2018 Capital One.