Structuring a Go app

Written by Luke Morton on 19th January 2018

On how to structure a web application built in Go. A brief overview of separating business and application delivery concerns.

I first played with Go earlier this year and really enjoyed it. As these things go, I dropped it and forgot about it. Recently I've reignited my passion for the language, learning all things web.

I have to say, the one thing that really shines for Go is the use of it for microservices. I wouldn't like to build a massive monolith in Go, all code tends to be found in the root folder of a repository. Go has the idea of packages which are essentially ways of namespacing types and functions for sharing. Much like a Ruby Gem, PHP namespace or Java module. No one really advises splitting domain logic into multiple packages to be consumed by a single application. Just put each package in it's own web application. If you need to blend two domains together then they should probably be one domain in the Go world.

Single domain Go app

If we take a look at an example directory structure you'll see why Go programmers are fanatical about keeping things small and concise.

In an imaginary $GOPATH/src/github.com/lukemorton/facebook/users/ directory:

bin/
    server/
        main.go
        main_test.go
Dockerfile
Makefile
database.go
user.go
register.go
register_test.go
reset_password.go
reset_password_test.go

I mean look at that. It's beautiful. Eat your heart out Uncle Bob with your concept of understanding what an application does from it's folder structure. Since a Go application's domain stays in one package, it's in the root directory! No more app/{controllers,models,views}, no more lib/{use_case,domain,gateway}, just put all your use cases or actions in their own files in the root directory.

Theres an exception to every rule

So you know I said the Go lot, as in the Go community, tend to have one package per application. I kind of lied. They do tend to have a sub package for commands or binaries. For a web application, the binary runs the application.

In the example before you'll notice there's a folder called bin/server/, that's a separate package to the one in the root directory. When that package is compiled it produces a binary called server. That binary is a portable application you can run anywhere, that's the thing you distribute.

Overview of domain logic

The example above that I'm now going to run with is a users service that currently manages user registration and resetting of passwords. Don't bother asking me how they actually log in, it's just an example.

If we take a peak inside register.go we quickly understand that it does indeed do what it says on the tin:

package users

import (
    "errors"
    "time"
)

func Register(db *DB, user *User) error {
    err := validate(user)
    user.CreatedAt = time.Now()
    user.UpdatedAt = time.Now()

    if err != nil {
        return err
    }

    return db.create(user)
}


func validate(user *User) error {
    if user.Email == nil {
        return errors.New("User requires email address")
    }
}

The exported function from this file does indeed register a user after a quick bit of validation. Notice the package users at the top of the file. Everything in this directory will have the same package name of users.

Another file of note is user.go, this contains a struct that represents a user. This is akin to a methodless model from Ruby on Rails.

package users

import (
    "time"
)

type User struct {
    CreatedAt time.Time
    UpdatedAt time.Time
    Email string
}

The last file to note would be database.go. We can assume reset_password.go looks much like register.go because they're both actions.

package users

import (
    "log"
    "sqlx"
)

func ConnectDB() *DB {
    db, err := sqlx.Connect("postgres", "user=luke dbname=users")

    if err != nil {
        log.Fatalln(err)
    }

    return &DB{db}
}

type DB struct {
    sqlx.DB
}

func (db *DB) Create(user User) error {
    q := `
        INSERT INTO users (created_at, updated_at, email)
        VALUES (:created_at, :updated_at, :email)
    `

    _, err := db.NamedExec(q, user)
  return err
}

So we've got some SQL in the mix here. All the database queries can be kept to this file so they're all in one place.

Serving the application

Now it's time to get around to talking about bin/server/main.go. Files named main.go typically have a package named main. They are interpreted by the go compiler as files that should be compiled into binaries. They always have a main() function too:

package main

import (
    "github.com/lukemorton/facebook/users"
    "gopkg.in/gin-gonic/gin.v1"
    "log"
)

func main() {
    AppEngine().Run(":3000")
}

func AppEngine() *gin.Engine {
    a := App{users.ConnectDB()}
    return a.Engine()
}

type App struct {
    db *DB
}

func (app *App) Engine() *gin.Engine {
    app := gin.Default()

    app.POST("/register.json", func(c *gin.Context) {
        var user User
        c.BindJSON(&user)

        err := users.Register(app.db, &user)

        if err == nil {
            c.JSON(200, user)
        } else {
            c.JSON(400, gin.H{"error": err})
        }
    })

    app.POST("/reset-password.json", func(c *gin.Context) {
        var user User
        c.BindJSON(&user)

        err := users.ResetPassword(app.db, &user)

        if err == nil {
            c.JSON(200, user)
        } else {
            c.JSON(400, gin.H{"error": err})
        }
    })

    app.NoRoute(func(c *gin.Context) {
        c.JSON(400, api.Error("Bad request"))
    })

    return app
}
---

Feel free to read some more thoughts or go back to the introduction.