Golang - Gin Guidelines

# Golang - Gin Guidelines

Junie guidelines for developing go services with the [gin-gonic](https://gin-gonic.com) framework.

## 1. Organize Project Structure

* Follow a domain-driven or feature-based structure rather than organizing by technical layers
* Keep related functionality together to improve code discoverability
* Use a consistent naming convention for packages and files

**Explanation:**

* A well-organized project structure makes it easier to understand the codebase, locate files, and maintain the application over time.
* Domain or feature-based organization helps developers find all related code (handlers, services, models) in one place.
* A consistent structure reduces cognitive load when navigating the codebase.

```go
// Example project structure
project/
├── cmd/
│   └── main.go               // Application entry point
├── internal/                 // Private application code
│   ├── auth/                 // Auth feature
│   │   ├── handler.go        // HTTP handlers
│   │   ├── middleware.go     // Auth middleware
│   │   ├── service.go        // Business logic
│   │   └── repository.go     // Data access
│   ├── user/                 // User feature
│   └── product/              // Product feature
├── pkg/                      // Public libraries
│   ├── database/             // Database utilities
│   └── validator/            // Validation utilities
├── api/                      // API documentation
├── config/                   // Configuration files
└── go.mod                    // Go module definition
```

## 2. Dependency Injection with Explicit Construction

* Create service structs with explicit dependencies passed via constructors
* Avoid global variables or singletons
* Use interfaces to define dependencies for better testability

**Explanation:**

* Explicit dependency injection makes code more maintainable, testable, and readable
* Dependencies are clearly visible in function signatures rather than hidden in implementation
* This approach enables mocking dependencies for unit testing

```go
// user/repository.go
type Repository interface {
    FindByID(ctx context.Context, id string) (*User, error)
    // Other methods...
}

type postgresRepository struct {
    db *sql.DB
}

func NewRepository(db *sql.DB) Repository {
    return &postgresRepository{db: db}
}

// user/service.go
type Service struct {
    repo Repository
    logger *log.Logger
}

func NewService(repo Repository, logger *log.Logger) *Service {
    return &Service{
        repo: repo,
        logger: logger,
    }
}
```

## 3. Centralized Error Handling

* Define custom error types for different error categories
* Use middleware to catch and handle errors consistently
* Return structured error responses with appropriate HTTP status codes

**Explanation:**

* Consistent error handling improves user experience and makes debugging easier
* Centralized error handling avoids code duplication and ensures uniform error responses
* Structured error responses provide clear information to API consumers

```go
// pkg/errors/errors.go
type AppError struct {
    Type    string `json:"type"`
    Message string `json:"message"`
    Code    int    `json:"-"` // HTTP status code
}

func (e AppError) Error() string {
    return e.Message
}

// Predefined error types
var (
    ErrNotFound = func(msg string) AppError {
        return AppError{Type: "not_found", Message: msg, Code: http.StatusNotFound}
    }
    
    ErrBadRequest = func(msg string) AppError {
        return AppError{Type: "bad_request", Message: msg, Code: http.StatusBadRequest}
    }
    
    // Other error types...
)

// middleware/error_handler.go
func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        
        if len(c.Errors) > 0 {
            err := c.Errors.Last().Err
            
            var appErr AppError
            if errors.As(err, &appErr) {
                c.JSON(appErr.Code, gin.H{
                    "error": appErr,
                })
                return
            }
            
            // Handle unexpected errors
            c.JSON(http.StatusInternalServerError, gin.H{
                "error": gin.H{
                    "type": "internal_error",
                    "message": "An unexpected error occurred",
                },
            })
        }
    }
}
```

## 4. Secure Middleware Configuration

* Configure security-related middleware in the correct order
* Use HTTPS by default in production
* Implement proper CORS, CSP, and other security headers

**Explanation:**

* Security middleware protects your application from common attacks
* The order of middleware is crucial - some security protections must be applied before others
* Well-configured security headers protect against XSS, CSRF, and other common vulnerabilities

```go
func setupRouter() *gin.Engine {
    // Use release mode in production
    gin.SetMode(gin.ReleaseMode)
    
    r := gin.Default()
    
    // Recovery middleware recovers from panics
    r.Use(gin.Recovery())
    
    // Custom logger that doesn't log sensitive data
    r.Use(middleware.SecureLogger())
    
    // Set security headers
    r.Use(middleware.SecurityHeaders())
    
    // CORS configuration
    r.Use(middleware.ConfigureCORS())
    
    // Rate limiting
    r.Use(middleware.RateLimiter())
    
    // Error handling
    r.Use(middleware.ErrorHandler())
    
    // Request ID for tracing
    r.Use(middleware.RequestID())
    
    // Add routes...
    
    return r
}

// middleware/security.go
func SecurityHeaders() gin.HandlerFunc {
    return func(c *gin.Context) {
        // Prevent MIME type sniffing
        c.Header("X-Content-Type-Options", "nosniff")
        
        // Prevent clickjacking
        c.Header("X-Frame-Options", "DENY")
        
        // XSS protection
        c.Header("X-XSS-Protection", "1; mode=block")
        
        // Content Security Policy
        c.Header("Content-Security-Policy", "default-src 'self'")
        
        // HTTP Strict Transport Security (for HTTPS)
        c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        
        c.Next()
    }
}
```

## 5. Input Validation and Sanitization

* Validate all input data before processing
* Use a structured validation library compatible with Gin
* Sanitize inputs to prevent injection attacks

**Explanation:**

* Input validation is the first line of defense against many attacks
* Structured validation makes requirements clear and helps catch errors early
* Proper sanitization prevents SQL injection, XSS, and other injection attacks

```go
// user/handler.go
type CreateUserRequest struct {
    Username string `json:"username" binding:"required,alphanum,min=3,max=30"`
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=12"`
}

