How I code in Go in 2019



Arnaud "Arhuman" Assad (arhuman@gmail.com)
Aussi sur Twitter - Linkedin - Github Blog

Things change...

  • I don't code in Go as I used to do it 2 years ago
  • I don't code as I used to do it 5 years ago
  • My programming environment/constraint is very different from what it used to be 30 years ago

Good old days

In 1985

  • Computer resources were scarse (RAM, CPU, language, documentation, expertise)
  • Computer where rare
  • A good program was a working one
  • Basic was so COOL
  • Self-modifying code was even cooler

In 1995

  • The web was the new eldorado
  • Object oriented was the way to Go
  • PHP and Java were invented

In 2005

  • MVC (popularized in 1998) architecture was the state of the art

In 2009

  • Go

Paradigms change

  • Machine Code
  • Procedural languages
  • Object Oriented languages

Even more paradigms

  • Functionnal programming (Haskell)
  • Symbolic Programming (Lisp)
  • Logic programming (Fril, Planner)
  • Constraint programming (Prolog)
  • Event-driven programming (Fril, Planner)

How is Go different?

  • Not Object Oriented
    • Interface instead of inheritance
    • Struct instead of Class
    • Polymorphism
  • Multiple return values
  • Simple
  • Opiniated
    • Coding Style
    • Community values
    • Programming values

Coding Style

  • gofmt
    • Enforced by tooling
    • The good coding style is the one used by everybody
    • Freed from details we can focus on what matters

Community values

  • Concise and NOT Verbose
  • Genuine and NOT Dubious
  • Friendly and NOT Exclusive
  • Direct and NOT Ambiguous
  • Thoughtful and NOT Reactive
  • Humble and NOT Haughty

Programming values

  • Thoughtful (Deliberate and considerate)
  • Simple (Clear and precise)
  • Efficient (Do more with less)
  • Reliable (It just works)
  • Productive (Realize your vision, faster)
  • Friendly (Accessible and welcoming)

Programming the Go way

  • Simple things
  • Consise things
  • Non ambiguous things
  • Do more with less
  • In a productive way

Simple things

Definitely not like this


#!/usr/bin/perl

$|=1;$N=shift||100;$M=int(3.3*$N)
;$t[0]=2;$s[0]=2;sub z{$a=$r=00};
for(                         $k=1
;$k<$M   ;$k++){$a=$r=0;   };for(
$k=01;   $k<$M;$k++){&z;   for($i
=$N;$i   >=0;$i--){$a=$t   [$i]*(
$k)+$r   ;$t[$i]=int($a%   10);${
r}=int   ($a/10);}$K=($k   <<1)+1
;&{z};   map{$a=$t[$_]+(   10)*((
$r));;   ${t}[$_]=int($a   /($K))
;${r}=   int(($a)%($K))}   (0..$N
);if((   $r>=(int(($K)/2   )))){;
;;${t}   [$N]++}while($t   [$N]>9
){${t}   [$N]-=10;;$t[$N   -1]++}
&z();   for($i=$N;$i>=0   ;$i--){
$a=    ($t[$i]+$s[$i]+   $r);${s}
[$i]=int($a%10);$r=int($a/10)}if(
$k>$x+3){${x}=$k;;print"$s[$y++]"
;};}for($y..${N}){print"$s[$_]";}
                        

Simple things

Happy path


if !something.OK() {  // flipped
        return errors.New("something not ok")
}
err := something.Do()
if err != nil {       // flipped
        return err
}
doWork()
log.Println("finished")
return nil
                        

Simple things

Complex path


if something.OK() {
        err := something.Do()
        if err == nil {
                doWork()
                log.Println("finished")
                return nil
        } else {
                return err
        }
} else {
        return errors.New("something not ok")
}
                        

Non ambiguous things

Now


err := doSomething()
if err != nil {
        return err
}
                        

Non ambiguous things

Now


err := doSomething()
if err != nil {
        log.Error("[functionName] Can't do something: "+ err.Error())
        return err
}
                        

Do more with less

  • What is the recommended framework?
  • What is the recommended standard module?

The big question

