DEV Community

protium
protium

Posted on

Github template for Golang services

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))
Enter fullscreen mode Exit fullscreen mode

Taskfile

dotenv: ['.env']
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

CLI args

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

hello:
  @echo $(name)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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}}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/*
Enter fullscreen mode Exit fullscreen mode

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`
Enter fullscreen mode Exit fullscreen mode

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'`]
}
Enter fullscreen mode Exit fullscreen mode

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')
);
Enter fullscreen mode Exit fullscreen mode

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:

Top comments (0)