Refactoring Go Web Projects: HTTP Layer Separation with an API Package and When to Use init Functions
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
apidirectory - 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()
Recommended Import Pattern
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
apidirectory - 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
- API Versioning: Consider adding versioning in the future (such as
/api/v1) - Error-Handling Middleware: Standardize the API error response format
- Configuration Validation: Validate configuration completeness at startup
- 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
Comments 0