1. go
  2. /best practices
  3. /code-style

Code Style Best Practices in Go

Writing clean, idiomatic Go code is essential for maintainability and collaboration. This guide covers best practices for Go code style.

Naming Conventions

1. Package Names

Follow package naming conventions:

// Good: Simple, clear package names
package user
package database
package httputil

// Bad: Mixed case, underscores
package userService
package data_store
package HTTPUtil

// Good: Short but descriptive
package auth
package config
package cache

// Bad: Too generic or unclear
package utils
package helpers
package stuff

2. Variable Names

Use clear, descriptive variable names:

// Good: Clear and descriptive
var (
    userID      string
    maxRetries  int
    isValid     bool
    retryCount  int
)

// Bad: Unclear or too short
var (
    uid    string
    max    int
    valid  bool
    rc     int
)

// Good: Loop variables
for i := 0; i < len(items); i++ {}
for index, item := range items {}
for key, value := range map {}

// Bad: Meaningless loop variables
for x := 0; x < len(items); x++ {}
for a, b := range items {}

3. Interface Names

Follow interface naming conventions:

// Good: Single method interfaces end in 'er'
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// Good: Descriptive interface names
type UserService interface {
    Create(user User) error
    Update(user User) error
    Delete(id string) error
}

// Bad: Unclear or redundant names
type IUserService interface {}
type UserInterface interface {}

Code Organization

1. File Structure

Organize files logically:

// main.go - Application entry point
package main

import (
    "myapp/internal/server"
    "myapp/internal/config"
)

func main() {
    cfg := config.Load()
    srv := server.New(cfg)
    srv.Start()
}

// user/service.go - Business logic
package user

type Service struct {
    repo Repository
}

func NewService(repo Repository) *Service {
    return &Service{repo: repo}
}

// user/repository.go - Data access
package user

type Repository interface {
    Find(id string) (*User, error)
    Save(user *User) error
}

2. Import Organization

Organize imports properly:

// Good: Grouped imports
import (
    // Standard library
    "context"
    "fmt"
    "time"
    
    // Third-party packages
    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
    
    // Internal packages
    "myapp/internal/config"
    "myapp/internal/models"
)

// Bad: Unsorted imports
import (
    "myapp/internal/models"
    "fmt"
    "github.com/gin-gonic/gin"
    "context"
)

Code Structure

1. Function Organization

Structure functions clearly:

// Good: Clear function organization
type UserService struct {
    repo Repository
    logger *zap.Logger
}

// Constructor first
func NewUserService(repo Repository, logger *zap.Logger) *UserService {
    return &UserService{
        repo:   repo,
        logger: logger,
    }
}

// Public methods next
func (s *UserService) CreateUser(ctx context.Context, user User) error {
    if err := s.validateUser(user); err != nil {
        return fmt.Errorf("invalid user: %w", err)
    }
    return s.repo.Create(ctx, user)
}

// Private methods last
func (s *UserService) validateUser(user User) error {
    if user.Name == "" {
        return errors.New("name is required")
    }
    return nil
}

2. Error Handling

Follow error handling conventions:

// Good: Error wrapping
func ProcessFile(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("failed to open file: %w", err)
    }
    defer file.Close()
    
    return nil
}

// Bad: Error hiding
func ProcessFile(path string) error {
    file, _ := os.Open(path) // Don't ignore errors
    defer file.Close()
    
    return nil
}

// Good: Error handling with cleanup
func ProcessData() (err error) {
    resource, err := acquireResource()
    if err != nil {
        return fmt.Errorf("failed to acquire resource: %w", err)
    }
    defer func() {
        if cerr := resource.Close(); cerr != nil && err == nil {
            err = cerr
        }
    }()
    
    return process(resource)
}

Code Formatting

1. Line Length

Keep lines readable:

// Good: Readable line length
func ProcessItems(
    ctx context.Context,
    items []Item,
    options Options,
) ([]Result, error) {
    // Processing logic
}

// Bad: Long line
func ProcessItems(ctx context.Context, items []Item, options Options, callback func(Item) error, errorHandler func(error) error) ([]Result, error) {
    // Processing logic
}

// Good: Split long lines
var (
    defaultTimeout = 30 * time.Second
    maxRetries     = 3
    backoffFactor  = 2.0
)

// Bad: Long line
var defaultTimeout, maxRetries, backoffFactor = 30 * time.Second, 3, 2.0

2. Alignment

Align code for readability:

// Good: Aligned struct fields
type Config struct {
    Host        string
    Port        int
    Timeout     time.Duration
    MaxRetries  int
    EnableTLS   bool
}

// Good: Aligned const declarations
const (
    StatusPending   = "pending"
    StatusActive    = "active"
    StatusInactive  = "inactive"
    StatusDeleted   = "deleted"
)

// Bad: Unaligned declarations
const (
    ErrNotFound = errors.New("not found")
    ErrInvalidInput = errors.New("invalid input")
    ErrUnauthorized = errors.New("unauthorized")
)

Documentation

1. Package Documentation

Document packages properly:

// Package user provides functionality for user management.
// It includes user creation, authentication, and authorization.
package user

// Package config handles application configuration loading
// and validation from various sources (env, file, etc.).
package config

2. Function Documentation

Document functions clearly:

// CreateUser creates a new user in the system.
// It validates the user input, checks for duplicates,
// and stores the user in the database.
//
// If the user email already exists, it returns ErrDuplicateEmail.
// If the user input is invalid, it returns ErrInvalidInput.
func CreateUser(ctx context.Context, user User) error {
    // Implementation
}

// FindByID retrieves a user by their ID.
// It returns ErrNotFound if the user doesn't exist.
func FindByID(ctx context.Context, id string) (*User, error) {
    // Implementation
}

Testing

1. Test Organization

Organize tests clearly:

func TestUserService_CreateUser(t *testing.T) {
    // Table-driven tests
    tests := []struct {
        name    string
        user    User
        wantErr bool
    }{
        {
            name: "valid user",
            user: User{
                Name:  "John Doe",
                Email: "[email protected]",
            },
            wantErr: false,
        },
        {
            name: "invalid email",
            user: User{
                Name:  "John Doe",
                Email: "invalid",
            },
            wantErr: true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            service := NewUserService(mockRepo)
            err := service.CreateUser(context.Background(), tt.user)
            if (err != nil) != tt.wantErr {
                t.Errorf("CreateUser() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

2. Test Helpers

Create clear test helpers:

// Test helpers at the top of the test file
func setupTestDB(t *testing.T) (*sql.DB, func()) {
    t.Helper()
    
    db, err := sql.Open("postgres", testDBURL)
    if err != nil {
        t.Fatalf("failed to open DB: %v", err)
    }
    
    return db, func() {
        db.Close()
    }
}

func createTestUser(t *testing.T, db *sql.DB) User {
    t.Helper()
    
    user := User{
        Name:  "Test User",
        Email: "[email protected]",
    }
    
    // Create user in test DB
    return user
}

Next Steps