Photo by https://go.dev/

Golang Idioms and Best Practices: Part 1

Guidelines and conventions in Go (Golang) concerning naming conventions, code organization, steering clear of tools like init and panic, and incorporating documentation comments.

Tuesday, Dec 12, 2023

avatargolang

This is the first segment of a series of blog posts in which I plan to explore Golang idioms and best practices that I've encountered. This will include insights into practices that, looking back, I wish I had embraced earlier in my journey with Golang.

Quick Background

Prior to 2022, my development background predominantly revolved around Javascript, Typescript, NodeJS, and Salesforce Apex (During my Salesforce development days). However, upon acquiring proficiency in other programming languages like Go, my inclination has shifted away from using NodeJs and Typescript as the go-to choices for the backends I build.

Golang

Golang has quickly become one of my preferred programming languages to work with.

Go, as a statically typed and compiled language, is crafted for optimal performance and is often positioned as a contemporary alternative to the C programming language. Additionally, Go boasts features like a garbage collector, robust support for concurrency, a comprehensive standard library, and a straightforward syntax that contributes to its remarkable readability and ease of adoption.

Notably, the language has influenced Ryan Dahl, the creator of Node.js, prompting him to incorporate many Go-inspired ecosystem features into JavaScript through Deno.

Naming

Naming in respect to variables, functions, and interfaces are important to practice and adopt when writing Go. I personally find employing naming conventions early on in the learning process beneficial. I tend to spend too much effort and time on naming functions or variables than the actual problem at hand.

