Lessons Learned: Using Go for Web Development

A cartoon gopher climbing up a mountain.

After becoming bored with PHP and Laravel, I went on a journey to a find a replacement language for web development. Along the way, I came across Go and it looked quite promising. Turns out though, it’s not the best for web development. Here’s what I learned.

The Appeal of Go

When I first saw Go, I saw how much it resembled C and Swift – two languages I’ve used and enjoyed for some time. The main features/benefits that interested me were:

In addition, there’s no framework required to build a web application. There’s a fast production-grade web server built right into the language itself, along with all the tools required to handle web requests. That means more control and less reliance on external dependencies. With that, it looked like the perfect language.

Go was still new and there wasn’t much to read about regarding web development in the real world. A few big companies were using it, but how wasn’t that clear at the time. For all the places that were using it (list), it was typically on the backend, e.g. data processing and networking tools. Quite different from the application I would be building: a standard CRUD application. Without concrete examples, I began developing and seeing where it would lead.

In the beginning it was fantastic, but just a few months later, I stopped. I realised Go wasn’t a good choice.

What Went Well

Go lives up to its name. It’s exceptionally fast, easy to learn, understand and write. In its target arena, it excels. There are problems in web development that Go is well-suited for, but a monolithic CRUD application is not one.

No Framework

The standard router that Go provides is limited, so you’ll want to use something like gorilla/mux. But, other than a database driver, there’s no additional components that will need introducing; that’s the entire ‘framework’. Total control can be a good thing, sometimes.

It takes a handful of lines to get a web server up and running:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello World!")
    })

    http.ListenAndServe(":80", nil)
}

There’s protection that can come with being insulated from external dependencies should they go rogue, but it will mean missing out on all the benefits that open-source brings.

Performance

With PHP, it’s often quicker to have the database server do some CPU-intensive processing, but with Go, we’re not forced down that road. Likewise, if there’s some processing that can’t be done in the database, then Go can do it much faster than PHP ever could. Sub-second response times can be normal with Go.

Static Typing

This isn’t specific to Go, but as I was comparing it to PHP, it was something I was grateful for. The ability for the compiler to catch errors earlier means I have a slight gain in confidence. This is also what gives it a speed advantage over PHP, but we’re comparing apples and oranges in that respect.

Tooling

The compiler is fast, almost instant, so there’s no hanging around. The gofmt tool will keep code formatted and consistent to one single specification — no more flame wars, arguments over conventions and debates on tabs vs. spaces. That ends now. Testing and code coverage also comes as standard.

Easy Deployments

A program compiles to a single binary (file). You can even bundle the HTML templates with it. Which means that one file can be passed between developers, testers and application servers without wondering if the build is the same.

Hardware Cost

From an economic perspective, the higher performance will mean fewer servers are required. Servers can be smaller and cheaper, while still supporting a large user base.

What Didn’t Go Well

No Framework

Without a framework, you have full control. You also have to write everything yourself. I was spending time writing things like middleware, session handling, mailers and template generation. Things that all modern frameworks have right from the start. Their code is mature, stable and well-tested – unlike mine. This starts becoming a waste of time. Code reuse also has its benefits.

Repetition (boilerplate)

Go’s simplicity through fewer keywords has its strengths, but it can also be seen as a weakness, especially when it comes to repetition.

Constantly repeating this gets tiring:

x, err := doSomething()
if err != nil {
    return err
}

Due to the lack of generics, there’s much repetition between the (MVC) models, implemented as structs. There are ways to DRY the code, e.g. common methods that take an interface, but that’s giving up the benefits of static typing.

What could be done with a map or reduce function needs implementing in a long-winded for loop. Fewer keywords is good for beginners when learning the language, but to experienced programmers, life without these missing ‘shortcuts’ and syntactic sugar can become tedious.

Templating

Like the router, templating is limited. Injecting data into a template once is simple and clearcut, but injecting something like “the logged in users’s name” across various templates can result in code that feels wrong, depending on the approach. I found three options, each one sub-optimal:

  1. Duplicate the code in each handler (MVC controller). You can abstract the bulk into a single function, but that function still needs to be called, checked for errors and then injected into the template several times over.
  2. Pass all templates through a global ‘builder’ that injects data into the required pages. This removes the duplicate issue, but begins to pollute the template builder with business logic and error handling.
  3. Use a global template function which can be called from the individual templates as and when required. However, this means errors can’t be checked and it’s introducing processing into the view layer. Global functions are designed for things like truncation, not fetching a users name.

Uses for Concurrency

Sending an email in the background is the perfect example for concurrent actions. When a user registers, the page can respond back without waiting for a welcome email to be sent. You could place the email sending code in a goroutine, removing the need for placing it on an external queue such as Redis and then processing it moments later. It seems the simpler option with the processing being done close to where it was actioned. However, if that action was to fail, how do you go about retrying it? A queue is good at those things.

When it came down to it, all the concurrent processing I thought could be moved from a queue, actually needed what a queue provided.

Productivity (or human cost)

This was the deciding factor for me. What you gain in reduced hardware costs will be made up for in human costs.

Go prioritises machine speed over human speed.

After a while, I was finding it was taking a long time to implement features, features that could be done in hours or minutes in a traditional web framework. Lack of productivity was down to a few things:

  1. Re-inventing the wheel. Without a framework to give me a head start, I had to write simple and common features myself. It’s simple to do, but time consuming.
  2. With such few keywords, some syntactic sugar and shortcuts that I’ve come to rely on are missing. I have to take the time to write the extra lines of code. Over time this adds up.
  3. Without generics, code repetition is rife. You gain protections, but the code gets wetter and it takes longer.
  4. Error checking soon gets time-consuming and frustrating. I’m not saying it’s a bad thing, but other languages have taken a better approach.

When you’re building an MVP or prototype, it’s important to have something to show early on, rather than pouring over the finer details.

Moving Forward

I think Go is great in the arena it was designed for: large teams working on high-performance applications, networking services and CPU-intensive tasks.

When it comes to the web however, CRUD apps in particular, it’s important to consider other languages, especially those with battle-hardened frameworks (e.g. Rails). There’s no point in taking 12 months to build something perfect when change is rapid and funding might not last. In addition, most time will be spent in the database when it comes to run-of-the-mill web apps, negating the need for the high-performance that Go offers.

If you like reinventing the wheel, want to learn about web frameworks or need ultra-high performance, then go with Go. Just make sure that in the long run, the return on investment will justify the upfront labour cost. Otherwise, I suggest sticking with a batteries-included battle-hardened more dynamic framework and when/if the time comes, extract CPU-intensive tasks into smaller Go services.

tl;dr Use the tool that is right for the job.