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.