3. Go Coding Standard

Contents

For quick access, use the content links below.

Why?

This document is meant to reflect the coding standard used in the KatApp backend environment. Following this coding standard is mandatory.

Why are coding standards important?

  • 80% of the lifetime cost of a software application goes into maintenance.
  • Hardly any software is maintained for its whole life by the original author.
  • Coding conventions improve the readability of software, allowing developers to understand new code more quickly.

Introduction

This document outlines a Go coding standard. This coding standard applies to the KatApp backend. All backend services are defined to be written in Go.

Naming conventions

General Conventions

  • All code and comments should use english spelling and grammar
  • Type and variable names are preferred to be nouns
  • Method names are preferred to be verbs, as they describe the purpose of a method

Names

Names are as important in Go as in any other language. They even have semantic effect: the visibility of a name outside a package is determined by whether its first character is upper case. So, keep this in mind following this coding standard.

Semicolons

Like C, Go’s formal grammar uses semicolons to terminate statements, but unlike in C, those semicolons do not appear in the source. Instead the lexer uses a simple rule to insert semicolons automatically as it scans, so the input text is mostly free of them.

The rule is this. If the last token before a newline is an identifier (which includes words like int and float64), a basic literal such as a number or string constant, or one of the tokens the lexer always inserts a semicolon after the token. This could be summarized as, “if the newline comes after a token that could end a statement, insert a semicolon”.

As long as it isn’t necessary, do not use semicolons in the code.

The blank identifier

The blank identifier (_) can be assigned or declared with any value of any type, with the value discarded harmlessly. It’s a bit like writing to the Unix /dev/null file: it represents a write-only value to be used as a place-holder where a variable is needed but the actual value is irrelevant. It has uses beyond those we’ve seen already.

Use the blank identifier whereever it’s necessary to discard a value, which is specifically most often the case in multi-assignments. Single assignment variables shouldn’t be declared with the blank identifier, as this code is unused in release builds anyway.

Unused imports & variables

In Go, unused imports and variables automatically lead to compilation errors. Therefore, developers are forced to remove unused code. However, be careful not to comment out unused variables and leave them in the code without a comment explaining why the variable was left there.

Packages

Fundamentally, all package names are written in lower case letters. Ideally, package names do not use under_scores or mixedCaps. Package names should be singleword names.

Examples:

package io
package main
package http

Another convention is that the package name is the base name of its source directory. This means, a package in src/encoding/base64 is imported as encoding/base64 but has the name base64, not encoding_base64 and not encodingBase64.

Types

Fundamentally, all type names are written in either PascalCase or camelCase, depending on whether they are exported from the package.

Examples:

// Example for a public struct.
type PublicStruct struct {
 Value string
}

// Example for a private struct.
type privateStruct struct {
 Value float64
}

Type definitions & aliases

Supplementary to the above rules, all type definitions including type aliases are written in PascalCase or camelCase, depending on their visibility. We recommend to use the = syntax, to keep code as uniform as possible.

// Bad
type Dummy int
type Alias float64
type privateType int

// Good
type Dummy = int
type Alias = float64
type privateType = int

Interfaces

Interfaces follow the same rules as the type naming conventions. As a consequence, public interfaces are written in PascalCase and private interfaces are written in camelCase, although private interface do not make much sense in most of the cases.

type Runnable interface {
    Execute() int
}

Enums

In Go, there’s no native enum type. Enumerations in Go are declared as a group of constant variables. For enum declarations the we encourage the use of the predeclared identifier iota, which represents a successive untyped integer constant. It is automatically reset to 0 whenever a new const declaration begins.

All enums as well as their members, depending on their visibility, are either written in camelCase or in PascalCase.

const (
  // iota is reset to 0
  Value0 = iota // Value0 == 0
  Value1 = iota // Value1 == 1
  Value2 = iota // Value2 == 2
)

const (
  // iota is reset to 0
  A = 1 << iota // A == 1
  B = 1 << iota // B == 2
  C = 1 << iota // C == 4
)