Variable Naming Conventions

  • Use camelCase for variable names, capitalize for exported variables. (e.g parseOptions or ParseOptions)
  • Single letter names for indexes: i, j, k, ...
  • For acronyms in names, use all capitals. (e.g. MVC, CRUD, ACID)
  • For collections, slices, arrays use repeated letters to represent, single letters in loop. (e.g. with a range loop for t := range tt {)
  • Avoid repeating package name in variable name.

Function Naming Conventions

Function names can naturally become too wordy and long.

You can combat this by omitting the following from function names:

  • The type of a method's receiver. (e.g. func (s *service) GetAccount() *account {...})
  • Whether an input or output is a pointer.
  • Inputs and outputs types when there is no naming collision. (e.g SetConfig)
  • Avoid Get or Set naming. (e.g. GetName)

Examples

Repeating the name of the package

// Bad:
package yamlconfig
 
func ParseYAMLConfig(input string) (*Config, error)
 
// Good:
package yamlconfig
 
func Parse(input string) (*Config, error)

Method recievers, omit the name of the struct the reciever is for.

// Bad:
func (c *Config) WriteConfigTo(w io.Writer) (int64, error)
// Good:
func (c *Config) WriteTo(w io.Writer) (int64, error)

Avoid repeating the names of variables passed as parameters and types of return values

// Bad:
func TansformJSONToConfig(json string) *Config
// Good:
func Transform(json string) *Config

Interface Naming Convention

Name interfaces with a verb.

Here are some naming examples: [Stringer(), Storer(), Reader(), Writer()]

Examples from the io standard library package:

Real World Example

Imagine building a CLI package library and requiring an interface type as an argument. This value must satisfy the method signature of a function named Run. This function must accept a slice of strings representing command-line arguments and return an integer.

Best practice, you'd name this interface ending with er, giving the verb like intention.

Runn(er) interface

type Runner Interface {
  Run(args []string) int
}

Grouping

Grouping is another idiomatic practice that contributes to more maintainable projects.

Variable Grouping

Group similar variable declarations of constants, variables, and type constants together.

var (
  a = 1
  b = 2
)
const (
  blue = "blue"
  red = "red"
)

Function Grouping

  • Place exported functions closer to top of the file, while un-exported or private functions towards the bottom.
  • Place similar functions together, especially those that serve similar purposes but vary by varying parameters and return types.

Mutext Grouping / Struct field Grouping

  • When declaring mutex struct field, group next to the value the mutex is protecting race conditions with
  • Name mutex field with the same name as the protected field with an appended, Mu.
type Cmd struct {
 
  resultMu sync.Mutex
  result map[string]interface{}
}

This can be applied to other struct fields that have a tight-coupling behavior.

Avoiding init

What is init()?

Go's init() is a package scope block that is invoked only once regardless of how many times the package is imported. It's primary purpose is to set up any required state before the package is used.

This can be useful for a lot of use cases however there are some drawbacks to it's use.

Use sync.Once

The init() function makes it difficult to test and you, the developer, have no control over when this function is called.

Sometimes this special function is used to initialize a shared variable once to implement a singleton, making it concurrent safe.

This same pattern can be accomplished with the sync standard library package's sync.Once method.

Example

Imagine you have the following package, named store.

package store
 
type storage struct {
	data map[string]interface{}
}
 
func (s *storage) Add(key string, val interface{}) {
	s.data[key] = val
}
 
func (s *storage) Remove(key string) {
	delete(s.data, key)
}
 
func NewStore() *storage {
	return &storage{
    data: make(map[string]interface{})
  }
}

This approach may function well initially, but when introducing concurrency to your application, you might encounter unexpected side effects. In such cases, resorting to the use of init() becomes a viable solution.

package store
 
var sharedStore *storage
 
type storage struct {
	data map[string]interface{}
}
 
func (s *storage) Add(key string, val interface{}) {
	s.data[key] = val
}
 
func (s *storage) Remove(key string) {
	delete(s.data, key)
}
 
// Bad
func init() {
	sharedStore = &storage{
		data: make(map[string]interface{}),
	}
}
 
func NewStore() *storage {
	return sharedStore
}

While you are ensuring a single instance of the storage struct is allocated, you lose the ability to dictate when the init function is executed.

Utilize sync.Once to implement the Singleton pattern in your package, maintaining control over the moment when the sharedStorage variable is allocated in memory.

package store
 
import "sync"
 
var (
	sharedStore *storage
	once        sync.Once
)
 
type storage struct {
	data map[string]interface{}
}
 
func (s *storage) Add(key string, val interface{}) {
	s.data[key] = val
}
 
func (s *storage) Remove(key string) {
	delete(s.data, key)
}
 
func NewStore() *storage {
  // GOOD
	once.Do(func() {
		sharedStore = &storage{
			data: make(map[string]interface{}),
		}
	})
	return sharedStore
}
 

If you must panic...

Instead of invoking panic() within the context of a function, opt to either return an error as the final return value or as the sole value when no computed result is returned, indicating an error.

func marshalData(val interface{}) ([]byte, error) {
  data, err := json.UnMarshal()
  if err != nil {
    return nil, err
  }
  // ...
}

If you must panic, prefix your function name with must

func mustMarshalData(val interface{}) ([]byte) {
  data, err := json.UnMarshal()
  if err != nil {
    panic(err)
  }
  // ...
}

Or MustMarshalData if the function is an exported one.

Go "Doc Comments"

If you come from a Javascript/Typescript background, you probably have come across JsDoc.

In Go, the "Doc Comments" idiom involves using lined-out comments to explain and document symbols such as packages, constants, functions, types, variable declarations, and commands. These comments are positioned directly above the respective subject they describe, enclosed by double backslash characters, //.

When publishing internal or public packages, it's crucial to document your Go source code in adherence to the Doc Comment specifications. Particularly noteworthy is the importance of providing a doc comment for every exported (capitalized) named symbol.

An example of Doc Comments are as follows:

// Package storer is a library that persists data
//
// The storer only supports x, y, z....
// ...
package storer

Doc Comments Support

The Go ecosystem incorporates a range of tools that facilitate the use of "Doc Comments". Within the standard library, Go offers support through packages such as go/doc and go/comment. These packages enable the extraction of documentation from Go source code. Additionally, external tools like cmd/pkgsite leverage this functionality to construct documentation websites, empowering sites like https://pkg.go.dev/.

Recap

We've touched on some of the idiomatic best practices when writing Go, in regards to:

  • Naming Conventions
  • Grouping Conventions
  • Avoiding init() function when possible
  • Don't panic(), but if you must, prefix with must
  • Using "Doc Comments"

Now move along and code on!