Dwi Wahyudi

Senior Software Engineer (Ruby, Golang, Java)


In this article, we’re going to kickstart our e-commerce app with Go.

Kickstarting Our Application

In previous article, we’ve listed some API to build in order for this app to run. We’re going to start with registration API. We need to store account’s information in database. But before that, let’s pay attention on some foundational code first.

But before that, let’s create a new app. We’re going to download some libraries:

$ mkdir go-mini-commerce
$ cd go-mini-commerce
$ go1.18 mod init go-mini-commerce
$ go1.18 get -u github.com/go-chi/chi/v5
$ go1.18 get -u github.com/jackc/pgx/v4
$ go1.18 get -u github.com/BurntSushi/toml@latest
$ go1.18 get -u github.com/jmoiron/sqlx

We’re going to use sqlx for extending standard sql package, it can support pgx driver too. The interface would still be the same with some nice features and extension both provided by sqlx and pgx.

Directory Structure

And here’s how our starting directory structure will look like.

  • common contains common codes used by multiple layers of our app.
  • config contains secrets and configs.
  • entity contains struct, var, const used by multiple layers of our app.
  • external contains external dependency other than core database. We decouple this part for future isolation use.
  • infra contains Go code to connect with infrastructure (PostgreSQL, ElasticSearch, etc). infra will compose external components.
  • repo contains Go code to communicate with our datasource, for now we’ll be using PostgreSQL for our core.
  • restful contains http handlers for restful communications.
  • schema contains database migration files.
  • service contains Go service code.

Directory structure or layout is debatable matter. It is always better to give a directory a name that screams its intention. Hopefully those directories above can scream their respective intention to whoever who see it.

For starting out, here’s our first server using chi router, this is our main.go. Don’t forget the chi router import to include v5.

package main

import (
	"net/http"

	"github.com/go-chi/chi/v5"
)

func main() {
	var (
		r = chi.NewRouter()
	)

	r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("pong"))
	})
	http.ListenAndServe(":4545", r)
}

Let’s run this server:

$ go1.18 run main.go

We can then test the /ping route:

$ curl localhost:4545/ping

Database

Because we’re going to use PostgreSQL, we run this command to run the database:

docker run -d -rm -p 5433:5432 -e POSTGRES_PASSWORD='chocolatecake' -e POSTGRES_USER='user00' -e ALLOW_IP_RANGE=0.0.0.0/0 -e "TZ=UTC" -e "PGTZ=UTC" -e PGDATA=/var/lib/postgresql/data/pgdata -v $HOME/postgres_14_data:/var/lib/postgresql/data --name postgresql-14 postgres:14.1

Above command tells docker to get and run PostgreSQL 14 container with username user00 and password chocolatecake. We also map certain dir $HOME/postgres_14_data as volume to PostgreSQL container PGDATA env. If it’s running, we can then check the container status. We’ll be using the default port, 5432.

$ docker container ls

CONTAINER ID   IMAGE           COMMAND                  CREATED        STATUS          PORTS                    NAMES
5148f39bcc2d   postgres:14.1   "docker-entrypoint.s…"   3 months ago   Up 46 seconds   0.0.0.0:5433->5432/tcp   postgresql-14

We’re going to name our database: minicommerce.

# psql -U user00 
psql (14.1 (Debian 14.1-1.pgdg110+1))
Type "help" for help.

user00=# CREATE DATABASE minicommerce;
CREATE DATABASE
user00=# 

For database migration, we’re going to use this: golang-migrate.

Database Connectivity.

Before we move to our first API, we need to construct some code to connect and communicate with PostgreSQL. We’ll do this in infra package.

In order to store secrets (such as database host, username and password), we’re going to do it in a toml file. Prepare a toml file app.toml in config/ directory. Add it to .gitignore.

[pg]
write_pg_database_url="postgres://user00:chocolatecake@0.0.0.0:5433/minicommerce?sslmode=disable"
read_pg_database_url="postgres://user00:chocolatecake@0.0.0.0:5433/minicommerce?sslmode=disable"

With this toml file in place, we’re going to use this with sqlx extension and pgx driver. Here’s how infra/infra.go will look like.

package infra

import (
	"log"
	"os"
	"sync"

	"github.com/BurntSushi/toml"
	_ "github.com/jackc/pgx/v4/stdlib"
	"github.com/jmoiron/sqlx"
)

type (
	Infra interface {
		PG() DB
	}

	DB struct {
		Read, Write *sqlx.DB
	}

	PGConfig struct {
		WritePgDatabaseURL string `toml:"write_pg_database_url"`
		ReadPgDatabaseURL  string `toml:"read_pg_database_url"`
	}

	config struct {
		PGConfig PGConfig `toml:"pgconfig"`
	}

	infra struct {
		config
	}
)

func NewInfra() Infra {
	return &infra{}
}

var (
	cfgOnce sync.Once
	cfg     config
)

func (i *infra) Config() config {
	cfgOnce.Do(func() {
		cfgFile, err := os.ReadFile("config/app.toml")
		if err != nil {
			log.Fatal(err)
		}

		_, err = toml.Decode(string(cfgFile), &cfg)
		if err != nil {
			log.Fatal(err)
		}
	})

	return cfg
}

var (
	dbOnce sync.Once
	db     DB
)

func (i *infra) PG() DB {
	dbOnce.Do(func() {
		cfg := i.Config()
		writeDB, err := sqlx.Open("pgx", cfg.PGConfig.WritePgDatabaseURL)
		if err != nil {
			log.Fatal(err)
		}

		readDB, err := sqlx.Open("pgx", cfg.PGConfig.ReadPgDatabaseURL)
		if err != nil {
			log.Fatal(err)
		}

		db = DB{Read: readDB, Write: writeDB}
	})

	return db
}
  • Config() function will read config/app.toml and decode it to config struct. So far this struct contains database URL for our app.
  • Infra interface exposes PG() method which will open (and connect with) the database, the method will return DB struct that has Read and Write database connections, the implementation above is using sqlx and pgx.
  • Infra interface also exposes Config() as well for our components (http handlers, services, repo, etc to use).
  • infra struct including DB struct above will be passed down to repo level for querying/executing queries statements.
  • Any instance of handler (restful or may be GRPC) should have infra as one of the fields, so that we can inject infra (together with database connection) on application startup.
  • We setup config and DB each with singleton variable (helped by sync.Once), so it will be only assigned once.

Later we can move some code related to Postgresql to infra/pg.db.

We’re now ready to create our first module, registration API. But before that, we’re going to create some common service first in our project in the next article. This is quite important as to highlight our intention on creating an application that has decoupled and testable components.