Effective Code Style in Go

Effective Code Style in Go

September 25, 2023

Code style rules is important to enhance code readability, maintainability.

Basic formatting, such as line breaks and indents, is determined by the standard utility gofmt. This article describes various elements that enhance code readability.

Importing Dependencies

In imports, everything is divided into 3 blocks:

  1. standard library
  2. third-party dependencies
  3. project components

Blocks are separated by an empty line. Example:

import (
	"fmt"
	"net/http"
	"os"

	"github.com/jmoiron/sqlx"
	_ "github.com/mattn/go-sqlite3"

	"example/internal/app"
)

When you split denedencies to blocks, goftm will keep this structure and new dependencies will be added to the correct block

Numbers in Code

In the Go language, numbers can be written in different formats, such as decimal, hexadecimal, and others.

The decimal system:

  • Size definition
  • Array index
  • Arithmetic operations

The hexadecimal system in capital letters:

  • Binary operations
  • Data arrays

In most cases use int, even if value is unsigned or is small.

For constants should be used untyped numbers.

Strings

Check for empty string by comparation with emtpy string:

if value == "" {
	//
}

Structures

When initializing a new structure with predefined values, fields should be written one per line.

Stack allocation

Allocate a new structure on the stack only for use within the function where it’s allocated:

var b bytes.Buffer

or

request := http.Request{
	Method: http.MethodGet,
	URL:    u,
}

Event if a structure is defined on the stack, Go automatically determines where to allocate it. If the structure is too large it will be allocated on the heap.

Structures created on the stack should be used by reference:

response, err := client.Do(&request)

Heap allocation

Allocate new structure on the heap if you intend to return it from a function or store it in an array, map, or elsewhere.

rect := new(image.Rectangle)

or

point := &image.Point{
	X: 640,
	Y: 480,
}

Function Names

Function names depend on the context:

  • Prefix Has - indicates the presence of something in an object (noun). For example: HasData
  • Prefix Is - checks a property (adjective or verb). For example: IsVisible

Errors

Static Global Errors

Errors can be defined at the beginning of a module. These errors can be accessed by any function within the package. The error name should start with Err, for example, ErrOutOfRange.

var (
   ErrNotComplete = errors.New("process: not complete")
)

Error Context

Original error can be wrapped with the fmt.Errorf() function to provide additional context, like module name or execution step. The error context should be short and start with a lowercase letter:

return fmt.Errorf("example context: %w", err)

The %w formatter indicates that original error should be wrapped. Avoid context duplication. For example, let’s create a function to apply database migrations:

func Migrate() error {
	if err := MigrateUserTable(); err != nil {
		return fmt.Errorf("user: %w", err)
	}

	if err := MigrateArticleTable(); err != nil {
		return fmt.Errorf("article: %w", err)
	}

	return nil
}

func Start() error {
	if err := Migrate(); err != nil {
		return fmt.Errorf("migrate: %w", err)
	}

	return nil
}

Each exception provides its own context, and you can easily find what went wrong.