feat: 添加服务和数据库初始化

This commit is contained in:
2026-03-02 23:30:00 +08:00
parent 607ff9a055
commit 52cfe1b911
9 changed files with 1091 additions and 33 deletions

321
README.md Normal file
View File

@@ -0,0 +1,321 @@
# go-web-gin
一个基于 [Gin](https://github.com/gin-gonic/gin) 的 Go Web 应用脚手架,提供开箱即用的基础设施组件。
## 特性
- 🚀 **快速启动** - 简洁的 API 设计,快速搭建 Web 服务
- 🔧 **配置管理** - 支持 YAML 配置文件和环境变量覆盖
- 📝 **结构化日志** - 基于 [Zap](https://github.com/uber-go/zap) 的高性能日志
- 🗄️ **数据库支持** - MySQL (GORM) 和 Redis 单例连接
- 🌍 **多环境** - 支持 Local、Development、Production 环境
- 🧩 **单例模式** - 组件单例管理,避免重复初始化
## 安装
```bash
go get git.hujye.com/infrastructure/go-web-gin
```
## 快速开始
### 1. 创建配置文件
在项目根目录创建 `config/config.yml`
```yaml
server:
host: 0.0.0.0
port: 8080
mode: debug # debug, release, test
app:
name: my-app
environment: local
log_level: info
log:
output_to_file: true
filename: logs/app.log
max_size: 100
max_backups: 3
max_age: 28
compress: true
mysql:
host: 127.0.0.1
port: 3306
user: root
password: ""
db_name: test
charset: utf8mb4
parse_time: true
max_idle_conns: 10
max_open_conns: 100
conn_max_lifetime: 3600
redis:
addr: 127.0.0.1:6379
password: ""
db: 0
pool_size: 10
min_idle_conns: 5
max_retries: 3
dial_timeout: 5
read_timeout: 3
write_timeout: 3
```
### 2. 编写主程序
```go
package main
import (
"context"
go_web_gin "git.hujye.com/infrastructure/go-web-gin"
"github.com/gin-gonic/gin"
)
func main() {
// 创建应用实例
app := go_web_gin.New()
// 初始化数据库(可选)
app.InitMySQL()
app.InitRedis()
// 注册全局中间件
app.UseMiddleware(gin.Recovery())
// 注册路由
app.RegisterRoutes(func(e *gin.Engine) {
e.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
e.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
// 使用日志
app.Logger().Info(c.Request.Context(), "get user", "id", id)
c.JSON(200, gin.H{"user_id": id})
})
// 使用数据库
e.GET("/db-test", func(c *gin.Context) {
db := app.DB()
if db != nil {
// 执行数据库操作
c.JSON(200, gin.H{"db": "connected"})
} else {
c.JSON(500, gin.H{"error": "db not initialized"})
}
})
// 使用 Redis
e.GET("/redis-test", func(c *gin.Context) {
rdb := app.Redis()
if rdb != nil {
rdb.Set(c.Request.Context(), "test", "value", 0)
c.JSON(200, gin.H{"redis": "connected"})
} else {
c.JSON(500, gin.H{"error": "redis not initialized"})
}
})
})
// 启动服务
// 非生产环境可传入自定义地址,生产环境使用配置文件地址
app.Run()
}
```
## API 文档
### App 结构
```go
type App struct {
engine *gin.Engine
db *gorm.DB
rdb *redis.Client
logger *logger.Logger
}
```
### 方法列表
| 方法 | 说明 | 返回值 |
|------|------|--------|
| `New()` | 创建新的 App 实例 | `*App` |
| `UseMiddleware(...)` | 注册全局中间件 | - |
| `RegisterRoutes(func)` | 注册路由 | - |
| `InitMySQL()` | 初始化 MySQL 连接 | `*gorm.DB` |
| `InitRedis()` | 初始化 Redis 连接 | `*redis.Client` |
| `DB()` | 获取数据库实例 | `*gorm.DB` |
| `Redis()` | 获取 Redis 实例 | `*redis.Client` |
| `Logger()` | 获取日志实例 | `*logger.Logger` |
| `Run(addr ...string)` | 启动服务 | `error` |
### Run 方法行为
- **非生产环境**:优先使用传入的地址,否则使用配置文件地址
- **生产环境**:始终使用配置文件地址
```go
// 使用配置文件地址
app.Run()
// 非生产环境使用 :3000生产环境仍用配置文件地址
app.Run(":3000")
```
## 环境变量
| 变量 | 说明 | 默认值 |
|------|------|--------|
| `RUN_ENV` | 运行环境 (`LOCAL`, `DEVELOPMENT`, `PRODUCTION`) | `LOCAL` |
| `CFG_PATH` | 配置文件路径 | `config/config.yml` |
## 日志使用
```go
// 基本用法
app.Logger().Info(ctx, "message")
app.Logger().Error(ctx, "error message")
app.Logger().Warn(ctx, "warning message")
app.Logger().Debug(ctx, "debug message")
// 带键值对
app.Logger().Info(ctx, "user logged in", "user_id", "123", "ip", "192.168.1.1")
```
日志格式会根据环境自动切换:
- **Local**: Console 格式,便于阅读
- **Development/Production**: JSON 格式,便于日志收集
## 上下文用户信息
使用 `web` 包在上下文中传递用户信息:
```go
import "git.hujye.com/infrastructure/go-web-gin/web"
// 设置用户信息
ctx = web.SetUserID(ctx, "user-123")
ctx = web.SetUserName(ctx, "john")
ctx = web.SetTrace(ctx, "trace-456")
ctx = web.SetFromIP(ctx, "192.168.1.1")
// 获取用户信息
userID := web.GetUserID(ctx)
userName := web.GetUserName(ctx)
// 或一次性构建
userInfo := web.NewUserInfo().
WithUserID("user-123").
WithUserName("john").
WithTrace("trace-456").
WithFromIP("192.168.1.1")
ctx = web.ToContext(ctx, userInfo)
```
日志会自动从上下文中提取用户信息并记录。
## 使用 svr 包直接操作 Gin
如果需要直接使用 Gin 引擎单例:
```go
import "git.hujye.com/infrastructure/go-web-gin/svr"
// 获取引擎
engine := svr.GetEngine()
// 快捷方法
svr.Use(middleware...)
svr.GET("/ping", handler)
svr.POST("/users", handler)
svr.PUT("/users/:id", handler)
svr.DELETE("/users/:id", handler)
// 路由组
api := svr.Group("/api/v1")
api.GET("/users", handler)
// 启动服务
svr.Run(":8080")
```
## 配置读取
```go
import "git.hujye.com/infrastructure/go-web-gin/config"
// 获取完整配置
cfg := config.Get()
// 访问配置项
addr := cfg.GetAddr() // host:port
isDebug := cfg.IsDebug() // 是否 debug 模式
isRelease := cfg.IsRelease() // 是否 release 模式
// 动态读取配置(支持嵌套键)
dbHost := config.GetString("mysql.host")
dbPort := config.GetInt("mysql.port")
```
## 项目结构
```
go-web-gin/
├── app.go # 应用核心
├── app_test.go # 测试套件
├── config/
│ └── config.go # 配置管理
├── database/
│ ├── mysql.go # MySQL 单例
│ └── redis.go # Redis 单例
├── env/
│ └── env.go # 环境变量
├── logger/
│ ├── logger.go # 日志核心
│ ├── encoder.go # 编码器
│ └── struct.go # 结构定义
├── server/
│ └── server.go # 服务器封装
├── svr/
│ └── server.go # Gin 单例
└── web/
└── user.go # 上下文用户信息
```
## 运行测试
```bash
# 运行所有测试
go test -v ./...
# 运行特定测试套件
go test -v ./... -run TestAppRunSuite
```
## 依赖
- [gin-gonic/gin](https://github.com/gin-gonic/gin) - Web 框架
- [uber-go/zap](https://github.com/uber-go/zap) - 高性能日志
- [go-gorm/gorm](https://github.com/go-gorm/gorm) - ORM
- [redis/go-redis](https://github.com/redis/go-redis) - Redis 客户端
- [spf13/viper](https://github.com/spf13/viper) - 配置管理
- [stretchr/testify](https://github.com/stretchr/testify) - 测试框架
## License
MIT License
## Author
**hujye**
- Email: hujie@hujye.com

55
app.go
View File

@@ -4,13 +4,20 @@ import (
"log" "log"
"git.hujye.com/infrastructure/go-web-gin/config" "git.hujye.com/infrastructure/go-web-gin/config"
"git.hujye.com/infrastructure/go-web-gin/server" "git.hujye.com/infrastructure/go-web-gin/database"
"git.hujye.com/infrastructure/go-web-gin/logger"
"git.hujye.com/infrastructure/go-web-gin/svr"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
) )
// App represents the application // App represents the application
type App struct { type App struct {
server *server.Server engine *gin.Engine
db *gorm.DB
rdb *redis.Client
logger *logger.Logger
} }
// New creates a new App instance and loads config file // New creates a new App instance and loads config file
@@ -21,21 +28,57 @@ func New() *App {
} }
return &App{ return &App{
server: server.New(), engine: svr.GetEngine(),
logger: logger.GetLogger(),
} }
} }
// UseMiddleware registers global middleware // UseMiddleware registers global middleware
func (a *App) UseMiddleware(middleware ...gin.HandlerFunc) { func (a *App) UseMiddleware(middleware ...gin.HandlerFunc) {
a.server.Engine().Use(middleware...) a.engine.Use(middleware...)
} }
// RegisterRoutes registers routes with the given handler function // RegisterRoutes registers routes with the given handler function
func (a *App) RegisterRoutes(registerFunc func(*gin.Engine)) { func (a *App) RegisterRoutes(registerFunc func(*gin.Engine)) {
registerFunc(a.server.Engine()) registerFunc(a.engine)
}
// InitMySQL initializes the MySQL database connection
func (a *App) InitMySQL() *gorm.DB {
a.db = database.GetDB()
return a.db
}
// DB returns the gorm.DB instance
func (a *App) DB() *gorm.DB {
return a.db
}
// InitRedis initializes the Redis connection
func (a *App) InitRedis() *redis.Client {
a.rdb = database.GetRedis()
return a.rdb
}
// Redis returns the redis.Client instance
func (a *App) Redis() *redis.Client {
return a.rdb
}
// Logger returns the logger.Logger instance
func (a *App) Logger() *logger.Logger {
return a.logger
} }
// Run starts the application server // Run starts the application server
func (a *App) Run(addr ...string) error { func (a *App) Run(addr ...string) error {
return a.server.Run(addr...) listenAddr := config.Get().GetAddr()
// Non-production: use provided address if given, otherwise use config
if !config.Get().IsRelease() && len(addr) > 0 && addr[0] != "" {
listenAddr = addr[0]
}
a.logger.Info(nil, "Server starting", "addr", listenAddr, "mode", config.Get().Server.Mode)
return a.engine.Run(listenAddr)
} }

356
app_test.go Normal file
View File

@@ -0,0 +1,356 @@
package go_web_gin
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
)
// AppTestSuite is the test suite for App
type AppTestSuite struct {
suite.Suite
app *App
}
// SetupSuite runs once before all tests
func (s *AppTestSuite) SetupSuite() {
gin.SetMode(gin.TestMode)
s.app = New()
}
// TearDownSuite runs once after all tests
func (s *AppTestSuite) TearDownSuite() {
// Cleanup if needed
}
// SetupTest runs before each test
func (s *AppTestSuite) SetupTest() {
// Reset state if needed
}
// TearDownTest runs after each test
func (s *AppTestSuite) TearDownTest() {
// Cleanup after each test if needed
}
// TestNew tests the New function
func (s *AppTestSuite) TestNew() {
app := New()
s.NotNil(app, "New should return a non-nil App")
s.NotNil(app.engine, "engine should be initialized")
s.NotNil(app.logger, "logger should be initialized")
}
// TestNewSingleton tests that svr.GetEngine returns the same instance
func (s *AppTestSuite) TestNewSingleton() {
app1 := New()
app2 := New()
s.Equal(app1.engine, app2.engine, "engine should be singleton")
}
// TestUseMiddleware tests the UseMiddleware method
func (s *AppTestSuite) TestUseMiddleware() {
middlewareCalled := false
middleware := func(c *gin.Context) {
middlewareCalled = true
c.Next()
}
s.app.UseMiddleware(middleware)
// Create a test route to verify middleware
s.app.engine.GET("/test-middleware", func(c *gin.Context) {
c.Status(http.StatusOK)
})
req := httptest.NewRequest("GET", "/test-middleware", nil)
w := httptest.NewRecorder()
s.app.engine.ServeHTTP(w, req)
s.True(middlewareCalled, "middleware should be called")
}
// TestRegisterRoutes tests the RegisterRoutes method
func (s *AppTestSuite) TestRegisterRoutes() {
app := New()
app.RegisterRoutes(func(e *gin.Engine) {
e.GET("/test-route", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "ok"})
})
})
req := httptest.NewRequest("GET", "/test-route", nil)
w := httptest.NewRecorder()
app.engine.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
s.Contains(w.Body.String(), "ok")
}
// TestRegisterRoutesMultiple tests registering multiple routes
func (s *AppTestSuite) TestRegisterRoutesMultiple() {
app := New()
app.RegisterRoutes(func(e *gin.Engine) {
e.GET("/route1", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"route": 1})
})
e.POST("/route2", func(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{"route": 2})
})
e.PUT("/route3", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"route": 3})
})
})
// Test GET
req1 := httptest.NewRequest("GET", "/route1", nil)
w1 := httptest.NewRecorder()
app.engine.ServeHTTP(w1, req1)
s.Equal(http.StatusOK, w1.Code)
// Test POST
req2 := httptest.NewRequest("POST", "/route2", nil)
w2 := httptest.NewRecorder()
app.engine.ServeHTTP(w2, req2)
s.Equal(http.StatusCreated, w2.Code)
// Test PUT
req3 := httptest.NewRequest("PUT", "/route3", nil)
w3 := httptest.NewRecorder()
app.engine.ServeHTTP(w3, req3)
s.Equal(http.StatusOK, w3.Code)
}
// TestDB tests the DB method returns nil when not initialized
func (s *AppTestSuite) TestDB() {
app := New()
s.Nil(app.DB(), "DB should be nil when not initialized")
}
// TestRedis tests the Redis method returns nil when not initialized
func (s *AppTestSuite) TestRedis() {
app := New()
s.Nil(app.Redis(), "Redis should be nil when not initialized")
}
// TestLogger tests the Logger method
func (s *AppTestSuite) TestLogger() {
app := New()
logger := app.Logger()
s.NotNil(logger, "Logger should not be nil")
}
// TestLoggerNotNil tests that logger is always initialized
func (s *AppTestSuite) TestLoggerNotNil() {
s.NotNil(s.app.Logger(), "Logger should always be initialized in New()")
}
// TestLoggerMethods tests that logger methods work
func (s *AppTestSuite) TestLoggerMethods() {
s.NotPanics(func() {
s.app.Logger().Info(nil, "Test info message")
s.app.Logger().Debug(nil, "Test debug message")
s.app.Logger().Warn(nil, "Test warn message")
}, "Logger methods should not panic")
}
// TestLoggerWithContext tests logger with context
func (s *AppTestSuite) TestLoggerWithContext() {
ctx := context.Background()
s.NotPanics(func() {
s.app.Logger().Info(ctx, "Test with context")
}, "Logger should work with context")
}
// TestRunInvalidAddress tests Run with invalid address
func (s *AppTestSuite) TestRunInvalidAddress() {
app := New()
app.RegisterRoutes(func(e *gin.Engine) {
e.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"pong": true})
})
})
// Use a channel to handle the async nature of Run
errCh := make(chan error, 1)
go func() {
errCh <- app.Run("invalid:address:format")
}()
select {
case err := <-errCh:
s.Error(err, "Run should return error for invalid address")
case <-time.After(2 * time.Second):
// Server might start, which is fine for this test
}
}
// TestHTTPMethod tests various HTTP methods
func (s *AppTestSuite) TestHTTPMethod() {
app := New()
app.RegisterRoutes(func(e *gin.Engine) {
e.DELETE("/delete", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"deleted": true})
})
e.PATCH("/patch", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"patched": true})
})
e.OPTIONS("/options", func(c *gin.Context) {
c.Status(http.StatusNoContent)
})
e.HEAD("/head", func(c *gin.Context) {
c.Status(http.StatusOK)
})
})
// Test DELETE
req := httptest.NewRequest("DELETE", "/delete", nil)
w := httptest.NewRecorder()
app.engine.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
// Test PATCH
req2 := httptest.NewRequest("PATCH", "/patch", nil)
w2 := httptest.NewRecorder()
app.engine.ServeHTTP(w2, req2)
s.Equal(http.StatusOK, w2.Code)
// Test OPTIONS
req3 := httptest.NewRequest("OPTIONS", "/options", nil)
w3 := httptest.NewRecorder()
app.engine.ServeHTTP(w3, req3)
s.Equal(http.StatusNoContent, w3.Code)
// Test HEAD
req4 := httptest.NewRequest("HEAD", "/head", nil)
w4 := httptest.NewRecorder()
app.engine.ServeHTTP(w4, req4)
s.Equal(http.StatusOK, w4.Code)
}
// TestRouteGroup tests route groups
func (s *AppTestSuite) TestRouteGroup() {
app := New()
app.RegisterRoutes(func(e *gin.Engine) {
api := e.Group("/api")
{
api.GET("/users", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"users": []string{}})
})
api.GET("/posts", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"posts": []string{}})
})
}
})
req1 := httptest.NewRequest("GET", "/api/users", nil)
w1 := httptest.NewRecorder()
app.engine.ServeHTTP(w1, req1)
s.Equal(http.StatusOK, w1.Code)
req2 := httptest.NewRequest("GET", "/api/posts", nil)
w2 := httptest.NewRecorder()
app.engine.ServeHTTP(w2, req2)
s.Equal(http.StatusOK, w2.Code)
}
// TestMiddlewareChain tests middleware chain
func (s *AppTestSuite) TestMiddlewareChain() {
app := New()
order := []string{}
app.UseMiddleware(func(c *gin.Context) {
order = append(order, "middleware1-before")
c.Next()
order = append(order, "middleware1-after")
})
app.UseMiddleware(func(c *gin.Context) {
order = append(order, "middleware2-before")
c.Next()
order = append(order, "middleware2-after")
})
app.RegisterRoutes(func(e *gin.Engine) {
e.GET("/chain", func(c *gin.Context) {
order = append(order, "handler")
c.Status(http.StatusOK)
})
})
req := httptest.NewRequest("GET", "/chain", nil)
w := httptest.NewRecorder()
app.engine.ServeHTTP(w, req)
expected := []string{
"middleware1-before",
"middleware2-before",
"handler",
"middleware2-after",
"middleware1-after",
}
s.Equal(expected, order, "middleware should execute in correct order")
}
// TestQueryParam tests query parameters
func (s *AppTestSuite) TestQueryParam() {
app := New()
app.RegisterRoutes(func(e *gin.Engine) {
e.GET("/search", func(c *gin.Context) {
q := c.Query("q")
c.JSON(http.StatusOK, gin.H{"query": q})
})
})
req := httptest.NewRequest("GET", "/search?q=test", nil)
w := httptest.NewRecorder()
app.engine.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
s.Contains(w.Body.String(), "test")
}
// TestPathParam tests path parameters
func (s *AppTestSuite) TestPathParam() {
app := New()
app.RegisterRoutes(func(e *gin.Engine) {
e.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"user_id": id})
})
})
req := httptest.NewRequest("GET", "/users/123", nil)
w := httptest.NewRecorder()
app.engine.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
s.Contains(w.Body.String(), "123")
}
// TestNotFound tests 404 handling
func (s *AppTestSuite) TestNotFound() {
app := New()
req := httptest.NewRequest("GET", "/nonexistent", nil)
w := httptest.NewRecorder()
app.engine.ServeHTTP(w, req)
s.Equal(http.StatusNotFound, w.Code)
}
// TestAppRunSuite runs the test suite
func TestAppRunSuite(t *testing.T) {
suite.Run(t, new(AppTestSuite))
}