What is the recommended Architecture for Go projects?

Make it as simple as possible but not simpler -- Albert Einstein

Code layout

Project layout

  • Executables in /cmd (daemon, server, command line tools)
  • Documentation in /docs (daemon, server, command line tools)
  • One README.md file
  • Dependencies in /vendor
  • Configuration in /configs
  • System and container orchestration deployment configurations /deployments

Go 1.13 and modules

  • dependencies in /vendor
  • go.mod and go.sum (both) versionned

Documentation

  • Documentation in /docs
  • Documentation in /doc
  • One README.md file
  • One INSTALL.md file

What is a good architecture

  • Uniform
  • Natural, easy to understand
  • Easy to modify, loose coupling
  • Easy to test

Good architecture

Clean Architecture

Clean Architecture concept

  • Code in layers
  • Change in one layer should not affect another layer
  • Contract between layers

Clean Golang Architecture

  • Use of model layer
  • Use of repositorylayer
  • Use of usecase layer
  • Interfaces are the contract between layer
  • Use of dependency injection

Directory layout

                        
go.mod
go.sum
config/
      db/
        project-tables.sql
models/
      init.go
      alias.go
      links.go
alias/
     repository.go
     repository/
               gorm_alias.go
               gorm_alias_test.go
               pg_alias.go
               pg_alias_test.go
     usecase.go
     usecase/
            alias_usecase.go
            alias_usecase_test.go
link/
     repository.go
     repository/
               gorm_link.go
               gorm_link_test.go
               pg_link.go
               pg_link_test.go
     usecase.go
     usecase/
            link_usecase.go
            link_usecase_test.go
                        
                        

Model

  • Lowest layer
  • Data definition
  • Can't call any layer
  • Struct definition

Model example: models/alias.go


package models

import "time"

// Alias structure holds the information about email aliases
type Alias struct {
    ID           int64     `json:"id"`
    IDAccount    int64     `json:"id_account"`
    Email        string    `json:"email"`
    DstEmail     string    `json:"dst_email"`
    Class        string    `json:"class"`
    Permanent    bool      `json:"permanent"`
    DateCreation time.Time `json:"date_creation"`
    Active       bool      `json:"active"`
    DateEnd      time.Time `json:"date_end"`
}
                        

Model specificity: models/init.go


...

// Init initializes a connection to the database and returns a database handle and an error
func Init(l *logrus.Logger) (*gorm.DB, error) {
    log = l
 
    err := godotenv.Load(".env")
    if err != nil {
        log.Infof("INFO: unable to load .env file: %s", err)
        return nil, err
    }
 
    db, err := gorm.Open("postgres", getConnectString())
    // db, err := gorm.Open("sqlite3", "/tmp/project.db")
    if err != nil {
        return nil, err
    }
 
    log.Infof("Successfully connected to 'database' %s on host %s as user '%s'", name, host, user)
 
    return db, err
}

...
                        

Repository

  • Data access
  • Use models
  • No business logic

Repository interface: alias/repository.go


package alias

import (
    "mailcape/models"
)

// Repository represent the alias repository contract
type Repository interface {
    GetAll() ([]*models.Alias, error)
    GetByID(id int64) (*models.Alias, error)
    GetByEmail(title string) (*models.Alias, error)
    Update(a *models.Alias) error
    Store(a *models.Alias) error
    Delete(id int64) error
}

                        

Repository implementation: alias/repository/gorm_alias.go


package models
package repository

import (
    "mailcape/alias"
    "mailcape/models"

    "github.com/jinzhu/gorm"
)

type gormAliasRepository struct {
    db *gorm.DB
}

// NewGormAliasRepository will create an object that represent the alias.Repository interface
func NewGormAliasRepository(db *gorm.DB) alias.Repository {
    return &gormAliasRepository{db}
}

func (repo *gormAliasRepository) Delete(id int64) error {
    alias := new(models.Alias)
    alias.ID = id
    if err := repo.db.First(&alias).Error; err != nil {
        return err
    }

    if err := repo.db.Delete(alias).Error; err != nil {
        return err
    }

    return nil
}