func (h *Handler) CreateUser(c *gin.Context) {
    var req CreateUserRequest
  // Binding does the validation
    if err := c.ShouldBindJSON(&req); err != nil {
        c.Error(errors.ErrBadRequest("Invalid input: " + err.Error()))
        return
    }
    
    // Sanitize inputs to prevent XSS
    req.Username = bluemonday.StrictPolicy().Sanitize(req.Username)
    req.Email = bluemonday.StrictPolicy().Sanitize(req.Email)
    
    // Process the validated and sanitized request...
    
    // Return response
    c.JSON(http.StatusCreated, gin.H{"message": "User created"})
}
```

## 6. Secure Authentication and Authorization

* Use JWT or sessions with proper security configurations
* Don't store and use passwords, instead rely on OAuth2 and OIDC protocols
* If you must store passwords, use strong hashing algorithm (bcrypt/argon2)
* Implement proper authorization checks at every secured endpoint

**Explanation:**

* Authentication verifies user identity; authorization verifies permissions
* Secure storage of credentials is essential to prevent data breaches
* Best way would be to not store any user-credentials directly but instead rely on OAuth2 Identity Providers to handle Authentication for you.
* Authorization checks must be consistent across all endpoints

## 7. Database Access Best Practices

* Use prepared statements to prevent SQL injection
* Implement context-aware database calls
* Apply proper database connection management

**Explanation:**

* Prepared statements protect against SQL injection attacks
* Context-aware calls allow for proper timeout and cancellation
* Proper connection management prevents resource leaks

```go
// user/repository.go
func (r *postgresRepository) FindByID(ctx context.Context, id string) (*User, error) {
    // Use prepared statement with placeholder
    query := "SELECT id, username, email, created_at FROM users WHERE id = $1"
    
    var user User
    err := r.db.QueryRowContext(ctx, query, id).Scan(
        &user.ID,
        &user.Username,
        &user.Email,
        &user.CreatedAt,
    )
    
    if err != nil {
        if err == sql.ErrNoRows {
            return nil, errors.ErrNotFound("User not found")
        }
        return nil, fmt.Errorf("database error: %w", err)
    }
    
    return &user, nil
}

