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
- Learn about Error Handling
- Explore Performance
- Study Security