开发环境和生产环境有很大不同。生产环境需要考虑性能、安全、可观测性等多个方面。这一章我们讨论 Gin 应用在生产环境中的配置要点。
Gin 有三种运行模式:
debug:开发模式,日志详细release:生产模式,日志简洁test:测试模式设置方式:
// 代码设置
gin.SetMode(gin.ReleaseMode)
// 环境变量
// export GIN_MODE=release
生产环境必须使用 release 模式,避免输出敏感信息和影响性能。
生产环境推荐使用结构化日志,便于检索和分析:
package main
import (
"os"
"time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
gin.SetMode(gin.ReleaseMode)
// 配置 zerolog
log.Logger = log.Output(zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: time.RFC3339,
}).With().Caller().Logger()
router := gin.New()
// 自定义日志中间件
router.Use(func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
latency := time.Since(start)
status := c.Writer.Status()
log.Info().
Str("method", c.Request.Method).
Str("path", path).
Str("query", query).
Int("status", status).
Dur("latency", latency).
Str("client_ip", c.ClientIP()).
Str("user_agent", c.Request.UserAgent()).
Msg("Request")
})
router.Use(gin.Recovery())
router.Run(":8080")
}
使用 lumberjack 进行日志轮转:
import (
"github.com/natefinch/lumberjack"
)
func setupLogger() {
logFile := &lumberjack.Logger{
Filename: "/var/log/myapp/app.log",
MaxSize: 100, // MB
MaxBackups: 3,
MaxAge: 28, // days
Compress: true,
}
gin.DefaultWriter = logFile
gin.DefaultErrorWriter = logFile
}
package config
import (
"os"
"strconv"
)
type Config struct {
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
Log LogConfig
}
type ServerConfig struct {
Port int
ReadTimeout int
WriteTimeout int
}
type DatabaseConfig struct {
Host string
Port int
User string
Password string
DBName string
}
type RedisConfig struct {
Addr string
Password string
DB int
}
type LogConfig struct {
Level string
Output string
}
func Load() *Config {
return &Config{
Server: ServerConfig{
Port: getEnvInt("PORT", 8080),
ReadTimeout: getEnvInt("READ_TIMEOUT", 10),
WriteTimeout: getEnvInt("WRITE_TIMEOUT", 10),
},
Database: DatabaseConfig{
Host: getEnv("DB_HOST", "localhost"),
Port: getEnvInt("DB_PORT", 5432),
User: getEnv("DB_USER", "postgres"),
Password: getEnv("DB_PASSWORD", ""),
DBName: getEnv("DB_NAME", "myapp"),
},
Redis: RedisConfig{
Addr: getEnv("REDIS_ADDR", "localhost:6379"),
Password: getEnv("REDIS_PASSWORD", ""),
DB: getEnvInt("REDIS_DB", 0),
},
Log: LogConfig{
Level: getEnv("LOG_LEVEL", "info"),
Output: getEnv("LOG_OUTPUT", "stdout"),
},
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if i, err := strconv.Atoi(value); err == nil {
return i
}
}
return defaultValue
}
敏感信息不要硬编码,使用环境变量或密钥管理服务:
# .env 文件(不要提交到版本控制)
DATABASE_URL=postgres://user:password@localhost:5432/myapp
REDIS_URL=redis://localhost:6379
JWT_SECRET=your-secret-key
API_KEY=your-api-key
生产环境必须有健康检查接口:
router.GET("/health", func(c *gin.Context) {
// 检查数据库连接
if err := db.Ping(); err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{
"status": "unhealthy",
"error": "database connection failed",
})
return
}
// 检查 Redis 连接
if err := redis.Ping(context.Background()).Err(); err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{
"status": "unhealthy",
"error": "redis connection failed",
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
})
})
// 就绪检查
router.GET("/ready", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"ready": true,
})
})
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status"},
)
httpRequestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: []float64{0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
},
[]string{"method", "path"},
)
)
func init() {
prometheus.MustRegister(httpRequestsTotal)
prometheus.MustRegister(httpRequestDuration)
}
func prometheusMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
duration := time.Since(start).Seconds()
status := strconv.Itoa(c.Writer.Status())
httpRequestsTotal.WithLabelValues(c.Request.Method, c.FullPath(), status).Inc()
httpRequestDuration.WithLabelValues(c.Request.Method, c.FullPath()).Observe(duration)
}
}
func main() {
router := gin.New()
router.Use(prometheusMiddleware())
// 指标端点
router.GET("/metrics", gin.WrapH(promhttp.Handler()))
// ... 其他路由
}
import "github.com/ulule/limiter/v3"
func securityMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 移除敏感头
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-Frame-Options", "DENY")
c.Header("X-XSS-Protection", "1; mode=block")
c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
c.Next()
}
}
import (
"github.com/ulule/limiter/v3"
mgin "github.com/ulule/limiter/v3/drivers/middleware/gin"
"github.com/ulule/limiter/v3/drivers/store/memory"
)
func rateLimitMiddleware() gin.HandlerFunc {
store := memory.NewStore()
rate := limiter.Rate{
Period: 1 * time.Minute,
Limit: 100,
}
instance := limiter.New(store, rate)
return mgin.NewMiddleware(instance)
}
import "github.com/gin-contrib/cors"
func main() {
router := gin.Default()
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"https://example.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
// ...
}
func main() {
router := gin.New()
// ... 路由配置
srv := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 关闭数据库连接
sqlDB, _ := db.DB()
sqlDB.Close()
// 关闭 Redis 连接
redis.Close()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("Server shutdown error: %v", err)
}
log.Println("Server stopped")
}
func recoveryMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
log.Error().
Str("path", c.Request.URL.Path).
Interface("error", err).
Msg("Panic recovered")
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal server error",
})
c.Abort()
}
}()
c.Next()
}
}
func notFoundHandler(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"error": "Not found",
})
}
func methodNotAllowedHandler(c *gin.Context) {
c.JSON(http.StatusMethodNotAllowed, gin.H{
"error": "Method not allowed",
})
}
生产环境配置的关键点:
release 模式运行生产环境的配置需要在实际运行中不断调整优化,关键是建立完善的监控和告警机制,及时发现问题并处理。