// main.go
func setupDatabase() (*sql.DB, error) {
    db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        return nil, err
    }
    
    // Set connection pool parameters
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(5)
    db.SetConnMaxLifetime(5 * time.Minute)
    
    // Test connection
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    if err := db.PingContext(ctx); err != nil {
        return nil, err
    }
    
    return db, nil
}
```

## 8. Structured Logging

* Use a structured logging library (e.g.: slog)
* Include contextual information in logs (e.g.: traceId)
* Avoid logging sensitive information

**Explanation:**

* Structured logs are easier to parse and analyze
* Contextual information makes troubleshooting more efficient
* Secure logging prevents leaking sensitive data

```go
// ConfigureLogger sets up the global logger with the specified log level from environment
func ConfigureLogger() {
  // Parse log level from environment
  env := os.Getenv("LOG_LEVEL")
  var level slog.Level
  switch strings.ToLower(env) {
  case "debug":
    level = slog.LevelDebug
  case "info":
    level = slog.LevelInfo
  case "warn":
    level = slog.LevelWarn
  case "error":
    level = slog.LevelError
  default:
    level = slog.LevelInfo
  }

  // Create logger with timestamp and level
  logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: level,
  }))

  // Set as default logger
  slog.SetDefault(logger)
  slog.Info("logging level set", "level", level.String())
}
```

## 9. API Design and Response Structure

* Define consistent response formats
* Use appropriate HTTP status codes
* Include pagination for list endpoints

**Explanation:**

* Consistent responses make APIs easier to consume
* Proper HTTP status codes communicate intent clearly
* Pagination prevents performance issues with large datasets

## 10. Effective Testing

* Write unit tests for business logic
* Use Go's testing package and testify for assertions
* Implement integration tests for critical paths

**Explanation:**

* Tests ensure code correctness and prevent regressions
* Unit tests focus on business logic without external dependencies
* Integration tests verify that components work together correctly

```go
// user/service_test.go
func TestUserService_CreateUser(t *testing.T) {
    // Create a mock repository
    mockRepo := new(mocks.Repository)
    
    // Set expectations
    mockRepo.On("FindByEmail", mock.Anything, "[email protected]").
        Return(nil, errors.ErrNotFound("User not found"))
    
    mockRepo.On("Create", mock.Anything, mock.MatchedBy(func(u *User) bool {
        return u.Email == "[email protected]" && u.Username == "testuser"
    })).Return("user-id", nil)
    
    // Create service with mock dependencies
    service := NewService(mockRepo, log.New(os.Stdout, "", 0))
    
    // Call the method being tested
    id, err := service.CreateUser(context.Background(), CreateUserInput{
        Username: "testuser",
        Email:    "[email protected]",
        Password: "securepassword",
    })
    
    // Assert results
    assert.NoError(t, err)
    assert.Equal(t, "user-id", id)
    
    // Verify expectations were met
    mockRepo.AssertExpectations(t)
}

// integration_test.go
func TestUserAPI_Integration(t *testing.T) {
    // Skip if not running integration tests
    if testing.Short() {
        t.Skip("Skipping integration tests")
    }
    
    // Setup test database and server
    db := setupTestDatabase(t)
    router := setupRouter(db)
    
    // Create test server
    ts := httptest.NewServer(router)
    defer ts.Close()
    
    // Test creating a user
    resp, body := testRequest(t, ts, "POST", "/api/v1/users", map[string]interface{}{
        "username": "integrationtest",
        "email":    "[email protected]",
        "password": "secure-password-123",
    })
    
    assert.Equal(t, http.StatusCreated, resp.StatusCode)
    
    var response map[string]interface{}
    err := json.Unmarshal(body, &response)
    assert.NoError(t, err)
    
    data := response["data"].(map[string]interface{})
    assert.Equal(t, "integrationtest", data["username"])
    assert.Equal(t, "[email protected]", data["email"])
    assert.NotContains(t, data, "password")
}
```

## 11. Configuration Management

* Use environment variables for configuration
* Implement secure handling of secrets
* Provide sensible defaults

**Explanation:**

* Environment variables are the standard for configuration in containers
* Secrets should never be hardcoded or committed to version control
* Sensible defaults make the application easier to run

```go
type Config struct {
  Enabled          bool   `mapstructure:"enabled"`
  Broker           string `mapstructure:"broker"`
  ConnectionString string `mapstructure:"connection_string"`
  ClientID         string `mapstructure:"client_id"`
  Topic            string `mapstructure:"topic"`
}

