| name | golang |
| description | Best practices for writing production Go code. Use when writing, reviewing, or refactoring Go code. Covers error handling, concurrency, naming conventions, testing patterns, performance optimization, generics, and common pitfalls. Based on Google Go Style Guide, Uber Go Style Guide, Effective Go, and Go Code Review Comments. Updated for Go 1.25. |
Go Best Practices
Battle-tested patterns from Google, Uber, and the Go team. These are practices proven in large-scale production systems, updated for modern Go (1.25).
Core Principles
Readable code prioritizes these attributes in order:
- Clarity: purpose and rationale are obvious to the reader
- Simplicity: accomplishes the goal in the simplest way
- Concision: high signal to noise ratio
- Maintainability: easy to modify correctly
- Consistency: matches surrounding codebase
Error Handling
Return Errors, Do Not Panic
Production code must avoid panics. Return errors and let callers decide how to handle them.
func run(args []string) {
if len(args) == 0 {
panic("an argument is required")
}
}
func run(args []string) error {
if len(args) == 0 {
return errors.New("an argument is required")
}
return nil
}
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Error Wrapping
Use %w when callers need to inspect the underlying error with errors.Is or errors.As. Use %v when you want to hide implementation details or at system boundaries.
if err != nil {
return fmt.Errorf("load config: %w", err)
}
if err != nil {
return fmt.Errorf("database unavailable: %v", err)
}
Keep context succinct. Avoid phrases like "failed to" that pile up as errors propagate.
return fmt.Errorf("failed to create new store: %w", err)
return fmt.Errorf("new store: %w", err)
Joining Multiple Errors (Go 1.20+)
Use errors.Join when multiple operations can fail independently.
func validateUser(u User) error {
var errs []error
if u.Name == "" {
errs = append(errs, errors.New("name required"))
}
if u.Email == "" {
errs = append(errs, errors.New("email required"))
}
return errors.Join(errs...)
}
if err := validateUser(u); err != nil {
if errors.Is(err, ErrNameRequired) {
}
}
Error Types
Choose based on caller needs:
| Caller needs to match? | Message type | Approach |
|---|
| No | Static | errors.New("something bad") |
| No | Dynamic | fmt.Errorf("file %q not found", file) |
| Yes | Static | Exported var ErrNotFound = errors.New("not found") |
| Yes | Dynamic | Custom error type with Error() method |
Sentinel Errors and errors.Is
Define sentinel errors for conditions callers need to check.
var (
ErrNotFound = errors.New("not found")
ErrInvalidUser = errors.New("invalid user")
)
if errors.Is(err, ErrNotFound) {
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("failed path:", pathErr.Path)
}
Error Naming
Exported error variables use Err prefix. Custom error types use Error suffix.
var (
ErrNotFound = errors.New("not found")
ErrInvalidUser = errors.New("invalid user")
)
type NotFoundError struct {
Resource string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s not found", e.Resource)
}
Handle Errors Once
Do not log an error and also return it. The caller will likely log it again.
if err != nil {
log.Printf("could not get user %q: %v", id, err)
return err
}
if err != nil {
return fmt.Errorf("get user %q: %w", id, err)
}
if err := emitMetrics(); err != nil {
log.Printf("could not emit metrics: %v", err)
}
Error Strings
Do not capitalize error strings or end with punctuation. They often appear mid-sentence in logs.
fmt.Errorf("Something bad happened.")
fmt.Errorf("something bad happened")
Indent Error Flow
Keep the happy path at minimal indentation. Handle errors first.
if err != nil {
} else {
}
if err != nil {
return err
}
Concurrency
Channel Size
Channels should have size zero (unbuffered) or one. Any other size requires justification about what prevents filling under load.
c := make(chan int, 64)
c := make(chan int)
c := make(chan int, 1)
Goroutine Lifetimes
Document when and how goroutines exit. Goroutines blocked on channels will not be garbage collected even if the channel is unreachable.
func (w *Worker) Run(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case job := <-w.jobs:
w.process(job)
}
}
}
Use errgroup for Concurrent Operations
Prefer errgroup.Group over manual sync.WaitGroup for error-returning goroutines.
import "golang.org/x/sync/errgroup"
func processItems(ctx context.Context, items []Item) error {
g, ctx := errgroup.WithContext(ctx)
for _, item := range items {
g.Go(func() error {
return process(ctx, item)
})
}
return g.Wait()
}
func processItemsLimited(ctx context.Context, items []Item) error {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10)
for _, item := range items {
g.Go(func() error {
return process(ctx, item)
})
}
return g.Wait()
}
Prefer Synchronous Functions
Synchronous functions are easier to reason about and test. Let callers add concurrency when needed.
func Fetch(url string) <-chan Result
func Fetch(url string) (Result, error)
Zero Value Mutexes
The zero value of sync.Mutex is valid. Do not use pointers to mutexes or embed them in exported structs.
mu := new(sync.Mutex)
type SMap struct {
sync.Mutex
data map[string]string
}
type SMap struct {
mu sync.Mutex
data map[string]string
}
Atomic Operations (Go 1.19+)
Use the standard library's typed atomics. External packages are no longer necessary.
import "sync/atomic"
type Counter struct {
value atomic.Int64
}
func (c *Counter) Inc() {
c.value.Add(1)
}
func (c *Counter) Value() int64 {
return c.value.Load()
}
sync.Map Performance (Go 1.24+)
The sync.Map implementation was significantly improved in Go 1.24. Modifications of disjoint sets of keys are much less likely to contend on larger maps, and there is no longer any ramp-up time required to achieve low-contention loads.
Naming
MixedCaps Always
Go uses MixedCaps, never underscores. This applies even when it breaks other language conventions.
MAX_LENGTH, max_length, HTTP_Server
MaxLength, maxLength, HTTPServer
Initialisms
Initialisms maintain consistent case: URL not Url, ID not Id, HTTP not Http.
xmlHttpRequest, serverId, apiUrl
xmlHTTPRequest, serverID, apiURL
Short Variable Names
Variables should be short, especially with limited scope. The further from declaration a name is used, the more descriptive it needs to be.
for i, v := range items { }
r := bufio.NewReader(f)
var DefaultTimeout = 30 * time.Second
Receiver Names
Use one or two letter abbreviations of the type. Be consistent across methods. Do not use generic names like this, self, or me.
func (this *Client) Get() {}
func (c *Client) Get() {}
func (cl *Client) Post() {}
func (c *Client) Get() {}
func (c *Client) Post() {}
Pointer vs Value Receivers
| Use pointer receiver when | Use value receiver when |
|---|
| Method modifies the receiver | Struct is small and immutable |
| Struct is large (avoid copying) | Method doesn't modify state |
| Consistency with other methods | Receiver is a map, func, or chan |
| Struct contains sync.Mutex | Basic types (int, string, etc.) |
func (s *Server) Shutdown() error {
s.running = false
return s.listener.Close()
}
func (p Point) Distance(q Point) float64 {
return math.Hypot(p.X-q.X, p.Y-q.Y)
}
Package Names
Package names are lowercase, single words. Avoid util, common, misc, api, types. The package name becomes part of the identifier at call sites.
package chubby
type ChubbyFile struct{}
package chubby
type File struct{}
Avoid Repetition in Names
Do not repeat package or receiver names in function names.
package http
func HTTPServe() {}
func (c *Config) WriteConfigTo(w io.Writer) {}
package http
func Serve() {}
func (c *Config) WriteTo(w io.Writer) {}
Imports
Grouping
Organize imports in three groups separated by blank lines: standard library, external packages, internal packages.
import (
"context"
"fmt"
"os"
"github.com/google/uuid"
"golang.org/x/sync/errgroup"
"yourcompany/internal/config"
"yourcompany/internal/metrics"
)
Avoid Renaming
Rename imports only to avoid collisions. Prefer renaming the most local import.
Avoid Import Dot
The dot import (import . "pkg") makes code harder to read. Use only in test files with circular dependencies.
Blank Imports
Import for side effects (import _ "pkg") only in main packages or tests.
Module Management
Tool Directives (Go 1.24+)
Go modules can now track executable dependencies using tool directives in go.mod. This removes the need for the previous workaround of adding tools as blank imports to a file conventionally named "tools.go".
module example.com/myproject
go 1.24
tool (
golang.org/x/tools/cmd/stringer
github.com/golangci/golangci-lint/cmd/golangci-lint
)
go get -tool golang.org/x/tools/cmd/stringer
go tool stringer -type=Status
go get tool
go install tool
Structs
Use Field Names in Initialization
Always use field names. Positional arguments break when fields are added.
k := User{"John", "john@example.com", true}
k := User{
Name: "John",
Email: "john@example.com",
Active: true,
}
Omit Zero Value Fields
Do not initialize fields to their zero values.
user := User{
Name: "John",
Active: false,
Count: 0,
}
user := User{
Name: "John",
}
Embedding
Do not embed types in public structs. Embedding exposes methods and fields to the public API unintentionally.
type SMap struct {
sync.Mutex
data map[string]string
}
type SMap struct {
mu sync.Mutex
data map[string]string
}
Use var for Zero Value Structs
var user User
user := User{}
Slices and Maps
Nil Slice Declaration
Prefer nil slices over empty slices. They are functionally equivalent but nil is the preferred style.
var t []string
t := []string{}
Copy at Boundaries
Slices and maps hold references. Copy them when storing or returning to prevent mutation.
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
}
func (d *Driver) SetTrips(trips []Trip) {
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
}
func (d *Driver) SetMetadata(m map[string]string) {
d.metadata = maps.Clone(m)
}
Specify Capacity
Preallocate when size is known. This reduces allocations.
var result []Item
for _, v := range input {
result = append(result, transform(v))
}
result := make([]Item, 0, len(input))
for _, v := range input {
result = append(result, transform(v))
}
Use slices and maps Packages
Prefer standard library functions for common operations.
import (
"cmp"
"maps"
"slices"
)
slices.Sort(numbers)
slices.SortFunc(users, func(a, b User) int {
return cmp.Compare(a.Name, b.Name)
})
idx, found := slices.BinarySearch(sorted, target)
copy := slices.Clone(original)
mapCopy := maps.Clone(original)
if slices.Equal(a, b) { }
if maps.Equal(m1, m2) { }
Generics (Go 1.18+)
When to Use Generics
Use generics when you find yourself writing the same code for different types.
func Filter[T any](slice []T, predicate func(T) bool) []T {
result := make([]T, 0, len(slice))
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
func Map[T, U any](slice []T, transform func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = transform(v)
}
return result
}
adults := Filter(users, func(u User) bool { return u.Age >= 18 })
names := Map(users, func(u User) string { return u.Name })
Type Constraints
Use constraints for type safety.
import "cmp"
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](values []T) T {
var total T
for _, v := range values {
total += v
}
return total
}
Generic Type Aliases (Go 1.24+)
Type aliases can now be parameterized like defined types.
type Set[T comparable] = map[T]struct{}
var s Set[string]
s = make(Set[string])
s["hello"] = struct{}{}
type OrderedSlice[T cmp.Ordered] = []T
Avoid Over-Generalization
Do not use generics when a concrete type or interface suffices.
func PrintAll[T fmt.Stringer](items []T) {
for _, item := range items {
fmt.Println(item.String())
}
}
func PrintAll(items []fmt.Stringer) {
for _, item := range items {
fmt.Println(item.String())
}
}
Iterators (Go 1.23+)
Range Over Functions
Go 1.23 introduced range-over-func, allowing custom iterators.
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)
func Backward[T any](s []T) func(yield func(int, T) bool) {
return func(yield func(int, T) bool) {
for i := len(s) - 1; i >= 0; i-- {
if !yield(i, s[i]) {
return
}
}
}
}
for i, v := range Backward(items) {
fmt.Println(i, v)
}
String and Bytes Iterators (Go 1.24+)
New iterator functions for efficient string and byte processing.
import "strings"
text := "line1\nline2\nline3"
for line := range strings.Lines(text) {
fmt.Print(line)
}
csvData := "apple,banana,cherry"
for value := range strings.SplitSeq(csvData, ",") {
fmt.Println(value)
}
for part := range strings.SplitAfterSeq(csvData, ",") {
fmt.Println(part)
}
Structured Logging (Go 1.21+)
Use slog for New Code
The standard library now includes structured logging.
import "log/slog"
slog.Info("user created", "id", userID, "email", email)
slog.Error("request failed", "err", err, "method", r.Method)
logger := slog.With("service", "auth", "version", "1.0")
logger.Info("starting")
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
slog.SetDefault(slog.New(handler))
slog.DiscardHandler (Go 1.24+)
Use the built-in discard handler for suppressing logs in tests.
log := slog.New(slog.NewJSONHandler(io.Discard, nil))
log := slog.New(slog.DiscardHandler)
Structured Logging Best Practices
slog.Info("request completed",
"method", r.Method,
"path", r.URL.Path,
"status", statusCode,
"duration_ms", duration.Milliseconds(),
)
slog.Info("user action",
slog.Group("user",
"id", user.ID,
"role", user.Role,
),
slog.Group("request",
"method", r.Method,
"path", r.URL.Path,
),
)
Performance
Prefer strconv Over fmt
strconv is faster for primitive conversions.
s := fmt.Sprintf("%d", n)
s := strconv.Itoa(n)
Avoid Repeated String to Byte Conversions
for i := 0; i < n; i++ {
w.Write([]byte("hello"))
}
data := []byte("hello")
for i := 0; i < n; i++ {
w.Write(data)
}
Specify Map Capacity
m := make(map[string]int)
m := make(map[string]int, len(items))
Use strings.Builder for Concatenation
var s string
for _, part := range parts {
s += part
}
var b strings.Builder
b.Grow(totalLen)
for _, part := range parts {
b.WriteString(part)
}
s := b.String()
Testing
Table Driven Tests with Parallel Execution
Use table driven tests to avoid code duplication. Run subtests in parallel when safe.
func TestSplit(t *testing.T) {
tests := []struct {
name string
input string
sep string
want []string
}{
{
name: "simple",
input: "a/b/c",
sep: "/",
want: []string{"a", "b", "c"},
},
{
name: "empty",
input: "",
sep: "/",
want: []string{""},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := strings.Split(tt.input, tt.sep)
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("Split() mismatch (-want +got):\n%s", diff)
}
})
}
}
T.Context and T.Chdir (Go 1.24+)
New helper methods for test context and working directory.
func TestWithContext(t *testing.T) {
ctx := t.Context()
result, err := doWork(ctx)
if err != nil {
t.Fatal(err)
}
}
func TestWithChdir(t *testing.T) {
t.Chdir("testdata")
data, err := os.ReadFile("input.txt")
}
Benchmark with b.Loop (Go 1.24+)
Use b.Loop() for cleaner, more accurate benchmarks.
func BenchmarkOld(b *testing.B) {
input := setupInput()
b.ResetTimer()
for i := 0; i < b.N; i++ {
process(input)
}
}
func BenchmarkNew(b *testing.B) {
input := setupInput()
for b.Loop() {
process(input)
}
}
Benefits of b.Loop():
- Setup code runs exactly once per
-count, automatically excluded from timing
- No need to call
b.ResetTimer()
- Function call parameters and results are kept alive, preventing compiler optimization
Testing Concurrent Code with synctest (Go 1.25+)
The testing/synctest package provides deterministic testing for concurrent code using synthetic time.
import "testing/synctest"
func TestTimeout(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
time.Sleep(4 * time.Second)
if err := ctx.Err(); err != nil {
t.Fatalf("unexpected timeout: %v", err)
}
time.Sleep(2 * time.Second)
if ctx.Err() != context.DeadlineExceeded {
t.Fatal("expected deadline exceeded")
}
})
}
Key concepts:
synctest.Test creates an isolated "bubble" with synthetic time
- Time only advances when all goroutines in the bubble are blocked
- Initial time is midnight UTC 2000-01-01
synctest.Wait() waits for all goroutines to be durably blocked
func TestConcurrentCounter(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
var counter atomic.Int64
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Add(1)
}()
}
wg.Wait()
if got := counter.Load(); got != 10 {
t.Errorf("got %d, want 10", got)
}
})
}
func TestPeriodicTask(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
var count atomic.Int64
ctx, cancel := context.WithCancel(t.Context())
go func() {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
count.Add(1)
}
}
}()
time.Sleep(350 * time.Millisecond)
synctest.Wait()
cancel()
synctest.Wait()
if got := count.Load(); got != 3 {
t.Errorf("got %d ticks, want 3", got)
}
})
}
Important restrictions in synctest bubbles:
- Do not call
t.Run(), t.Parallel(), or t.Deadline()
- Channels created outside the bubble behave differently
- External I/O operations are not durably blocking
Use go-cmp for Comparisons
Prefer github.com/google/go-cmp/cmp over reflect.DeepEqual.
import "github.com/google/go-cmp/cmp"
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(User{})); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
Useful Test Failures
Include: what was wrong, inputs, actual result, expected result.
if got != want {
t.Error("wrong result")
}
if got != want {
t.Errorf("Foo(%q) = %d; want %d", input, got, want)
}
Use t.Fatal for Setup Failures
f, err := os.CreateTemp("", "test")
if err != nil {
t.Fatal("failed to set up test")
}
Interfaces Belong to Consumers
Define interfaces in the package that uses them, not the package that implements them.
package producer
type Thinger interface { Thing() bool }
func NewThinger() Thinger { return &thinger{} }
package producer
type Thinger struct{}
func (t *Thinger) Thing() bool { return true }
func NewThinger() *Thinger { return &Thinger{} }
package consumer
type Thinger interface { Thing() bool }
func Process(t Thinger) { }
Resource Management
runtime.AddCleanup (Go 1.24+)
Prefer runtime.AddCleanup over runtime.SetFinalizer for cleanup operations.
import "runtime"
type Resource struct {
handle uintptr
}
func NewResource() *Resource {
r := &Resource{handle: allocHandle()}
runtime.AddCleanup(r, func(handle uintptr) {
freeHandle(handle)
}, r.handle)
return r
}
Key advantages over SetFinalizer:
- Multiple cleanups per object
- Works with interior pointers
- No cycle-related leaks
- Object freed promptly (single GC cycle)
Weak Pointers (Go 1.24+)
The weak package provides weak references that don't prevent garbage collection.
import "weak"
type ExpensiveResource struct {
data []byte
}
func NewCache() *Cache {
return &Cache{
items: make(map[string]weak.Pointer[ExpensiveResource]),
}
}
type Cache struct {
mu sync.Mutex
items map[string]weak.Pointer[ExpensiveResource]
}
func (c *Cache) Get(key string) *ExpensiveResource {
c.mu.Lock()
defer c.mu.Unlock()
if wp, ok := c.items[key]; ok {
if r := wp.Value(); r != nil {
return r
}
delete(c.items, key)
}
return nil
}
func (c *Cache) Set(key string, r *ExpensiveResource) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = weak.Make(r)
}
Use cases for weak pointers:
- Caches that shouldn't prevent garbage collection
- Canonicalization maps (interning)
- Observer patterns where observers may be collected
Secure Directory Access with os.Root (Go 1.24+)
The os.Root type provides safe, scoped file system access that prevents path traversal attacks.
import "os"
func ServeUserFiles(userDir string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
root, err := os.OpenRoot(userDir)
if err != nil {
http.Error(w, "directory not found", http.StatusNotFound)
return
}
defer root.Close()
filename := r.URL.Query().Get("file")
f, err := root.Open(filename)
if err != nil {
http.Error(w, "file not found", http.StatusNotFound)
return
}
defer f.Close()
io.Copy(w, f)
}
}
Key benefits:
- Prevents path traversal vulnerabilities ("../" attacks)
- Symlinks cannot escape the root directory
- Race-condition safe (uses openat2 on Linux)
- Drop-in replacement for typical file operations
Patterns
Functional Options
Use functional options for configurable constructors with many optional parameters.
type Server struct {
addr string
timeout time.Duration
logger *slog.Logger
}
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) { s.timeout = d }
}
func WithLogger(l *slog.Logger) Option {
return func(s *Server) { s.logger = l }
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
timeout: 30 * time.Second,
logger: slog.Default(),
}
for _, opt := range opts {
opt(s)
}
return s
}
srv := NewServer("localhost:8080",
WithTimeout(60*time.Second),
WithLogger(logger),
)
Verify Interface Compliance
Use compile time checks to verify interface implementations.
type Handler struct{}
var _ http.Handler = (*Handler)(nil)
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {}
Defer for Cleanup
Use defer to clean up resources. The small overhead is worth the readability and safety.
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
Graceful Shutdown Pattern
Production servers need graceful shutdown to drain connections.
func main() {
srv := &http.Server{Addr: ":8080", Handler: handler}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
slog.Error("server error", "err", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
slog.Error("shutdown error", "err", err)
}
slog.Info("server stopped")
}
Start Enums at One
Zero values should represent invalid or unset state.
type Operation int
const (
OperationUnknown Operation = iota
OperationAdd
OperationSubtract
)
Use time Package for Time
Do not use integers for time. Use time.Time for instants and time.Duration for periods.
func poll(delay int) {
time.Sleep(time.Duration(delay) * time.Millisecond)
}
poll(10)
func poll(delay time.Duration) {
time.Sleep(delay)
}
poll(10 * time.Second)
Handle Type Assertions
Always use the two value form to avoid panics.
t := i.(string)
t, ok := i.(string)
if !ok {
}
Context as First Parameter
Context should be the first parameter, named ctx. Do not store context in structs.
func (s *Service) Process(ctx context.Context, req *Request) (*Response, error) {
}
Avoid Mutable Globals
Use dependency injection instead of modifying global state.
var db *sql.DB
func init() {
db, _ = sql.Open("postgres", os.Getenv("DSN"))
}
type Server struct {
db *sql.DB
}
func NewServer(db *sql.DB) *Server {
return &Server{db: db}
}
Avoid init()
Prefer explicit initialization in main. init() makes code harder to reason about and test.
Embed Static Files (Go 1.16+)
Use //go:embed for static assets.
import "embed"
var templates embed.FS
var configData []byte
Use Field Tags in Marshaled Structs
Explicit field names protect against accidental contract changes from refactoring.
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
Container and Runtime Considerations
Container-Aware GOMAXPROCS (Go 1.25+)
Go 1.25 automatically adjusts GOMAXPROCS based on container CPU limits.
This means Go programs in containers should now perform better out-of-the-box without manual GOMAXPROCS tuning.
Common Gotchas
Loop Variable Capture (Fixed in Go 1.22+)
Prior to Go 1.22, loop variables were reused. This is no longer an issue.
for _, v := range values {
go func() {
process(v)
}()
}
for _, v := range values {
v := v
go func() {
process(v)
}()
}
for _, v := range values {
go func() {
process(v)
}()
}
Defer Argument Evaluation
Defer evaluates arguments immediately, not when deferred function runs.
for i := 0; i < 5; i++ {
defer fmt.Println(i)
}
for _, f := range files {
defer f.Close()
}
for _, f := range files {
f := f
defer f.Close()
}
Nil Interface vs Nil Pointer
An interface containing a nil pointer is not nil.
type MyError struct{}
func (e *MyError) Error() string { return "error" }
func returnsError() error {
var e *MyError = nil
return e
}
if err := returnsError(); err != nil {
fmt.Println("error is not nil!")
}
func returnsError() error {
var e *MyError = nil
if e == nil {
return nil
}
return e
}
Use Result Before Checking Error (Go 1.25 Fix)
Go 1.25 fixed a compiler bug where using a result before checking for error sometimes didn't panic. Your code should always check errors first.
f, err := os.Open("file.txt")
fmt.Println(f.Name())
if err != nil {
return err
}
f, err := os.Open("file.txt")
if err != nil {
return err
}
fmt.Println(f.Name())
In Go 1.21-1.24, a compiler bug sometimes suppressed the panic. Go 1.25 correctly panics, so ensure your code follows the proper pattern.
Map Iteration Order
Map iteration order is randomized. Do not depend on it.
for k, v := range m {
results = append(results, v)
}
keys := slices.Sorted(maps.Keys(m))
for _, k := range keys {
results = append(results, m[k])
}
Slice Append Gotcha
Append may or may not allocate new backing array.
a := []int{1, 2, 3}
b := a[:2]
b = append(b, 4)
b := a[:2:2]
b = append(b, 4)
Experimental Features
encoding/json/v2 (Go 1.25, Experimental)
A new JSON engine is available with improved performance and streaming support.
import (
"encoding/json/jsontext"
"encoding/json/v2"
)
This is experimental and subject to change.
Documentation
Comment Sentences
Comments documenting declarations should be full sentences starting with the name being described.
type Request struct{}
func Encode(w io.Writer, req *Request) error {}
Package Comments
Package comments appear before the package declaration with no blank line.
package math
References
- Google Go Style Guide
- Uber Go Style Guide
- Effective Go
- Go Code Review Comments
- Go 1.23 Release Notes
- Go 1.24 Release Notes
- Go 1.25 Release Notes