const (
  // iota is reset to 0
  U         = iota * 42 // U == 0     (untyped integer constant)
  V float64 = iota * 42 // V == 42.0  (float64 constant)
  W         = iota * 42 // W == 84    (untyped integer constant)
)

const PublicX = iota   // x == 0 (iota has been reset)
const privateY = iota  // y == 0 (iota has been reset)

Constants

Constant variables are declared just like enums. They also follow the common pattern of public/private casing. Public constants are written in PascalCase, private constants are written in camelCase.

If you declare multiple constants in one block, make sure that they all have the same visibility, do not declare publc and private constants in the same const block.

Examples:

// Valid:
const PublicConstant = 3
const privateConstant = 2

// Good:
const (
  ConstantVariable1 = 1
  ConstantVariable2 = 2
)

// Good:
const (
  constantVariable3 = 3
  constantVariable4 = 4
)

// Bad:
const (
  ConstantVariable5 = 5
  constantVariable6 = 6
)

Functions

Fundamentally, all function names are either written in PascalCase, or in camelCase, depending on whether the function is exported from its package.

Examples:

// Declares a public function.
func PerformPublicAction() {

}

// Declares a private function.
func performPrivateAction() {

}

Variables

Fundamentally, all local variables are written in camelCase.

Example:

func DoSomething() {
    // Good:
    someValue := 3
    anotherValue := 4

    // Bad:
    BigValue := 999
}

Moreover, all function parameters are generally written in camelCase as well.

Example:

// Good:
func DoSomething(value int) { }

// Bad:
func DoSomething(Value int) { }

Furthermore, type member variables are written either in PascalCase or in camelCase, depending on whether they are private or public.

Examples:

type PublicStruct struct {
 PublicValue string
 privateValue string
}

Global variables, which are globally visible within one package, i.e. they are not exported from the package, are written in camelCase, whereas exported global variables are written in PascalCase.

// Exported from the package:
var PublicGlobal = 3

// Only visible within the package:
var privateGlobal = 3

The var Keyword

Variables in Go can be declared by either using the var keyword or by using the special assignment operator :=. To keep the consistent as possible, omit the var keyword wherever possible and use the := operator instead.

Pointers & references

Pass by value & pass by reference

It is important to understand how Go passes on variables. Per default, Go passes variables and objects by value. This means, every time a variable/an object is passed, a local copy of this variable/object is created. This concept is very important, as it does not allow the developer to modify the original object.

If you need to modify the original object, you can pass variables/objects by reference. This means the passed variable/object has to be a pointer to that variable/object.

Smaller objects are typically passed by value, as this more often than not results in faster code. Very big objects should be passed by reference to improve performance, except when a copy is needed anyway.

Object initialization

In Go, objects can be created in two different ways. You can either create an instance of a type on the go, or you can use the new keyword.

We prefer to use the first style, as this makes it easier to directly assign member fields.

func Dummy() {
  // Good:
  result := &DummyType{}

  // Valid, but not preferred:
  result := new(DummyType)
}

Recommendations

Generally, we recommend to use pass by value, except for the following cases:

  • The variable/object needs to be modified within a function

Also, if you are working with very complex objects, pass by reference is recommended, as it prevents useless copying of the data. This can impact performance, especially, if performed in a loop.

Struct, file and package organization

File organization

Typically, in each go file, the package declaration goes first. However, afterwards, no specific definition order is provided by Go itself, so this guide defines its own declaration order to ensure common file organization.

In each file, all type defintions go first. This means that all structs defined in this file will be declared at the top. If a file declares new type aliases, make sure to declare them at the very top, even before the struct declarations. All global variables in a file should be declared right after the type alias declarations. All functions go at the bottom of the file.

For all declaration types listed above there is the following rule: public declarations are always listed before private declarations.

Here’s an example of how to organize Go files:

package example

// Type aliases:
type PublicIntAlias int
type privateFloatAlias = float64

// Global variables:
var (
  PublicGlobalValue int = 3
  internalGlobalValue int = 4
)

