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.

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 [...] reasonf, _ := 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
panicin your code. Useerrorsinstead 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.Printlnorfmt.Printffor logging. Avoid usinglogfrom 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| └── storageRule 11: Use io.Reader and io.Writer
Try to use
io.Readerandio.Writerinstaed of[]bytewhen possible.
io.Readerandio.Writerare 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.Readerorio.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) errorRule 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
deferto close resources or cleanup.
deferis 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
deferas 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.