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/urlinstead 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, andvardeclarations.
- For receiver functions, those starting with neworNewshould 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 nilinstead of an empty slice.
| Not Recommended | Recommended | 
|---|---|
|  |  | 
- Use len(s) == 0to 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 Fatalcan make it difficult to control the program flow.
- Fatalerrors may prevent all tests from running.
- Fatalerrors may prevent- deferfrom 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.