Let’s Discuss Golang Best Practices!

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
Go

Clashes 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"
)
Go

Directory 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
Go

Filenames

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
Go

Filenames 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
Go

Variable 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
Go

Variable 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"}
Go

Avoid 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)
Go

Function 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
}
Go

Interface Names

Should be a verb and not use abbreviations.

// bad
type Operations interface {
	// ...
}

type Ops interface {
	// ...
}

// good
type Operator interface {
	// ...
}
Go

Include 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
}
Go

Map 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"]
Go

Health 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
Go

Mock 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
Go

Go 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.
*/
Go

The 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.
*/
Go

Avoid 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
*/
Go

Guidelines

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,
	}
}
Go

Using 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()  
}
Go

Go 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()  
}
Go

Credit: 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": {
	
	},
}
Go

Do 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)
	},
}
Go

Optimisation

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()  
}
Go

Maps

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
}
Go

Slices / 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
}
Go

Set 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
}
Go

Types

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()
Go

Conclusion

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

Your email address will not be published. Required fields are marked *