// Structs:
type PublicStruct struct {
  PublicValue string
  privateValue string
}

type privateStruct struct {
  PublicValue string
  privateValue string
}

// Functions:
func PublicHandleSomething() {

}

func privateHandleSomething() {

}

Comments

Go provides C-style /* */ block comments and C++-style // line comments. Line comments are the norm; block comments appear mostly as package comments, but are useful within an expression or to disable large swaths of code.

Comments that appear before top-level declarations, with no intervening newlines, are considered to document the declaration itself.

Moreover, there are some general comment guidelines, which also apply to this project.

Guidelines

  • Write self documenting code

    // Bad:
    d := t - s;
    
    // Good:
    directionVector := targetVector - sourceVector;
  • Write useful comments

    // Bad:
    // increment position
    ++position;
    
    // Good:
    // we know there is another position
    ++position;
  • Do not comment bad code, rewrite it

    // Bad:
    // the direction is the difference between
    // the source position and
    // the target position
    d := t - s;
    
    // Good:
    directionVector := targetVector - sourceVector;
  • Do not contradict code

    // Bad:
    // never increment position!
    ++position;
    
    // Good:
    // we know there is another position
    ++position;

Usage

To keep comments consistent across the code base, we follow some simple rules. Those rules are represented by the following examples.

Package comments

Use mutli-line comments exclusively for package descriptions.

/*
This text should describe what this package is responsible for.
*/
package log

Type & Alias comments

For types and aliases we use description blocks within the comments. go fmt is automatically formatting indented comments to keep a newline before the indented block. Such blocks make it way easier to read the comments in the code as well as a possible IDE preview.

For type fields, we recommend to use short, descriptive comments only.

// Description:
//
//  This text should describe what this alias is used for.
type Alias = int

// Description:
//
//  This text should describe what this alias is used for.
//
// Fields:
//
//  Name: Short comment here.
//  Message: Another short comment here.
type Dummy struct {
  Name string
  Message string
}

Function comments

Function comments usually consist of 3 blocks, the Description block, the Parameters block and the Returns block. Every function should at least have a description. Parameter and return blocks are only necessary if the function receives parameters or returns values respectively.

// Description:
//
//  This text should describe what this function actually does.
//
// Parameters:
//
//  input: Short description about the parameter here.
//
// Returns:
//
//  This block should provide a short description about the return 
//  value and/or possible errors which can occur.
func DoSomething(input string) (string, error) {
  ...
}

Type parameter comments

Structs as well as functions can also use type parameter comments, typically, they are declared right after the description:

// Description:
//
//  This text should describe what this alias is used for.
//
// Type Parameters:
//
//  T: Short comment here.
// 
// ...
type Dummy[T interface{}] struct {
  Value T
}

// Description:
//
//  This text should describe what this function actually does.
//
// Type Parameters:
//
//  T: Short comment here.
//
// ...
func DoSomething[T interface{}](input string) (string, error) {
  ...
}

Local comments

For local comments, follow the general comment guidelines.

Code formatting

This guide follows the official recommandation to use the inbuilt Go formatter. To format your Go files use go fmt.

General coding guidelines

Those are some general, recommended but not mandatory coding guidelines. Those guidelines can make code more pleasant to read and are therefore encouraged to be used.

Code aesthetics

To make code more readable, it is recommended to leave some blank lines in between code, which mark logical and visual separation between code lines. This not only makes reading code much easier, but also improves the debugging experience.

It is pretty much impossible to write down hard rulesets which guide a programmer where to leave those spaces. Therefore, these blank lines are inserted according to the personal judgement and taste of the programmer.

Example:

// Bad
func Health(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "application/json")
  _, err := io.WriteString(w, `{"alive": true}`)
  if err != nil {
    log.Error(err)
  }
  log.Info("Healthcheck run")
}

// Good
func Health(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "application/json")

  _, err := io.WriteString(w, `{"alive": true}`)
  if err != nil {
    log.Error(err)
  }

  log.Info("Healthcheck run")
}