Effective Code Style in Go
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:
- standard library
- third-party dependencies
- 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.