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)
}
})
}
}