Wood Chen

Refactoring Go Web Projects: HTTP Layer Separation with an API Package and When to Use init Functions

0 comments61 views471 words

This post was translated from Chinese by AI. If anything reads oddly, the Chinese original is authoritative. 中文原文

Overview

This article explores best practices for separating the HTTP layer and managing initialization in Go web projects, based on an architecture refactoring of CZL Connect (an OAuth2/OIDC authentication service). It applies to Go web applications using Gin or similar architectures.

The core idea: reorganize HTTP-related code through sensible directory structure changes to improve clarity and maintainability.

Main Changes

1. Directory Structure Changes

Before

internal/
├── handler/         # HTTP handlers
├── middleware/      # Middleware
├── router/          # Route definitions
├── service/         # Business logic
├── model/          # Data models
├── ...

After

internal/
├── api/            # HTTP presentation layer
│   ├── handler/    # HTTP handlers
│   ├── middleware/ # Middleware
│   └── router.go   # Route definitions
├── service/        # Business logic
├── model/         # Data models
├── config/        # Configuration management (moved from the root directory)
├── init/          # System initialization
├── ...

2. Key File Path Changes

Component Previous Path New Path
Route definitions internal/router/router.go internal/api/router.go
HTTP handlers internal/handler/ internal/api/handler/
Middleware internal/middleware/ internal/api/middleware/
Configuration management config/ internal/config/

Architectural Benefits

1. Clearer Layers

  • Presentation Layer: internal/api/ - All HTTP-related code
  • Business Layer: internal/service/ - Business logic
  • Data Layer: internal/model/ - Data models

2. Better Code Organization

  • HTTP-related components are grouped in the api directory
  • Configuration management is centralized under internal
  • Follows the standard Go project layout

3. Easier Maintenance

  • API layer changes are confined to one directory
  • Reduces the complexity of searching across directories
  • Makes the project structure easier to understand

Guidelines for Using the init Package

Analysis of the Current Design

Initialization Flow in main.go

func main() {
    // 1. Initialize configuration
    config.Init()
    
    // 2. Initialize the database (with retries)
    config.InitDBWithRetry(10, 3*time.Second)
    
    // 3. Initialize Redis (failure does not terminate the program)
    config.InitRedis(config.AppConfig)
    
    // 4. Initialize system settings (depends on the database)
    systemInit.InitSystem()
    
    // 5. Start the server
}

Guidelines for Using init Functions

✅ Appropriate Uses of init Functions

// Upstream provider registration - simple registration logic with no dependencies
func init() {
    upstream.RegisterProvider("github", &GitHubProvider{})
}

Characteristics:

  • Requires no arguments
  • Cannot fail
  • Pure registration logic
  • No complex dependencies

❌ Inappropriate Uses of init Functions

// Database initialization - requires arguments, error handling, and dependency ordering
config.InitDBWithRetry(10, 3*time.Second)

Reasons:

  • Requires arguments (retry count, timeout)
  • Requires complex error-handling strategies
  • Has explicit dependency ordering requirements
  • Requires different handling strategies on failure

Initialization Error-Handling Strategies

// Database failure → terminate the program
if err := config.InitDBWithRetry(...); err != nil {
    log.Fatalf("数据库初始化失败: %v", err)
}

// Redis failure → keep the program running
if err := config.InitRedis(...); err != nil {
    log.Printf("Redis初始化失败: %v", err)
    log.Println("程序将继续运行,但Redis功能将不可用")
}

Package Imports and Naming

Resolving Package Name Conflicts

// Use an alias to avoid a conflict with Go's built-in init keyword
systemInit "czlconnect/internal/init"

// Call the initialization function
systemInit.InitSystem()
import (
    // Standard library
    "log"
    "time"
    
    // Project packages
    "czlconnect/internal/api"
    "czlconnect/internal/config"
    systemInit "czlconnect/internal/init"  // Use an alias
    
    // Automatically registered upstream providers
    _ "czlconnect/pkg/upstream/github"
    _ "czlconnect/pkg/upstream/discourse"
    
    // Third-party libraries
    "github.com/gin-gonic/gin"
)

Best Practices Summary

1. Code Organization

  • Organize directories by functional layer
  • Keep HTTP layer code in the api directory
  • Separate business logic from the presentation layer

2. Initialization Management

  • Keep complex initialization in the main function
  • Use explicit calls rather than automatic init functions
  • Apply different error-handling strategies based on business requirements

3. Package Management

  • Use meaningful package aliases
  • Follow Go import conventions
  • Distinguish between explicit calls and automatic registration

Suggestions for Further Improvements

  1. API Versioning: Consider adding versioning in the future (such as /api/v1)
  2. Error-Handling Middleware: Standardize the API error response format
  3. Configuration Validation: Validate configuration completeness at startup
  4. Service Factory Pattern: Consider dependency injection improvements if needed

Project Background

CZL Connect is an OAuth2/OIDC authentication aggregation service built with Go + Gin + GORM, with the following characteristics:

  • Backend: Go + Gin framework + PostgreSQL + Redis
  • Frontend: Next.js 15 + TypeScript + shadcn/ui
  • Architecture: Layered architecture with support for multiple upstream authentication providers
  • Scale: A medium-sized project with user management, application management, analytics, and other features

The architectural changes described here can serve as a reference for Go web projects of a similar size.


Implementation Date: 2025-08-11Applicable Scenarios: Medium-sized Go web projects using frameworks such as GinArchitecture Pattern: Layered architecture + domain-driven design

Related posts

Comments 0