// PrepareEnvironment loads the correct config yaml file
func PrepareEnvironment(profile string) (Config, error) {
  var cfg Config
  viper.SetConfigName(profile)
  viper.SetConfigType("yaml")
  viper.AutomaticEnv()
  viper.SetEnvKeyReplacer(strings.NewReplacer(`.`, `__`))

  data, err := config.Dir.ReadFile(fmt.Sprintf("yaml/%s.yaml", profile))
  if err != nil {
    return cfg, err
  }

  if err := viper.ReadConfig(bytes.NewReader(data)); err != nil {
    return cfg, err
  }
  if err := viper.Unmarshal(&cfg); err != nil {
    return cfg, err
  }

  return cfg, nil
}
```

## 12. Context Propagation

* Use context for request scoped values and cancellation
* Propagate context through all layers of the application
* Set appropriate timeouts

**Explanation:**

* Context propagation ensures proper request handling and cancellation
* Request-scoped values (user ID, tracing ID) should be passed via context
* Timeouts prevent long-running operations from consuming resources

```go
// user/handler.go
func (h *Handler) GetUser(c *gin.Context) {
    // Extract user ID from URL
    userID := c.Param("id")
    
    // Create context with timeout
    ctx, cancel := context.WithTimeout(c.Request.Context(), 3*time.Second)
    defer cancel()
    
    // Call service with context
    user, err := h.service.GetUserByID(ctx, userID)
    if err != nil {
        c.Error(err)
        return
    }
    
    response.Success(c, http.StatusOK, user)
}

// user/service.go
func (s *Service) GetUserByID(ctx context.Context, id string) (*User, error) {
    // Log with request context
  log.Ctx(ctx).
    Info().
    Str("user_id", id).
    Msg("Getting user by ID")
    
    // Use the same context for repository call
    user, err := s.repo.FindByID(ctx, id)
    if err != nil {
        return nil, err
    }
    
    return user, nil
}
```

## 13. Graceful Shutdown

* Implement graceful shutdown to handle in-flight requests
* Close resources properly when shutting down
* Use appropriate timeouts for shutdown

**Explanation:**

* Graceful shutdown ensures in-flight requests are completed
* Proper resource cleanup prevents leaks
* Shutdown timeouts prevent hanging during termination

```go
func main() {
  router := gin.Default()
  router.GET("/", func(c *gin.Context) {
    c.String(http.StatusOK, "Welcome Gin Server")
  })

  srv := &http.Server{
    Addr:    ":8080",
    Handler: router.Handler(),
  }

  go func() {
    // service connections
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
      log.Fatalf("listen: %s\n", err)
    }
  }()

  // Wait for interrupt signal to gracefully shutdown the server with
  // a timeout of 5 seconds.
  quit := make(chan os.Signal, 1)
  // kill (no params) by default sends syscall.SIGTERM
  // kill -2 is syscall.SIGINT
  // kill -9 is syscall.SIGKILL but can't be caught, so don't need add it
  signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
  <-quit
  log.Println("Shutdown Server ...")

  ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  defer cancel()
  if err := srv.Shutdown(ctx); err != nil {
    log.Println("Server Shutdown:", err)
  }
  // catching ctx.Done(). timeout of 5 seconds.
  <-ctx.Done()
  log.Println("timeout of 5 seconds.")
  log.Println("Server exiting")
}
```