Skip to content

Go Coding Style Guide

This Go coding style guide is adapted from The Effective Go.

Code Documentation

Package Documentation

Every package must have a package comment, a block comment preceding the
package clause. In multi-file packages the comment must only be present
in one file.

The comment must start with: "Package name"

// Package kafka handles connecting and listening to kafka
package kafka

Functions

When commenting a function there is no need for a description of input
parameters or return value as the names itself in combination with its
type should provide that information.

//RemoveByAge removes logs which are older than given age.
func RemoveByAge(logBefore []LogEntry, age time.Duration) []LogEntry {

Other elements

Every element which is being exported (capitalized name) must have a
comment. The comment must start with the name of the element. This
refers to e.g. structs and constants.

//SomeType stores some information to  
// do something
type SomeType struct{

}

Formatting

Formatting in titan is being done by using go formatter. Code must be
formatted in that matter before pushing.

This can be done by running:

go fmt ./...

Naming

In go we use mixedCaps or MixedCaps for multi word names. The capitalization
indicates whether the object is public or private.

Include (SI-)Units in Names

If a variable represents time, weight, or some other unit then include the unit in the name so developers can more easily spot problems.

var intervalMin

Code quality checks

Quality of go code is checked using the revive linter for Golang.

The rules are defined in revive.tomlin the flowengine-Go project's root directory.

To execute code quality checks run :

revive -config revive.toml -formatter friendly ./...

Please refrain from disabling rules in the code.

Unit Testing

Golang has build in unit testing library that help to write/execute unit test.

Unit test Name

For each package it is preferred to have one test file. Unit tests' name in the output should follow:

"Test FuncName StateUnderTest ExpectedBehavior"

Example

"Test RemoveByAge AgeLessThanZero nil"

Table Driven Unit testing

Table driven testing
is a popular technique to write unit tests. Thereby, each table entry is a
complete test case with inputs and expected results and a test name to ease
the identification of test cases in the test output. Given a table of test
cases, the actual test simply iterates through all table entries and for each
entry performs the necessary tests. The test code is written once and
evaluated over all table entries, thus follows the
DRY "Don't Repeat Yourself" concept.

Example for a table driven test

func TestUserValidate(t *testing.T) {
   type fields struct {
      FirstName string
      LastName  string
      Age       int
   }
   //Table of test cases
   tests := []struct {
      name    string
      fields  fields
      wantErr bool
   }{
      {
         name: "validate HasAllFields nil",
         fields: fields{
            FirstName: "demo",
            LastName:  "demo",
            Age:       33,
         },
         wantErr: false,
      },
      {
         name: "validate EmptyFirstname error",
         fields: fields{
            FirstName: "",
            LastName:  "demo",
            Age:       33,
         },
         wantErr: true,
      },
   }
   for _, tt := range tests {
      t.Run(tt.name, func(t *testing.T) {
         u := &User{
            FirstName: tt.fields.FirstName,
            LastName:  tt.fields.LastName,
            Age:       tt.fields.Age,
         }
         if err := u.validate(); (err != nil) != tt.wantErr {
            t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr)
         }
      })
   }
}

Dependency Injection

If we want to test a function depends on another function, DI needs to be
implemented to make unit testing possible. In Go we can do that by using
an interface that exposes the public variable of interface type.

Example for dependency injection

For example, we want to inject a product to a shopping basket:

Product interface

package product

// Expose the Product variable as type of Interface of Product
var (
    ManageProduct IProduct
)

// Product holds the characteristics 
// of individual goods
type Product struct {
    Id    int
    Name  string
    Price float64
}

type manageProduct struct{}

// IsAvailable checks the products' availability in the database 
func (*manageProduct) IsAvailable(id int) (Product, error) {
    if id < 1 {
        return Product{}, errors.New("id can not be 0 or less than 0")
    }
    return Product{
        Id:    12,
        Name:  "Laptop",
        Price: 890.99,
    }, nil
}

//IProduct is the interface representation
// of Product for unit testing
type IProduct interface {
    IsAvailable(id int) (Product, error)
}

// init function is called if this package is not initialized
func init() {
    ManageProduct = &manageProduct{}
}

Shopping Basket
package basket

//Checkout products from the shopping basket
func Checkout(p product.Product) (product.Product, error) {
    product, err := product.ManageProduct.IsAvailable(p.Id)
    if err != nil {
        return product, errors.New("failed to get the product")
    }
    return product, nil
}

Unit test for the checkout function

// Mock product
type mockManageProduct struct{}

var (
   isAvailableFunc func(id int) (product.Product, error)
)

func (*mockManageProduct) IsAvailable(id int) (product.Product, error) {
   return isAvailableFunc(id)
}

func TestCheckout(t *testing.T) {
   // Inject the mock struct to the ManageProduct
   product.ManageProduct = &mockManageProduct{}

   newProduct := product.Product{
      Id:    12,
      Name:  "Laptop",
      Price: 890.99,
   }

   notAvailableProduct := product.Product{}
   type args struct {
      p product.Product
   }
   tests := []struct {
      name            string
       // for each unit test we can expect different return from isAvailable func
      isAvailableFunc func(id float64) (product.Product, error) 
      args            args
      want            product.Product
      wantErr         bool
   }{
      {
         name: "Checkout productIsValid product",
         isAvailableFunc: func(id float64) (p product.Product, err error) {
            return newProduct, nil
         },
         args:    args{p: newProduct},
         want:    newProduct,
         wantErr: false,
      },
      {
         name: "Checkout productNotValid product",
         isAvailableFunc: func(id int) (p product.Product, err error) {
            return notAvailableProduct, errors.New("not valid")
         },
         args:    args{p: newProduct},
         want:    notAvailableProduct,
         wantErr: true,
      },
   }
   for _, tt := range tests {
      // injected the mock function for each test case
      isAvailableFunc = tt.isAvailableFunc
      t.Run(tt.name, func(t *testing.T) {
         got, err := Checkout(tt.args.p)
         if (err != nil) != tt.wantErr {
            t.Errorf("Checkout() error = %v, wantErr %v", err, tt.wantErr)
            return
         }
         if !reflect.DeepEqual(got, tt.want) {
            t.Errorf("Checkout() got = %v, want %v", got, tt.want)
         }
      })
   }
}