Golang Dev Specifications
Source
These guidelines are based on uber-go.
Basic Guidelines
Limit Line Length
Go code lines should be limited to 80 characters. This helps improve readability in both smaller and larger windows.
Group Related Declarations, Avoid Unrelated Ones
Group related declarations together, such as imports, constants, variables, types, and functions. Separate unrelated declarations with an empty line.
Not Recommended | Recommended |
---|---|
|
|
|
|
|
|
| Grouping within a function is also acceptable:
|
|
|
Single Variable Declarations
For single variable assignments, prefer using :=
. However, for slices, it's recommended to use var
declarations.
Not Recommended | Recommended |
---|---|
|
|
|
|
Import Grouping
Group imported packages, separating each group with an empty line, and sort them alphabetically within each group.
Common Grouping Patterns
Two common grouping patterns:
- Standard library vs. third-party libraries.
- Standard library vs. third-party libraries vs. local/private libraries.
Not Recommended | Recommended |
---|---|
|
|
Import Aliases
When the package name you're importing doesn't match the last word in the import path, it's recommended to use an alias. You can also use an alias if the package name is too long. However, in most cases, it's best to avoid aliases unless there's a package name conflict.
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
Package Names
When defining package names, follow these guidelines:
- Use all lowercase letters; avoid uppercase or special characters.
- In most cases, you don't need to rename packages when using named imports.
- Choose simple yet meaningful package names for easy recall and reference.
- Avoid using plurals; for example, use
net/url
instead ofnet/urls
. - Refrain from using generic names like "common," "util," "shared," or "lib."
Function Names
- Use camel case for function names; avoid underscores to separate words (except for some test functions).
- Function names should describe their purpose as clearly as possible; avoid using meaningless names.
Function Ordering
Follow these rules for function ordering:
- Arrange function definitions in the order of their expected invocation.
- Within the same file, place functions after
struct
,const
, andvar
declarations. - For receiver functions, those starting with
new
orNew
should come before others.
Not Recommended | Recommended |
---|---|
|
|
Reduce Nesting
Code should handle errors or special cases and return early rather than nesting code blocks. This approach leads to more straightforward and concise code.
Not Recommended | Recommended |
---|---|
|
|
Minimize Unnecessary else
Blocks
Not Recommended | Recommended |
---|---|
|
|
Top-Level Variable Declarations
Not Recommended | Recommended |
---|---|
|
|
If we want
|
|
Use _
as a Prefix for Unexported Top-Level Constants and Variables
Bad | Good |
---|---|
|
|
Separate Embedded Types in Structs with an Empty Line
Not Recommended | Recommended |
---|---|
|
|
Pros and Cons of Embedded Types
Pros:
- Concise code
- Direct access to methods and fields of embedded types
- Ability to implement interfaces
Cons:
- May expose unexported fields and methods to external packages
- Imports the special zero value of embedded methods
- Exposes all fields and methods of the embedded type, which may not be desired
- Can lead to method call ambiguity if the embedded type has the same method names
- Assigning values to fields of embedded types can be cumbersome, as they mix with other embedded types
Not Recommended | Recommended |
---|---|
|
|
|
|
|
|
nil
Is a Valid Slice
When a slice is nil
, it represents a slice with a length of 0.
- When returning an empty slice, it's better to return
nil
instead of an empty slice.
Not Recommended | Recommended |
---|---|
|
|
- Use
len(s) == 0
to check for emptiness rather thans != nil
.
Not Recommended | Recommended |
---|---|
|
|
- Zero-value slices (declared using
var
) can be used immediately without callingmake()
.
Not Recommended | Recommended |
---|---|
|
|
Reduce Variable Scope
Minimize variable scope unless the variable is used elsewhere.
Bad | Good |
---|---|
|
|
| By declaring the
|
Use Raw Strings Instead of Escaped Strings
When a string contains escape characters, prefer wrapping it in backticks (==
), indicating that it's a raw string and doesn't require escaping.
Not Recommended | Recommended |
---|---|
|
|
Struct Initialization
Initialize Structs Using Field Names
When initializing a struct, it's better to include field names to avoid errors due to changes in the struct's fields.
Bad | Good |
---|---|
|
|
Omit Fields with Zero Values
If the fields being initialized have default zero values, you can omit the field names.
Not Recommended | Recommended |
---|---|
|
|
If Initializing a Struct Variable with All Zero Values, Use var
Not Recommended | Recommended |
---|---|
|
|
Initialize Struct Pointers
When initializing a struct pointer, use the &
symbol instead of new()
.
Not Recommended | Recommended |
---|---|
|
|
Use make()
to Initialize Maps
When initializing a map, if it has initial values, use :=
instead of make()
. If there are no initial values, use make()
, and consider estimating the map's size by setting an initial capacity.
Not Recommended | Recommended |
---|---|
|
|
|
|
If Defining a Format String Outside Printf, Use const
When defining a format string outside of Printf
, it's recommended to use a const
constant. This avoids duplicate format string definitions and helps go vet
perform static analysis.
Bad | Good |
---|---|
|
|
Development Principles
Error Handling
Types of Errors
There are typically two types of errors:
- Static Errors: These are errors created using
errors.New()
. They are often used for predefined errors where the error message remains constant. - Dynamic Errors: These are errors created using
fmt.Errorf()
or custom error types. They are suitable for cases where the error message needs to be dynamic.
Error Matching
When checking error types, avoid using ==
for comparison. Instead, use errors.Is()
or errors.As()
for comparison. Also, create top-level error variables for better handling.
// package foo
var ErrCouldNotOpen = errors.New("could not open")
func Open() error {
return ErrCouldNotOpen
}
// package bar
if err := foo.Open(); err != nil {
if errors.Is(err, foo.ErrCouldNotOpen) {
// handle the error
} else {
panic("unknown error")
}
}
Error Wrapping
We can wrap errors using fmt.Errorf()
or errors.Wrap()
to preserve the original error information while adding additional context.
Warning
Starting from Go 1.13, you can use %w
as a formatting verb with fmt.Errorf()
. This allows proper error type matching using errors.Is()
and errors.As()
. Avoid using %v
as it loses error type information.
s, err := store.New()
if err != nil {
return fmt.Errorf(
"new store: %w", err)
}
Error Naming
For regular error variables, use names starting with Err
, followed by a description of the error using camel case:
var (
// Export these errors so users of this package can match them with errors.Is.
ErrBrokenLink = errors.New("link is broken")
ErrCouldNotOpen = errors.New("could not open")
// This error is not exported to avoid making it part of our public API.
errNotFound = errors.New("not found")
)
For custom error types, consider using Error
as a suffix:
// Similarly, export this error so users of this package can match it with errors.As.
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
// This error is not exported to avoid making it part of our public API.
type resolveError struct {
Path string
}
func (e *resolveError) Error() string {
return fmt.Sprintf("resolve %q", e.Path)
}
Sequential Error Handling
When handling errors, use errors.Is()
and errors.As()
to determine the error type. Handle different error types differently, and if an error cannot be handled, explicitly return it to allow higher-level handling.
Description | Code Example |
---|---|
Not Recommended: Log and return the error This approach may clutter application logs with similar error messages, but it doesn't provide significant benefits. |
|
Recommended: Wrap the error and return it Higher-level callers in the stack will handle this error. Using |
|
Recommended: Log the error and gracefully degrade If the operation is not absolutely critical, we can provide a degraded but uninterrupted experience by recovering from it. |
|
Recommended: Match the error and gracefully degrade If the callee defines a specific error in its contract and the failure is recoverable, match that error case and degrade gracefully. For all other cases, wrap the error and return it. Higher-level callers in the stack will handle other error cases. |
|
Type Assertions
When performing type assertions, always use the ok
return value to avoid causing a panic.
Not Recommended | Recommended |
---|---|
|
|
Minimize the Use of panic
In production code, it's best to avoid using panic
. panic
is a major contributor to cascading failures. If you must use panic
, make sure to handle it using recover()
.
Use Atomic Operations
In concurrent programming, use the atomic operations provided by the atomic
package to ensure thread safety. These operations guarantee that basic types like int32
and int64
can only be accessed by one goroutine at a time.
For other types, consider using channels or sync locks for control.
Not Recommended | Recommended |
---|---|
|
|
Avoid Embedding Types in Public Structures
Avoid embedding types in public structures. When multiple types are embedded, it can lead to a mix of exposed interfaces and variables, making management and configuration difficult. Additionally, conflicts may arise between identical variables and functions. There's no guarantee that future versions won't introduce conflicts.
Not Recommended | Recommended |
---|---|
|
|
Avoid Using Built-in Names
When declaring variables, avoid using built-in names such as len
, cap
, append
, copy
, new
, make
, close
, delete
, complex
, real
, imag
, panic
, recover
, print
, println
, error
, string
, int
, uint
, uintptr
, byte
, rune
, float32
, float64
, bool
, true
, false
, iota
, nil
, true
, false
, iota
, nil
, append
, cap
, close
, complex
, copy
, delete
, imag
, len
, make
, new
, panic
, print
, println
, real
, recover
, string
, uint
, uintptr
, byte
, rune
, float32
, float64
, int
, int8
, int16
, int32
, int64
, uint
, uint8
, uint16
, uint32
, uint64
, uintptr
, bool
, etc.
Avoid Using init
Functions
The init
function is automatically executed when a package is imported. However, since the order of execution of init
functions is not guaranteed, initializing variables within an init
function can lead to unpredictable results. Therefore, it's best to avoid using init
functions.
When to Use init()
- When the assignment process during package import is complex and cannot be done with a single variable assignment
- When using pluggable hook functions (e.g.,
database/sql
) - For optimizing precomputed methods
Preallocate Slice Capacity
If you can know the approximate amount of data in advance, you should configure capacity for slice
in advance to reduce the number of slice
expansions and improve performance.
Not Recommended | Recommended |
---|---|
|
|
|
|
Exiting the Main Program Using Exit
or Fatal
In the main program, if an error occurs, it's preferable to use os.Exit
or log.Fatal
to exit the program rather than using panic
. While panic
can cause the program to crash, os.Exit
or log.Fatal
will allow the program to exit gracefully. Additionally, errors should be propagated to the ultimate caller rather than handling fatal errors in every function.
Info
It's best to call either os.Exit
or log.Fatal*
only in main()
. All other functions should return errors to the main
program.
Reasons:
- Allowing too many functions to call
Fatal
can make it difficult to control the program flow. Fatal
errors may prevent all tests from running.Fatal
errors may preventdefer
from executing.
Bad | Good |
---|---|
|
|
Declaring Tags in Serialized Structs
In serialized structs, it's essential to declare tags (such as json
or xml
) to ensure correct parsing during serialization and deserialization.
Recommended | Not Recommended |
---|---|
|
|
Pay Attention to Goroutine Usage
Warning
When using goroutines, consider the following:
- Limit the number of goroutines to avoid unbounded creation.
- Ensure goroutines have predictable termination times.
- Provide a method for stopping goroutines.
Bad | Good |
---|---|
|
|
This goroutine cannot be stopped. It will keep running until the application exits. | This goroutine can be stopped using |
Waiting for Goroutines to Exit
When a goroutine is executing, use a mechanism to ensure that the main program doesn't exit prematurely. Otherwise, it might terminate the goroutine.
Two common approaches are:
Use
sync.WaitGroup
:
If you need to wait for multiple goroutines, use this approach.var wg sync.WaitGroup for i := 0; i < N; i++ { wg.Add(1) go func() { defer wg.Done() // ... }() } // To wait for all to finish: wg.Wait()
Add another
chan struct{}
that the goroutine closes when it's done.
If you have only one goroutine, use this approach.done := make(chan struct{}) go func() { defer close(done) // ... }() // To wait for the goroutine to finish: <-done
Avoid Using Goroutines in init()
Using goroutines in init()
functions can complicate program initialization. Since init()
functions execute when the program starts, and goroutines run asynchronously, the initialization order may become unpredictable.
Performance Optimization
Prefer strconv
Over fmt
When converting strings, prefer using the strconv
package over fmt
. The fmt
package is heavier, while strconv
is lighter. strconv
provides faster conversion and requires fewer resources.
Specify Map and Slice Capacities
If you know the approximate capacity in advance, preallocate it to avoid unnecessary memory allocation and automatic resizing.