View File

@@ -18,6 +18,8 @@ type Config struct {
Server ServerConfig `mapstructure:"server"` Server ServerConfig `mapstructure:"server"`
App AppConfig `mapstructure:"app"` App AppConfig `mapstructure:"app"`
Log LogConfig `mapstructure:"log"` Log LogConfig `mapstructure:"log"`
MySQL MySQLConfig `mapstructure:"mysql"`
Redis RedisConfig `mapstructure:"redis"`
} }
// ServerConfig represents server configuration // ServerConfig represents server configuration
@@ -44,6 +46,33 @@ type LogConfig struct {
Compress bool `mapstructure:"compress"` Compress bool `mapstructure:"compress"`
} }
// MySQLConfig represents mysql configuration
type MySQLConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"db_name"`
Charset string `mapstructure:"charset"`
ParseTime bool `mapstructure:"parse_time"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
MaxOpenConns int `mapstructure:"max_open_conns"`
ConnMaxLifetime int `mapstructure:"conn_max_lifetime"` // seconds
}
// RedisConfig represents redis configuration
type RedisConfig struct {
Addr string `mapstructure:"addr"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
PoolSize int `mapstructure:"pool_size"`
MinIdleConns int `mapstructure:"min_idle_conns"`
MaxRetries int `mapstructure:"max_retries"`
DialTimeout int `mapstructure:"dial_timeout"` // seconds
ReadTimeout int `mapstructure:"read_timeout"` // seconds
WriteTimeout int `mapstructure:"write_timeout"` // seconds
}
// Load loads configuration from environment variable CFG_PATH // Load loads configuration from environment variable CFG_PATH
// Default path is config/config.yml // Default path is config/config.yml
func Load() (*Config, error) { func Load() (*Config, error) {
@@ -94,6 +123,29 @@ func Get() *Config {
MaxAge: 28, MaxAge: 28,
Compress: true, Compress: true,
}, },
MySQL: MySQLConfig{
Host: "127.0.0.1",
Port: 3306,
User: "root",
Password: "",
DBName: "test",
Charset: "utf8mb4",
ParseTime: true,
MaxIdleConns: 10,
MaxOpenConns: 100,
ConnMaxLifetime: 3600,
},
Redis: RedisConfig{
Addr: "127.0.0.1:6379",
Password: "",
DB: 0,
PoolSize: 10,
MinIdleConns: 5,
MaxRetries: 3,
DialTimeout: 5,
ReadTimeout: 3,
WriteTimeout: 3,
},
} }
} }
return globalConfig return globalConfig

137
database/mysql.go Normal file
View File

@@ -0,0 +1,137 @@
package database
import (
"context"
"fmt"
"sync"
"time"
"git.hujye.com/infrastructure/go-web-gin/config"
"git.hujye.com/infrastructure/go-web-gin/logger"
"gorm.io/driver/mysql"
"gorm.io/gorm"
gormlogger "gorm.io/gorm/logger"
)
var (
db *gorm.DB
once sync.Once
)
// gormLogger implements gorm/logger.Interface using project's zap logger
type gormLogger struct {
logLevel gormlogger.LogLevel
}
// newGormLogger creates a new gorm logger
func newGormLogger(level gormlogger.LogLevel) *gormLogger {
return &gormLogger{logLevel: level}
}
// LogMode sets log level
func (l *gormLogger) LogMode(level gormlogger.LogLevel) gormlogger.Interface {
newLogger := *l
newLogger.logLevel = level
return &newLogger
}
// Info logs info messages
func (l *gormLogger) Info(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= gormlogger.Info {
logger.GetLogger().Info(ctx, fmt.Sprintf("gorm: %s", fmt.Sprintf(msg, data...)))
}
}
// Warn logs warn messages
func (l *gormLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= gormlogger.Warn {
logger.GetLogger().Warn(ctx, fmt.Sprintf("gorm: %s", fmt.Sprintf(msg, data...)))
}
}
// Error logs error messages
func (l *gormLogger) Error(ctx context.Context, msg string, data ...interface{}) {
if l.logLevel >= gormlogger.Error {
logger.GetLogger().Error(ctx, fmt.Sprintf("gorm: %s", fmt.Sprintf(msg, data...)))
}
}
// Trace logs sql query with execution time
func (l *gormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
if l.logLevel <= gormlogger.Silent {
return
}
elapsed := time.Since(begin)
sql, rows := fc()
switch {
case err != nil && l.logLevel >= gormlogger.Error:
logger.GetLogger().Error(ctx, "gorm query error", "duration", elapsed.Milliseconds(), "sql", sql, "rows", rows, "error", err.Error())
case elapsed > 200*time.Millisecond && l.logLevel >= gormlogger.Warn:
logger.GetLogger().Warn(ctx, "gorm slow query", "duration", elapsed.Milliseconds(), "sql", sql, "rows", rows)
case l.logLevel >= gormlogger.Info:
logger.GetLogger().Debug(ctx, "gorm query", "duration", elapsed.Milliseconds(), "sql", sql, "rows", rows)
}
}
// GetDB returns the gorm.DB singleton instance
// It will initialize the connection on first call
func GetDB() *gorm.DB {
once.Do(func() {
db = initDB()
})
return db
}
// initDB initializes and returns a new gorm.DB connection
func initDB() *gorm.DB {
cfg := config.Get().MySQL
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=%t&loc=Local",
cfg.User,
cfg.Password,
cfg.Host,
cfg.Port,
cfg.DBName,
cfg.Charset,
cfg.ParseTime,
)
var logLevel gormlogger.LogLevel
if config.Get().IsDebug() {
logLevel = gormlogger.Info
} else {
logLevel = gormlogger.Silent
}
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newGormLogger(logLevel),
})
if err != nil {
panic(fmt.Sprintf("failed to connect to database: %v", err))
}
sqlDB, err := db.DB()
if err != nil {
panic(fmt.Sprintf("failed to get database instance: %v", err))
}
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second)
return db
}
// Close closes the database connection
func Close() error {
if db != nil {
sqlDB, err := db.DB()
if err != nil {
return err
}
return sqlDB.Close()
}
return nil
}

69
database/redis.go Normal file
View File

@@ -0,0 +1,69 @@
package database
import (
"context"
"fmt"
"sync"
"time"
"git.hujye.com/infrastructure/go-web-gin/config"
"git.hujye.com/infrastructure/go-web-gin/logger"
"github.com/redis/go-redis/v9"
)
var (
rdb *redis.Client
redisOnce sync.Once
)
// GetRedis returns the redis.Client singleton instance.
// It will initialize the connection on first call
func GetRedis() *redis.Client {
redisOnce.Do(func() {
rdb = initRedis()
})
return rdb
}
// initRedis initializes and returns a new redis.Client connection.
// It will panic if connection fails
func initRedis() *redis.Client {
cfg := config.Get().Redis
rdb := redis.NewClient(&redis.Options{
Addr: cfg.Addr,
Password: cfg.Password,
DB: cfg.DB,
PoolSize: cfg.PoolSize,
MinIdleConns: cfg.MinIdleConns,
MaxRetries: cfg.MaxRetries,
DialTimeout: time.Duration(cfg.DialTimeout) * time.Second,
ReadTimeout: time.Duration(cfg.ReadTimeout) * time.Second,
WriteTimeout: time.Duration(cfg.WriteTimeout) * time.Second,
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := rdb.Ping(ctx).Err(); err != nil {
logger.GetLogger().Error(nil, "redis connect failed", "addr", cfg.Addr, "error", err.Error())
panic(fmt.Sprintf("failed to connect to redis: %v", err))
}
logger.GetLogger().Info(nil, "redis connected successfully", "addr", cfg.Addr, "db", cfg.DB)
return rdb
}
// CloseRedis closes the redis connection.
// Returns error if close fails, nil otherwise
func CloseRedis() error {
if rdb != nil {
if err := rdb.Close(); err != nil {
logger.GetLogger().Error(nil, "redis close failed", "error", err.Error())
return err
}
logger.GetLogger().Info(nil, "redis connection closed")
}
return nil
}

12
go.mod
View File

@@ -4,26 +4,35 @@ go 1.24.0
require ( require (
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/redis/go-redis/v9 v9.18.0
github.com/spf13/viper v1.18.2 github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.1
) )
require ( require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/iasm v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
@@ -43,13 +52,14 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.25.0 // indirect golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect golang.org/x/text v0.34.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

View File

@@ -2,7 +2,6 @@ package logger
import ( import (
"context" "context"
"fmt"
"os" "os"
"strings" "strings"
"sync" "sync"
@@ -125,53 +124,68 @@ func getUserFields(ctx context.Context) []zap.Field {
} }
// Info logs an info message // Info logs an info message
func (l *Logger) Info(ctx context.Context, msg string, values ...interface{}) { func (l *Logger) Info(ctx context.Context, msg string, keyValues ...interface{}) {
allFields := getUserFields(ctx) allFields := getUserFields(ctx)
formattedMsg := fmt.Sprintf(msg, values...) for i := 0; i < len(keyValues); i += 2 {
if len(values) > 0 { if i+1 < len(keyValues) {
allFields = append(allFields, zap.String("msg", formattedMsg)) if key, ok := keyValues[i].(string); ok {
allFields = append(allFields, zap.Any(key, keyValues[i+1]))
}
}
} }
l.logger.Info(formattedMsg, allFields...) l.logger.Info(msg, allFields...)
} }
// Error logs an error message // Error logs an error message
func (l *Logger) Error(ctx context.Context, msg string, values ...interface{}) { func (l *Logger) Error(ctx context.Context, msg string, keyValues ...interface{}) {
allFields := getUserFields(ctx) allFields := getUserFields(ctx)
formattedMsg := fmt.Sprintf(msg, values...) for i := 0; i < len(keyValues); i += 2 {
if len(values) > 0 { if i+1 < len(keyValues) {
allFields = append(allFields, zap.String("msg", formattedMsg)) if key, ok := keyValues[i].(string); ok {
allFields = append(allFields, zap.Any(key, keyValues[i+1]))
}
}
} }
l.logger.Error(formattedMsg, allFields...) l.logger.Error(msg, allFields...)
} }
// Debug logs a debug message // Debug logs a debug message
func (l *Logger) Debug(ctx context.Context, msg string, values ...interface{}) { func (l *Logger) Debug(ctx context.Context, msg string, keyValues ...interface{}) {
allFields := getUserFields(ctx) allFields := getUserFields(ctx)
formattedMsg := fmt.Sprintf(msg, values...) for i := 0; i < len(keyValues); i += 2 {
if len(values) > 0 { if i+1 < len(keyValues) {
allFields = append(allFields, zap.String("msg", formattedMsg)) if key, ok := keyValues[i].(string); ok {
allFields = append(allFields, zap.Any(key, keyValues[i+1]))
}
}
} }
l.logger.Debug(formattedMsg, allFields...) l.logger.Debug(msg, allFields...)
} }
// Warn logs a warning message // Warn logs a warning message
func (l *Logger) Warn(ctx context.Context, msg string, values ...interface{}) { func (l *Logger) Warn(ctx context.Context, msg string, keyValues ...interface{}) {
allFields := getUserFields(ctx) allFields := getUserFields(ctx)
formattedMsg := fmt.Sprintf(msg, values...) for i := 0; i < len(keyValues); i += 2 {
if len(values) > 0 { if i+1 < len(keyValues) {
allFields = append(allFields, zap.String("msg", formattedMsg)) if key, ok := keyValues[i].(string); ok {
allFields = append(allFields, zap.Any(key, keyValues[i+1]))
}
}
} }
l.logger.Warn(formattedMsg, allFields...) l.logger.Warn(msg, allFields...)
} }
// Fatal logs a fatal message and exits // Fatal logs a fatal message and exits
func (l *Logger) Fatal(ctx context.Context, msg string, values ...interface{}) { func (l *Logger) Fatal(ctx context.Context, msg string, keyValues ...interface{}) {
allFields := getUserFields(ctx) allFields := getUserFields(ctx)
formattedMsg := fmt.Sprintf(msg, values...) for i := 0; i < len(keyValues); i += 2 {
if len(values) > 0 { if i+1 < len(keyValues) {
allFields = append(allFields, zap.String("msg", formattedMsg)) if key, ok := keyValues[i].(string); ok {
allFields = append(allFields, zap.Any(key, keyValues[i+1]))
}
}
} }
l.logger.Fatal(formattedMsg, allFields...) l.logger.Fatal(msg, allFields...)
} }
// Sync flushes any buffered log entries // Sync flushes any buffered log entries

56
svr/server.go Normal file
View File

@@ -0,0 +1,56 @@
package svr
import (
"sync"
"github.com/gin-gonic/gin"
)
var (
engine *gin.Engine
once sync.Once
)
// GetEngine returns the gin.Engine singleton instance.
// It will initialize the engine on first call
func GetEngine() *gin.Engine {
once.Do(func() {
engine = gin.New()
})
return engine
}
// Use registers global middleware
func Use(middleware ...gin.HandlerFunc) {
GetEngine().Use(middleware...)
}
// Group creates a new router group
func Group(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup {
return GetEngine().Group(relativePath, handlers...)
}
// GET is a shortcut for router.Handle("GET", path, handlers)
func GET(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return GetEngine().GET(relativePath, handlers...)
}
// POST is a shortcut for router.Handle("POST", path, handlers)
func POST(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return GetEngine().POST(relativePath, handlers...)
}
// PUT is a shortcut for router.Handle("PUT", path, handlers)
func PUT(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return GetEngine().PUT(relativePath, handlers...)
}
// DELETE is a shortcut for router.Handle("DELETE", path, handlers)
func DELETE(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return GetEngine().DELETE(relativePath, handlers...)
}
// Run starts the server
func Run(addr ...string) error {
return GetEngine().Run(addr...)
}