feat: 添加服务和数据库初始化
This commit is contained in:
321
README.md
Normal file
321
README.md
Normal 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
55
app.go
@@ -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
356
app_test.go
Normal 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))
|
||||||
|
}
|
||||||
@@ -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
137
database/mysql.go
Normal 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
69
database/redis.go
Normal 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
12
go.mod
@@ -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
|
||||||
|
|||||||
@@ -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
56
svr/server.go
Normal 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...)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user