Best Practices for Go Development
This guide covers essential best practices for developing Go applications, helping you write clean, efficient, and maintainable code.
Project Structure
Standard Project Layout
├── cmd/
│ └── myapp/
│ └── main.go
├── internal/
│ ├── app/
│ │ └── app.go
│ ├── pkg1/
│ │ └── pkg1.go
│ └── pkg2/
│ └── pkg2.go
├── pkg/
│ └── shared/
│ └── shared.go
├── api/
│ └── openapi.yaml
├── web/
│ ├── templates/
│ └── static/
├── configs/
│ └── config.yaml
├── test/
│ └── integration/
├── scripts/
│ └── build.sh
├── docs/
│ └── README.md
├── go.mod
└── go.sum
Package Organization
// internal/app/app.go
package app
type App struct {
config Config
logger Logger
db Database
}
func New(config Config) *App {
return &App{
config: config,
logger: newLogger(config),
db: newDatabase(config),
}
}
Error Handling
Error Types
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s with ID %s not found", e.Resource, e.ID)
}
// Usage
if err := findUser(id); err != nil {
var nfe *NotFoundError
if errors.As(err, &nfe) {
// Handle not found case
return
}
// Handle other errors
}
Error Wrapping
func processUser(id string) error {
user, err := findUser(id)
if err != nil {
return fmt.Errorf("finding user %s: %w", id, err)
}
if err := validateUser(user); err != nil {
return fmt.Errorf("validating user %s: %w", id, err)
}
return nil
}
Logging
Structured Logging
import "go.uber.org/zap"
func initLogger() *zap.Logger {
config := zap.NewProductionConfig()
config.OutputPaths = []string{"stdout", "app.log"}
logger, _ := config.Build()
return logger
}
func processRequest(logger *zap.Logger, req *Request) {
logger.Info("processing request",
zap.String("request_id", req.ID),
zap.String("user", req.UserID),
zap.Duration("latency", req.Duration),
)
}
Configuration
Using Configuration Management
type Config struct {
Server struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
} `yaml:"server"`
Database struct {
DSN string `yaml:"dsn"`
} `yaml:"database"`
Logger struct {
Level string `yaml:"level"`
} `yaml:"logger"`
}
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config file: %w", err)
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("parsing config: %w", err)
}
return &config, nil
}
Performance
Profiling
import "runtime/pprof"
func profile() {
f, _ := os.Create("cpu.prof")
defer f.Close()
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Code to profile
}
Memory Management
// Good: Reuse slices
func processItems(items []Item) {
// Preallocate slice with known capacity
results := make([]Result, 0, len(items))
for _, item := range items {
results = append(results, process(item))
}
}
// Bad: Growing slice without preallocation
func processItemsBad(items []Item) {
var results []Result // Will need to grow multiple times
for _, item := range items {
results = append(results, process(item))
}
}
Security
Input Validation
func validateInput(input string) error {
if len(input) > 100 {
return errors.New("input too long")
}
if !regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(input) {
return errors.New("input contains invalid characters")
}
return nil
}
Secure Configuration
func loadSecrets() error {
// Use environment variables for sensitive data
dbPassword := os.Getenv("DB_PASSWORD")
if dbPassword == "" {
return errors.New("DB_PASSWORD environment variable not set")
}
// Never log sensitive information
log.Info("database connection established")
return nil
}
Code Style
Consistent Formatting
// Use gofmt
// Run: gofmt -w .
// Use consistent naming
type User struct {
ID string // Use ID, not Id
CreatedAt time.Time // Use CreatedAt, not Created_At
}
// Use meaningful variable names
func processTransaction(tx *Transaction) error {
// Good: Clear and descriptive
userID := tx.UserID
amount := tx.Amount
// Bad: Unclear abbreviations
uid := tx.UserID
amt := tx.Amount
}
Documentation
// Package users provides functionality for user management.
package users
// User represents a system user with associated metadata.
type User struct {
ID string
Name string
Email string
Created time.Time
}
// FindByID retrieves a user by their unique identifier.
// Returns ErrNotFound if the user doesn't exist.
func FindByID(id string) (*User, error) {
// Implementation
}
Testing
Table-Driven Tests
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
}{
{
name: "valid email",
email: "[email protected]",
wantErr: false,
},
{
name: "invalid email",
email: "invalid-email",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateEmail(tt.email)
if (err != nil) != tt.wantErr {
t.Errorf("validateEmail() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
Next Steps
- Learn about Project Structure
- Study Error Handling
- Explore Logging
- Understand Configuration
- Master Performance
- Practice Security
- Follow Code Style