Skip to content

Go

Introduction

Go is a statically typed, compiled language designed at Google. It is known for its simplicity and performance. Go is an amazing language for building web services, APIs and highly concurrent apps.

go

Why Go?

  • Very performant
  • Easy to learn
  • Very rich standard library
  • Built-in concurrency
  • Cross-platform and compiles to a single binary

Best practices

This section will cover some of the best practices and rules to follow when writing Go code. A useful resource to read is the official Effective Go guide by Google.

The following are the rules we follow at Quinck when writing Go code, to ensure consistency, safety and readability. Some of them are enforced by the linters (if using Blaze) while others are just good practices to follow.

Rule 1: Always check errors

Always check errors when calling functions that return an error.

f, err := os.Open("filename.ext")
if err != nil {
// handle the error
}

If you specifically want to ignore an error, you can use the _ operator. If doing so, make sure to add a comment explaining why you are ignoring the error.

// We don't care about the error here for [...] reason
f, _ := os.Open("filename.ext")

Rule 2: Return values


If a function returns an error, it must be its last returned value.

❌ Bad

func foo() (error, int) {
return nil, 0
}

✅ Good

func foo() (int, error) {
return 0, nil
}

Rule 3: Avoid pointers when not needed

Avoid using pointers when not needed. Use pointers when you need to mutate shared state or resources.

❌ Bad

func GetUser(id string) (*User, error) {
// ...
}

In this case, the pointer is not needed, and is potentially dangerous. Always prefer to return the value directly:

✅ Good

func GetUser(id string) (User, error) {
// ...
}

✅ Good

type Db struct {
*sql.DB // Pointer is needed here
}
func (d *Db) GetUser(id int) (User, error) {
// ...
}

In the above case, the pointer is needed, as we have to access the shared state of the database connection.


Rule 4: Avoid using iota for enums

Use constants for enums and do not use iota. This makes the code more readable and maintainable.

❌ Bad

type Status int
const (
StatusUnknown Status = iota // 0
StatusPending // 1
StatusApproved // 2
StatusRejected // ...
)

The above code is not very readable and can be confusing. iota automatically assigns sequential values to the constants, which can be hard to understand especially with large enums.

Instaed, use explicit values for each constant:

✅ Good

type Status int
const (
StatusUnknown Status = 0
StatusPending Status = 1
StatusApproved Status = 2
StatusRejected Status = 3
)

Rule 5: Don’t panic

NEVER use panic in your code. Use errors instead with error as values pattern.


Rule 6: Avoid using context.Background

Avoid using context.Background. Always pass a context from the caller.

  • If you are not sure which context to use, use context.TODO.
  • If you need to use context.Background, always add a deadline or timeout to it.
  • When writing http handlers, always use the request context.
  • When writing shared libraries, always accept context as the first argument.

❌ Bad

func handler(w http.ResponseWriter, r *http.Request) {
ctx := context.Background() // ❌ Bad
// ...
}

✅ Good

func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // ✅ always use the request context
// ...
}

❌ Bad

func (db *Db) GetUser(id string) (User, error) {
user := db.Get(id, context.Background())
}

For shared methods or libraries, always pass the context as first argument:

✅ Good

func (db *Db) GetUser(ctx context.Context, id string) (User, error) {
user := db.Get(id, ctx)
}

Rule 7: Use the logger

On larger apps, always use a logger. Do not use fmt.Println or fmt.Printf for logging. Avoid using log from the standard library if you already have a structured logger (like slog, zerolog or zap).

If using Blaze, you can use the global zerolog package initialized httpcore package.


Rule 8: Enum zero value

Prefer to define the zero value for your enums as an unkown value. Given Go approach to zero values, if the client does not pass any value for the enum we can assume it is unknown.

❌ Bad

type Status int
const (
StatusPending Status = 1
StatusApproved Status = 2
StatusRejected Status = 3
)

✅ Good

type Status int
const (
StatusUnknown Status = 0
StatusPending Status = 1
StatusApproved Status = 2
StatusRejected Status = 3
)

Rule 9: Avoid nil-nil

Avoid, if possible, to return a pointer as nil when the error is also nil. It is better to return the zero value of the type than nil. If the returned value can be optional, return a specific error.

❌ Bad

func GetUser(id string) (*User, error) {
// ...
return nil, nil // user not found
}