func (repo *gormAliasRepository) GetAll() ([]*models.Alias, error) {
    var aliases []*models.Alias
    if err := repo.db.Find(&aliases).Error; err != nil {
        return nil, err
    }
    return aliases, nil
}

...
                        

Usecase

  • Business logic
  • Also called service
  • Use repository
  • Use models

Usecase interface: alias/usecase.go


package alias

import (
    "mailcape/models"
)

// Usecase for alias
type Usecase interface {
    Delete(id int64) error
    GetAll() ([]*models.Alias, error)
    GetByID(int64) (*models.Alias, error)
    GetByEmail(string) (*models.Alias, error)
    Store(*models.Alias) error
    Update(*models.Alias) error
    Special(*models.Alias) error
}
                        

Usecase implementation: alias/usecase/alias_usecase.go


package usecase

import (
    "mailcape/alias"
    "mailcape/models"
)

type aliasUsecase struct {
    repo alias.Repository
}

// NewAliasUsecase returns a service to manipulate Alias
func NewAliasUsecase(r alias.Repository) alias.Usecase {
    return &aliasUsecase{repo: r}

}

func (a *aliasUsecase) Delete(id int64) error {
    return a.repo.Delete(id)
}

func (a *aliasUsecase) Special(id int64) error {
   // Do whatever you wan using alias repository here
   return nil
}
                        

Finally inject dependencies to use


import (
    aliasRepoF "mailcape/alias/repository"
    aliasServiceF "mailcape/alias/usecase"
    goalRepoF "mailcape/goal/repository"
    goalServiceF "mailcape/goal/usecase"
    objectiveRepoF "mailcape/objective/repository"
    objectiveServiceF "mailcape/objective/usecase"
    projectRepoF "mailcape/project/repository"
    projectServiceF "mailcape/project/usecase"

    "github.com/jinzhu/gorm"
    "github.com/sirupsen/logrus"
)

// In my program/API
func Init(d *gorm.DB, l *logrus.Logger) {
    db = d
    log = l

    aliasRepo := aliasRepoF.NewGormAliasRepository(db)
    aliasService = aliasServiceF.NewAliasUsecase(aliasRepo)

    goalRepo := goalRepoF.NewGormGoalRepository(db)
    goalService = goalServiceF.NewGoalUsecase(goalRepo)

    objectiveRepo := objectiveRepoF.NewGormObjectiveRepository(db)
    objectiveService = objectiveServiceF.NewObjectiveUsecase(objectiveRepo)

    projectRepo := projectRepoF.NewGormProjectRepository(db)
    projectService = projectServiceF.NewProjectUsecase(projectRepo)
}

                        

Why Devops?

  • Inline of Go objective
    • Bring order to the complexity of creating and running software at scale
  • Many tools written in Go
  • Ease scaling
  • Ease administration
  • It's the 21st Century

Go / Devops affinity

  • Many devops tools written in Go
    • Docker/Traefik
    • Gitea
    • Drone
  • Go compile to a single easy to deploy binary
  • Better dependencies

Multistage docker file


                        FROM golang:1.11 as builder         # build stage

                        WORKDIR /api                        # setup the working directory

                        COPY go.*  /api/                    # install dependencies
                        RUN go mod download                 # in a specific layer (cache optimisation)

                        ADD . .                             # add source code
                        RUN CGO_ENABLED=0 GOOS=linux go build -a  -o server ./cmd/daemon/main.go
                        # build the source

                        FROM alpine:3.7                     # use a minimal alpine image
                        # FROM gcr.io/distroless/base       # In prod use distroless

                        RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*

                        WORKDIR /root                       # set working directory

                        COPY --from=builder /api/server .   # copy the binary from builder

                        EXPOSE 8080                         # expose the port

                        CMD ["./server"]                    # run the binary
                        

What I don't do (properly) yet

  • Error wrapping
  • Proper testing
  • Documentation generation

What I will have to do

  • Handle generics
  • Adapt change layout/namespaces
  • Functional programming (immutability)
  • Publish modules

Thanks


Questions ?



Don't hesitate to join my network on Linkedin

Community