Wikipedia states that a best practice is a method or technique that has been generally accepted as superior. Unfortunately, personal preference can influence what is considered superior. Individuals may value different aspects, such as performance or readability, and trade-offs are common. Nevertheless, it is crucial that your team align on shared conventions.
In this blog post, I will share a list of best practices and conventions that I follow and hope can benefit you. I encourage you to leave comments with any changes or additions as this list will be regularly updated and credit will be given.
Table Of Contents
Introduction
Following best practices can enhance codebase readability, maintainability, and performance. Numerous style guides are available for Go, including:
The conventions listed in this guide have been influenced by the resources above. However, I have adopted a distinct approach that focuses on addressing common, everyday concerns. While gofmt and your linter can resolve surface-level issues, we will be digging deeper than that.
Style
Package Names
Single short and concise word with no mixed case or underscores. Use abbreviations and do not worry about future package name clashes.
// bad
package user_repository
// good
package repo
GoClashes are handled by the importing package. Do not use underscores or camelCase. Often the parent directory name can be used as a prefix or suffix.
// bad
import (
user_repo "XXX/repo/user"
userSvc "XXX/svc/user"
schema_user "XXX/schema/user"
)
// good
import (
userrepo "XXX/repo/user"
usersvc "XXX/svc/user"
schemauser "XXX/schema/user"
)
GoDirectory Names
Should equal the package name and take into account the context of the parent directory.
package repo
// bad
// user/userrepo/repo.go
// good
// user/repo/repo.go
GoFilenames
Single short and concise abbreviated word. If this is not possible, then separate by an underscore to match the same convention as test filenames.
// bad
// repository.go
// userRepo.go
// good
// repo.go
// user_repo.go
GoFilenames do not need to match the folder name and should take into account the context of the parent directories.
package repo
// bad
// repos/user_repo.go
// good
// repos/user.go
GoVariable Names
Single short and concise words using well-known abbreviations and mixed caps.
// bad
var repository Repository
var repository_name string
// good
var repo Repo
var repoName string
GoVariable Declaration
There are a number of ways to declare variables in Go. As a result, it is better to stick to a convention.
// bad
person := Person{}
var person = Person{Name: "TheGoDev"}
// good
var person Person
person := Person{Name: "TheGoDev"}
GoAvoid declaring new variables as pointers as it is better to see the effect of passing a parameter as a pointer.
// bad
person := &Person{Name: "TheGoDev"}
ProcessPerson(person)
// good
person := Person{Name: "TheGoDev"}
ProcessPerson(&person)
GoFunction Names
Getters in Go are encouraged, but we should remove the Get in the function name. The receiver variable should be a single letter.
// bad
func (person *Person) GetName() string {
return person.Name
}
// good
func (p *Person) Name() string {
return p.Name
}
GoInterface Names
Should be a verb and not use abbreviations.
// bad
type Operations interface {
// ...
}
type Ops interface {
// ...
}
// good
type Operator interface {
// ...
}
GoInclude all parameter variable names of the methods in an interface except for the return values.
// bad
type Operator interface {
Create(context.Context, string, string) error
}
type Operator interface {
Create(ctx context.Context, userID string, itemID string) (err error)
}
// good
type Operator interface {
Create(ctx context.Context, userID string, itemID string) error
}
GoMap Names
When checking if a map contains a key, name the variable ok
.
// bad
name, nameIsPresent := userMap["TheGoDev"]
name, isPresent := userMap["TheGoDev"]
name, exists := userMap["TheGoDev"]
// good
name, ok := userMap["TheGoDev"]
GoHealth Check Names
Health Check packages and endpoints should end with a z
. Google did this to avoid name clashes with their many existing packages, but the convention stuck.
// bad
package health
// endpoint - /health/ready
// good
package healthz
// endpoint - healthz/readyz
GoMock Package Names
Should have a suffix of _mock
to follow the same convention as tests and to avoid accidental import by the main application.
// bad
package repomock
package repoMock
package mock_repo
// good
package repo_mock
GoGo Docs
The package’s Go Doc comment should always use the format /**/
over //
.
// bad
// Package handler contains all of the handlers for a user.
// These are the endpoints that are exposed to the client and are
// responsible for parsing the request, calling the service, and
// returning the response.
// good
/*
Package handler contains all of the handlers for a user.
These are the endpoints that are exposed to the client and are
responsible for parsing the request, calling the service,
and returning the response.
*/
GoThe synopsis for a package should be high-level, short, and concise. We can go into detail later.
// bad
/*
Package mongo is where the connection to Mongo is made and
if there is no connection established, then we throw an error or
retry multiple times to try and reconnect.
*/
// good
/*
Package mongo is the entry point for connecting to MongoDB.
If there is no connection established, then we throw an error or
retry multiple times to try and reconnect.
*/
GoAvoid mentioning specifics, these will quickly become out-of-date and often the available options can be seen from the functions or variables.
// bad
/*
...
The endpoints include:
- /hello
- /world
*/
GoGuidelines
Using Factory Functions
There is no concept of a constructor in Go. Instead, we create factory functions. These should be used for more complex objects to ensure the object is fully hydrated.
Use single letters and abbreviations for the parameters of the factory function and return a pointer for memory efficiency. Use the context of the package name when naming the function.
package user
type User struct {
name string
age int
}
// bad
func NewUser(name string, age int) User {
return User{
name: name,
age: age,
}
}
// good
func New(n string, a int) *User {
return &User{
name: n,
age: a,
}
}
GoUsing Interfaces
Interfaces should be declared in the package they are used, not in the same package as the implementing struct. Only the package that uses the interface has knowledge of the required methods. Remember, you accept interfaces and return structs.
// bad
package producer
type Fooer interface {
Foo()
Bar()
}
type foo struct{}
func (f *foo) Foo() {}
func (f *foo) Bar() {}
func New() Fooer { // Returning the interface.
return &foo{}
}
- - - - - - - - -
package main
import "XXX/producer"
type app struct {
fooer producer.Fooer
}
func (a app) start() {
a.fooer.Foo()
// a.fooer.Bar() is available when it is not required.
}
func main() {
a := app{
fooer: producer.New(),
}
a.start()
}
GoGo interfaces are implemented implicitly which allows you to specify only the required methods, leading to a reduction in interface bloat. By returning a struct instead of an interface, we reduce preemptive interface creation too. Interfaces should only be created when they are needed.
// good
package producer
type foo struct{}
func (f *foo) Foo() {}
func (f *foo) Bar() {}
func New() *foo {
return &foo{}
}
- - - - - - - - -
package main
import "XXX/producer"
type fooer interface {
Foo()
}
type app struct {
fooer fooer
}
func (a app) start() {
a.fooer.Foo()
// We do not have access to Bar()
}
func main() {
a := app{
fooer: producer.New(),
}
a.start()
}
GoCredit: Kieran O’Sullivan, OSHANK K.
Testing
When writing table-driven unit tests, use a map rather than a slice. This simplifies the process of setting the test name. Although it’s slower, performance in Go unit testing is rarely an issue.
// bad
tests := []struct{
name string
}{
{
name: "this is the test name",
},
}
// good
tests := map[string]struct{
}{
"this is the test name": {
},
}
GoDo not import a package to convert a variable to a pointer. Create your own and use _test
as the package name to avoid accidental imports from the main application.
// bad
import "github.com/aws/aws-sdk-go/aws"
tests := map[string]struct{
age *int
}{
"": {
age: aws.Int(123)
},
}
// good
package _test
func IntPtr(i int) *int {
return &i
}
- - - - - - - - -
import "XXX/_test"
tests := map[string]struct{
age *int
}{
"": {
age: _test.IntPtr(123)
},
}
GoOptimisation
Building Strings
Strings are immutable in Go. Therefore, constructing a string requires multiple memory reallocations. Instead, use strings.Builder
.
// bad
func greet(name string) string {
greet := "Hello "
if name != "" {
greet += name
} else {
greet += "World"
}
greet += "!"
return greet
}
// good
func greet(name string) string {
var b strings.Builder
b.WriteString("Hello ")
if name != "" {
b.WriteString(name)
} else {
b.WriteString("World")
}
b.WriteString("!")
return b.String()
}
GoMaps
Set the capacity of a map if you know roughly what the size will be. This reduces the number of times Go has to reallocate memory.
// bad
func hasDuplicates(items []string) bool {
m := make(map[string]bool) // no capacity set
for _, val := range items {
if _, ok := m[val]; ok {
return true
}
m[val] = true
}
return false
}
// good
func hasDuplicates(items []string) bool {
m := make(map[string]bool, len(items)) // capacity set
for _, val := range items {
if _, ok := m[val]; ok {
return true
}
m[val] = true
}
return false
}
GoSlices / Arrays
Set the length of the slice if you know what it will be. This prevents the reallocation of memory.
// bad
func idsFromUsers(arr []user) []string {
var ids []string // no length set
for _, val := range arr {
ids = append(ids, val.ID)
}
return ids
}
// good
func idsFromUsers(arr []user) []string {
ids := make([]string, len(arr)) // length set
for i, val := range arr {
ids[i] = val.ID
}
return ids
}
GoSet the capacity of a slice if you know roughly what the length will be. This reduces the number of times Go has to reallocate memory.
// bad
func validUserIDs(arr []user) []string {
var ids []string // no capacity set
for _, val := range arr {
if val.Age > 18 {
ids = append(ids, val.ID)
}
}
return ids
}
// good
func validUserIDs(arr []user) []string {
ids := make([]string, 0, len(arr)) // capacity set
for _, val := range arr {
if val.Age > 18 {
ids = append(ids, val.ID)
}
}
return ids
}
GoTypes
Use pointer receivers where appropriate to prevent Go from creating a copy of the object.
// bad
func (p person) Greet() {}
// good
func (p *person) Greet()
GoConclusion
As mentioned in the introduction, personal preference does play a role in what is considered best practise. However, hopefully you found at least one of the tips useful to incorporate into your own Go projects.
If you have any questions or comments, please leave them below. Also, don’t forget to subscribe to our newsletter to receive more informative posts like this directly to your inbox.
If you found this article helpful, please share it with your friends and colleagues.
Thank you for reading!
Leave a Reply