kilabit.info
Build | GitHub | Mastodon | SourceHut |

In general Go already have gofmt that will format the code according to Go standard. Developers should already used this tool in their editor/IDE. This section describe informal coding style, that is not covered by Go format tool.

The following recommendation is subjective. If you work in large code base, with more than three developers, you should already have a common "language" between them, to make it consistent and readable.

Group imports

Imported packages should be grouped and ordered by system, third party, and then our packages. Each group separated by empty line. For example,

import (
	"os"
	"net/http"

	"third/party/library"

	"github.com/yourrepo/yourlib"
)

Structure the code as in Godoc layout

If you looks at the Godoc layout, each sections is ordered by the following format,

  • package description

  • package constants

  • package global variables

  • package global functions

  • type

  • type’s methods (ordered alphabetically)

Builtin functions, like init(), main(), and TestMain() should be at the bottom of the source code. As an example see net package [1].

Rationale: Following godoc format will make code easy to read, because we know where each of section is located.

Package should have a file with the same name

Package mypkg should have source file with the name mypkg.go. This file is used for documentation, declaring global variables, constants, and/or maybe init() function.

Rationale: easy to search where global variables, constants, and init() defined.

One type (struct/interface) per file

The filename should follow the name of the type. For example, package X have two exported structs: Y and Z. So, in the directory X there would be two files: y.go and z.go.

Rationale:

  • Easy to search where type is defined

  • Modularization by files

Define field name when creating struct instance

Bad practice:

x := ADT{
	"a",
	"b",
	"c",
}

Good practice:

x := ADT{
	name: "a",
	phone: "b",
	address: "c",
}

Rationale:

  • Prevent miss-assigned field value when refactoring struct. For example, new field "firstname" and "lastname" added the top of declaration, the "Bad" example still work but may not what developer wants.

  • Easy to read.

Use short variable names if possible

Common short variable names,

  • x, y, and z for looping. Not i, j, etc. because its prone to typo, and let more than three deeps looping (which is a signal for bad programming) and its not easy for quick reading.

  • err for error variable

  • ctx for context

  • req for client request

  • res for server response

  • msg for general message input/output

Common prefix for variable or function,

  • jXXX for message in JSON struct

  • bXXX for message in slice of bytes ([]byte)

  • DefXXX or defXXX for default variable/constanta

Rationale:

  • Searchability, find-and-replace with three characters is more easy than single character.

  • Readability, knowing what variable hold can help reader on longer function body.

Comment grammar

In Go, exported field or function denoted by capital letter on the first letter, and it should have comment.

For field (on struct, var, or const) the recommended comment format is by using "define" or "contains" verb after variable name.

For example,

// DefPort define the default port to listen on ...
var DefPort = 9002

If the function or method return an error, explain what cause them.

For example,

// GetEnv read system environment name `envName`.
//
// It will return an error if v envName is empty.
func GetEnv(envName string) (v string, err error) {
	...
}

Package that create binary should be in "cmd" directory

One of the things that I learned later in software development was when writing code, pretend that your code will be used by other developers, which means, write library first, program later. This is a mistake that we have been taught since college, because we learn to write program not library.

Go, in subtle way, embrace this kind of thinking when developing software.

Log on service level, not on library

Let say that we have HTTP service on package service/myhttp that use package account on the same module.

On myhttp package, we call function Get on package account,

package myhttp

import "account"

func handleGet(...) {
	...

	acc, err = account.Get(...)

	...
}

In package account we should not log any error like these,

package account

func Get() (Account, error) {
	...
	err = F()
	if err != nil {
		log.Printf("Get: %s", err)
		return nil, err
	}
	...
}

Instead, pass the error context inside the returned error to be logged by myhttp or any top packages that import it,

	...
	if err != nil {
		return nil, fmt.Errorf("account.Get: %w", err)
	}
	...

Rationale: A good library should not print any output, error or not. Centralizing the error on service level help us to forward the error to other output/services without modify or import third party module on library level.

Avoid ":=" if possible

Why?

First, when I read an unknown code, a code that I am unfamiliar with; inside the function/method body it call a function and return variable assigned with ":=", it is quite hard to derive what the return type is without checking the function/method signature.

Case in example,

x := f()

To know what type of x, I need to search and check the signature of f. If we declare they variable type, it will save time for reader.

var x T = f()

Second, there are another case where declaring variables before may minimize number of temporary variables.

Case in example,

paramX := form.Get("X")
request.X = convert(paramX)
...
paramY := form.Get("Y")
request.Y = convert(paramY)
...

The paramX and paramY are string. If we declare temporary variable before, we can save unneeded variable,

var param string
param = form.Get("X")
request.X = convert(param)
...
param = form.Get("Y")
request.Y = convert(param)
...

Third, the ":=" cause variable shadowing, and this sometimes cause subtle bugs and not-easy to read code [1][2].

Use raw literal string when possible

Raw literal string use backtick (``) and its read the string as is, which means in the compiler perspective no additional post-processing need to store the string in the stack.

It may improve the build time, but I don’t have the data or code to support this, so take this with grain of salt.

Create test Example over unit test if possible

A test Example, the function in _test.go file that have Example prefix, is also unit test. By creating Example on exported APIs, we also create an example in documentation. So, pretty much killing two birds with one stone.

Test example should be on separate file

If package x has test file x_test.go, any test Example for that package should be created under file x_example_test.go.

This is to allow the file to use different package name x_test instead of x (see below) and searchable by human eyes.

Use _test suffix in package name for Example

Package name in test Example should be different with the actual package. This is to minimize leaking the exported APIs in Example.