Dwi Wahyudi

Senior Software Engineer (Ruby, Golang, Java)


In this article we’re going to write common service to be used by our application. This is quite important as to highlight our intention on creating an application that has decoupled and testable components.

Preparing Other Moving Parts

Before we move into our very first business process code, we’re going to prepare some other things. We note that we require some other libraries for this registration module to work.

$ go get -u gopkg.in/gomail.v2
$ go get -u github.com/alexedwards/argon2id
$ go get -u github.com/go-ozzo/ozzo-validation/v4
  • gomail as its name implies, will be used to send email. We’ll be using mailtrap.io for testing purpose. All we need is the email’s hostname, port, username and password.
  • argon2id will be used to generate argon2 hash for our password. It will also be used for verifying account’s password against stored hash in database (when login).
  • ozzo-validation is a tool for validating data, this will be used for server-side validation of user’s input.

Password-Hashing Code

We’ll create a Go file external/extcommon/argon2.go. This file’s purpose is mainly to generate argon2 hash from a plaintext (mostly account’s password) and comparing such hash to the plaintext. Choosing argon2id parameters is outside the scope of this article. For testing purpose, we’re going to use 128 MB of memory with 8 parallelism. Different parameter will generate different hash, so that’s why hash generated by this library will also include all of those parameters for later comparation purpose.

package extcommon

import (
	"github.com/alexedwards/argon2id"
)

type (
	Argon2 interface {
		GenerateHash(plaintext string) (string, error)
		ComparePasswordAndHash(plaintext, hash string) (bool, error)
	}
)

var (
	argonParam = &argon2id.Params{
		Memory:      128 * 1024,
		Iterations:  2,
		Parallelism: 8,
		SaltLength:  16,
		KeyLength:   32,
	}
)

func (rcs *RealCommonService) GenerateHash(plaintext string) (string, error) {
	hash, err := argon2id.CreateHash(plaintext, argonParam)
	if err != nil {
		return "", nil
	}

	return hash, nil
}

func (rcs *RealCommonService) ComparePasswordAndHash(plaintext, hash string) (bool, error) {
	match, err := argon2id.ComparePasswordAndHash(plaintext, hash)
	if err != nil {
		return false, err
	}

	return match, nil
}

Random String Generator

We also need to create a Go method to generate random hash given the needed length. Here’s external/extcommon/random_generator.go

package extcommon

import "math/rand"

const charsetForRandomness = "abcdefghijklmnopqrstuvwxyz0123456789"

type (
	RandomGenerator interface {
		RandomString(length int) string
	}
)

func (rcs *RealCommonService) RandomString(length int) string {
	var seededRand *rand.Rand = rand.New(
		rand.NewSource(rcs.Now().UnixNano()))

	b := make([]byte, length)
	for i := range b {
		b[i] = charsetForRandomness[seededRand.Intn(len(charsetForRandomness))]
	}
	return string(b)
}

Time Utility

And here’s our time utility code external/extcommon/time.go, this separation is needed to ensure code decouplement from something that cannot be controlled. Everything’s that cannot be controlled or random should be decoupled so that we can control them in an isolated/controlled environment (tests).

package extcommon

import "time"

type (
	TimeUtil interface {
		Now() time.Time
	}
)

func (rcs *RealCommonService) Now() time.Time {
	return time.Now()
}

Common Utility Interface

We’ve made 3 common utility interfaces. We’re ready to pack them in an interface external/extcommon/extcommon.go:

package extcommon

type (
	CommonService interface {
		RandomGenerator
		Argon2
		TimeUtil
	}

	RealCommonService struct {
	}
)

Don’t forget external/extcommon/inject.go:

package extcommon

import "sync"

var (
	commonSvcOnce sync.Once
	commonSvc     CommonService
)

func InjectNewCommonService() CommonService {
	commonSvcOnce.Do(func() {
		commonSvc = &RealCommonService{}
	})

	return commonSvc
}

external.go for Packaging External Libraries

Here’s how our external/external.go code look like, External package may consist other things (other than extcommon.CommonService) like http api client, or grpc client or file uploader code.

package external

import (
	extcommon "go-mini-commerce/external/extcommon"
	"log"
	"os"
	"sync"

	"github.com/BurntSushi/toml"
)

type (
	External interface {
		CommonSvc() extcommon.CommonService
	}

	config struct {
	}

	external struct {
		config

		commonSvc extcommon.CommonService
	}
)

var (
	cfgOnce sync.Once
	cfg     config
)

var (
	commonSvcOnce sync.Once
	commonSvc     extcommon.CommonService
)

func (e *external) CommonSvc() extcommon.CommonService {
	commonSvcOnce.Do(func() {
		commonSvc = e.commonSvc
	})

	return commonSvc
}

As we can see above, we don’t build CommonSvc here, because we want to do it from outer call (for better testing/mocking purpose).

And finally we construct External interface inside external/inject.go. This is where we decide if we want to build our extcommon.CommonService (if commonSvc == nil), else just use the supplied commonSvc *extcommon.CommonService, this is useful for mocking from above call.

package external

import (
	"go-mini-commerce/external/extcommon"
	"log"
	"os"
	"sync"

	"github.com/BurntSushi/toml"
)

var (
	externalOnce sync.Once
	externalSvc  External
)

func InjectNewExternalService(
	commonSvc *extcommon.CommonService,
) External {

	var cfg config
	var decidedCommonSvc extcommon.CommonService

	cfgFile, err := os.ReadFile("config/external.toml")
	if err != nil {
		log.Fatal(err)
	}

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

	if commonSvc == nil {
		decidedCommonSvc = extcommon.InjectNewCommonService()
	}

	externalOnce.Do(func() {
		externalSvc = &external{
			commonSvc: decidedCommonSvc,
		}
	})

	return externalSvc
}

Don’t forget to add it to Infra interface in infra.go:

Infra interface {
	PG() DB
	External() external.External
}

// ... skipped for brevity

func NewInfra(
	ext external.External,
) Infra {
	return &infra{
		externalService: ext,
	}
}

// ... skipped for brevity

var (
	externalSvcOnce sync.Once
	externalSvc     external.External
)

func (i *infra) External() external.External {
	externalSvcOnce.Do(func() {
		externalSvc = i.externalService
	})

	return externalSvc
}

Why do we put them in external package? Because we want to decouple them from the main.go.

We are ready to build our first repo code in the next article.