3. Go Coding Standard
Contents
For quick access, use the content links below.
- Why?
- Introduction
- Naming conventions
- Pointers & references
- Struct, file & package organization
- Comments
- Code formatting
- General coding guidelines
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")
}