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
, andz
for looping. Noti
,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
ordefXXX
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.