In the above case, it is better to return a custom error instead of nil-nil. It makes the code more readable and explicit, and the caller can handle the error properly.

✅ Good

var ErrUserNotFound = errors.New("user not found")
func GetUser(id string) (*User, error) {
// ...
return nil, ErrUserNotFound
}

Rule 10: Keep the structure simple

Keep the structure of your code simple and easy to understand. Avoid deep folder nesting and complex structures. Go modules system makes doing deep nesting very impractical.

  • Keep the file names as short as possible.
  • Avoid deep nesting of folders.
  • Avoid using camelCase for file names. If you absolutely need two words, snake_case instead.
  • Avoid complex or multi word package names.
  • Keep the package names short but meaningful.

❌ Bad

├── cmd
│ └── myapp
│ └── main.go
│ └── internal
│ └── services
│ └── user
│ └── user_service.go
│ └── books
│ └── book_service.go

✅ Good

├── cmd
│ └── main.go
├── internal
│ └── services
│ └── user.go
│ └── book.go
|── pkg
| └── utils
| └── storage

Rule 11: Use io.Reader and io.Writer

Try to use io.Reader and io.Writer instaed of []byte when possible.

  • io.Reader and io.Writer are more flexible and can be used with any type of data.
  • They are more memory efficient and can be used with large data.
  • They handle automatic streaming and chunked reads and writes.
  • They are more idiomatic and can be used with other Go packages that expect io.Reader or io.Writer.

⚠️ OK but not optimal

func WriteToDisk(data []byte) error {
// ...
}

The above code is not optimal but can still work and is still idiomatic. However, especially when dealing with large data, it is better to use io.Reader:

✅ Good

func WriteToDisk(r io.Reader) error {
// ...
}

Rule 12: Method arguments order

Try to align the order of the arguments in your methods and interfaces.

  • Always pass the context as the first argument.

⚠️ OK but not optimal

func GetUser(ctx context.Context, id string) (User, error)
func GetUserFiltered(ctx context.Context, filter Filter, id string) error

✅ Good

func GetUser(ctx context.Context, id string) (User, error)
func GetUserFiltered(ctx context.Context, id string, filter Filter) error

Rule 13: Use the standard library

Always use the standard library when possible.

  • The Go standard library is very rich and has a lot of useful packages.
  • Always check if the functionality you need is already available in the standard library before using a third-party package.
  • Using the standard library makes your code more idiomatic and easier to maintain.
  • The std is very well tested and optimized, and is the best choice for most use cases.
  • Your code automatically becomes fully compatible with a lot of other Go packages that use the standard library.

Go comes with built in concurrency, network, tcp, udp, rpc, http, json, xml, html, templates, testing, logging, sql, images, crypto, os, math, time… and a lot of other packages to help you build your application.


Rule 14: Accept interfaces

It is a good practice (but not always necessary) to accept interfaces instead of concrete types.

  • Accepting interfaces makes your code more flexible and easier to test.
  • It allows you to easily swap implementations without changing the code.
  • It makes your code more idiomatic and easier to understand.
  • It allows to easily integrate with Go standard library and other packages that use interfaces.

To know more about this pattern, read this article

When possible, avoid using Java-like big interfaces. Instaed, write a concrete struct and let the consumer decide what to use by accepting a smaller interface.


Rule 15: Use defer

Use defer to close resources or cleanup.

  • defer is a very useful feature in Go that allows you to defer the execution of a function until the surrounding function returns.
  • It is very useful for closing resources, cleaning up, or releasing locks.
  • Always call defer as soon as possible in the function.

❌ Bad

func handler(w http.ResponseWriter, r *http.Request) {
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
// Do some stuff
f.Close() //❌ we can forget to close the file
}

The above code is not optimal, as we can forget to close the file if we add more code in the function. Instead, use defer right after opening the file:

✅ Good

func handler(w http.ResponseWriter, r *http.Request) {
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
defer f.Close() //✅ file will always be closed when the function returns
// Do some stuff
}

Rule 16: Communicate using channels

Do not communicate by sharing memory; instead, share memory by communicating.

This is a famous quote from the Go language creators.

Go has built-in concurrency primitives that make it easy to write concurrent code. Always prefer to use channels and goroutines instaed of locks to communicate between different parts of your application.

To read more about this, check out this official article.

It is absolutely fine to use locks when you need to protect shared state, as sometimes it is the only way to do so. However, always prefer to use channels when possible.