As a weekend project I created a github template that can be very handy for creating go services with relational databases. Let’s take a look at what is included.

Task Runner

For many years, GNU make has been my to-go tool to run rules and tasks for any sort of project. It is fairly simple to use but it can also become complex as some rules might require to execute external tools or even declare bash functions with in a rule definition. I must admit that the developer experience can be rough for those who haven’t used make before.

Developer experience is a very important topic to me and I decided to use this project to find a reliable alternative to make. And that’s how I found Task:

Task is a task runner / build tool that aims to be simpler and easier to use than, for example, GNU Make.

After checking some examples and its API docs, I got convinced I should give it a try. Although I’m not a fan of yaml I found some neat features that I prefer over make:

Import env variables

Makefile

include .env
$(eval export $(shell sed -ne 's/ *#.*$$//; /./ s/=.*$$// p' .env))

Taskfile

dotenv: ['.env']

Showing help

Although make doesn’t create a help command, there is a very common pattern to define one:

help: ## print this help
	@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort

Task provides a list of available commands when running task -l, but you need to add a desc field to each command:

run:
  desc: Run go app
  cmds:
    - go run cmd/main.go

CLI args

When working with make you can pass arguments/variables a target, for instance

hello:
  @echo $(name)

To pass the argument you would call it like make hello name=dev. This can become tedious when passing multiple args to the inner command. A simple hack to allow cli args is to filter out those args from the make goals and also avoid errors when targets are not found:

# Filter out make goals from CLI args
args = $(filter-out $@,$(MAKECMDGOALS))
# Do nothing if target not found
%:
	@:
hello:
  @echo $(args)

It works fine unless the arguments have a value that matches the name of any target.

On the other hand, task provides a simple template variable with the arguments CLI_ARGS

hello:
  cmds:
    - echo {{.CLI_ARGS}}

The command can then be called as task hello -- world

Verbose file targets definition

Although make file targets are not difficult to understand, I think task syntax is easier to understand for a new dev. Let’s compare them:

pkg/db/%.go: db/queries/%.sql
	@docker run --rm -v ${CURDIR}:/src -w /src kjconroy/sqlc generate

Taskfile

db-gen:
  desc: Generate queries code using Sqlc
  cmds:
    - docker run --rm -v $pwd:/src -w /src kjconroy/sqlc generate
  sources:
    - db/queries/*.sql
  generates:
    - pkg/db/*

So far task seems to be a very good alternative to make, at least for my personal use case.

Folder structure

The folder structure of this template is based on folder structures I’ve seen across many go repos, which I really like:

- `cmd/`: app entry points
- `db/`:
  - `migrations/`: SQL migrations files
  - `queries/`: SQL query files used by `sqlc`
- `pkg/`: app sources
  - `db/`: code generated by `sqlc`

Database

As mentioned before, this template is for a go service with a relational database. As a personal preference I chose postgres as db engine.

Schema

I have recently adopted the practice of defining my db schemas using dbml and generating the sql code using their CLI tool. Example: This schema

Table users {
  id uuid [pk]
  user_name text [not null, unique]
  password_hash bytea [not null]
  created_at timestamptz [not null, default: `now() at time zone 'utc'`]
  updated_at timestamptz [not null, default: `now() at time zone 'utc'`]
}

Will generate this SQL code:

CREATE TABLE "users" (
  "id" uuid PRIMARY KEY,
  "user_name" text UNIQUE NOT NULL,
  "password_hash" bytea NOT NULL,
  "created_at" timestamptz NOT NULL DEFAULT (now() at time zone 'utc'),
  "updated_at" timestamptz NOT NULL DEFAULT (now() at time zone 'utc')
);

Queries and migrations

After having worked with different ORMs (goent, gorm) and plain go sql code, I find that having a middle ground is always the most versatile option. This middle ground is about having control over raw SQL queries and the ability to generate code to run all those queries and represent models in code.

For this matter I chose sqlc which offers a good amount of features like support for different dbs and db drivers. Regarding db migrations, I prefer to run them isolated from the code. There are countless tools to manage migrations but recently I’ve been sticking with go-migrate which also provides a go library in case I want to integrate the migrations into the code.

To run both tools, I’ve created tasks that will use their docker images. Alternatively, there could be a task to install them into the system.

Docker

The template provides a multi-stage Dockerfile. It uses the official golang:1.18 image for building and a scrath image to copy the binaries.

A docker-compose file can be used to get a db instance and run migrations on it. In this cases the migrations service waits for the postgres service to be ready, so we only need to run docker-compose up -d.

CI

A github actions workflow is provided to run go fmt, vet, test and gosec. An initial configuration for dependabot is also provided.

That’s it, go take a look at the repo here.

Thanks for reading 👽

Other posts: