feat: initial commit - StreamBox Go+Gin framework

This commit is contained in:
noelorin 2026-08-25 12:12:18 +08:00
commit c400394ed6
31 changed files with 2388 additions and 0 deletions

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
PORT=8080
GIN_MODE=debug

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
bin/
vendor/
.env
*.log
.DS_Store
.idea/
.vscode/
tmp/

19
Makefile Normal file
View File

@ -0,0 +1,19 @@
.PHONY: run build tidy fmt lint clean
APP_NAME=streambox
MAIN=./cmd/server
run:
go run $(MAIN)
build:
go build -o bin/$(APP_NAME) $(MAIN)
tidy:
go mod tidy
fmt:
go fmt ./...
clean:
rm -rf bin/

48
README.md Normal file
View File

@ -0,0 +1,48 @@
# StreamBox
Go + Gin 项目框架
## 目录结构
```
.
├── cmd/server # 程序入口
├── config # 配置文件
├── internal
│ ├── handler # HTTP 处理层
│ ├── service # 业务逻辑层
│ ├── model # 数据模型
│ ├── middleware # 中间件
│ └── router # 路由注册
├── pkg
│ ├── logger # Zap 日志封装
│ └── response # 统一响应封装
├── api # API 文档
└── docs # 项目文档
```
## 快速开始
```bash
# 安装依赖
make tidy
# 启动服务
make run
# 或
# go run ./cmd/server
# 打包
make build
./bin/streambox
```
## 配置
- `config/config.yaml` 主配置
- 环境变量优先于配置文件,通过 `viper.AutomaticEnv` 覆盖
## 路由
- `GET /health` 健康检查
- `GET /api/v1/ping` 示例接口

61
cmd/server/main.go Normal file
View File

@ -0,0 +1,61 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"streambox/config"
"streambox/internal/router"
"streambox/pkg/logger"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatalf("load config failed: %v", err)
}
if err := logger.Init(cfg.Log.Level, cfg.Log.Encoding); err != nil {
log.Fatalf("init logger failed: %v", err)
}
defer logger.Sync()
r := router.New(cfg)
addr := fmt.Sprintf(":%d", cfg.Server.Port)
srv := &http.Server{
Addr: addr,
Handler: r,
}
// 可选超时
if d, err := time.ParseDuration(cfg.Server.ReadTimeout); err == nil {
srv.ReadTimeout = d
}
if d, err := time.ParseDuration(cfg.Server.WriteTimeout); err == nil {
srv.WriteTimeout = d
}
go func() {
logger.Sugar.Infow("server starting", "addr", addr, "mode", cfg.Server.Mode)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen failed: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Sugar.Info("shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
logger.Sugar.Errorf("server shutdown failed: %v", err)
}
logger.Sugar.Info("server exited")
}

65
config/config.go Normal file
View File

@ -0,0 +1,65 @@
package config
import (
"strings"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig `mapstructure:"server"`
Log LogConfig `mapstructure:"log"`
Cors CorsConfig `mapstructure:"cors"`
}
type ServerConfig struct {
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"`
ReadTimeout string `mapstructure:"read_timeout"`
WriteTimeout string `mapstructure:"write_timeout"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
Encoding string `mapstructure:"encoding"`
}
type CorsConfig struct {
AllowOrigins []string `mapstructure:"allow_origins"`
AllowMethods []string `mapstructure:"allow_methods"`
AllowHeaders []string `mapstructure:"allow_headers"`
}
var C *Config
func Load() (*Config, error) {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("./config")
viper.AddConfigPath(".")
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.mode", "debug")
viper.SetDefault("log.level", "debug")
viper.SetDefault("log.encoding", "console")
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// 允许 PORT 等环境变量直接覆盖
_ = viper.BindEnv("server.port", "PORT")
if err := viper.ReadInConfig(); err != nil {
// 配置文件不存在时使用默认值+环境变量,不视为致命错误
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, err
}
}
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, err
}
C = &cfg
return &cfg, nil
}

14
config/config.yaml Normal file
View File

@ -0,0 +1,14 @@
server:
port: 8080
mode: debug # debug / release
read_timeout: 10s
write_timeout: 10s
log:
level: debug # debug / info / warn / error
encoding: console # console / json
cors:
allow_origins: ["*"]
allow_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allow_headers: ["Origin", "Content-Type", "Authorization"]

View File

@ -0,0 +1,323 @@
# 第 1 章 Go 环境与基础语法
> 学完本章你将:装好 Go、理解 go mod、掌握变量/类型/流程/函数/结构体/接口/错误处理,能读懂本项目 90% 的非并发代码。
## 1.1 安装与环境
### 安装
- 官网 https://go.dev/dl 下载对应系统安装包
- 验证:`go version` 应输出 `go1.26.x`
- 本项目要求 `go 1.26.2`(见 `go.mod` 第一行)
### 环境变量
```bash
go env GOPATH GOROOT GOPROXY
# GOPATH Workspace 目录(Go 1.11 后已弱化,保留兼容)
# GOROOT Go 安装目录
# GOPROXY 代理,国内建议:
go env -w GOPROXY=https://goproxy.cn,direct
go env -w GOPRIVATE=""
```
### 编辑器
推荐 VS Code + Go 插件(gopls),或 GoLand。安装后执行 `go install golang.org/x/tools/gopls@latest`。
## 1.2 go mod 模块管理
Go 1.11+ 用 `go.mod` 管理依赖,替代 GOPATH。本项目 `go.mod` 开头:
```go
module streambox
go 1.26.2
```
常用命令(对应本项目 `Makefile`):
```bash
go mod init streambox # 初始化(已存在无需重复)
go mod tidy # 整理依赖,对应 make tidy
go get github.com/gin-gonic/gin@latest # 添加/升级依赖
go list -m all # 查看所有依赖
go mod why github.com/gin-gonic/gin # 查询谁依赖它
```
`go.sum` 是依赖校验和,提交到 Git,不要手改。
## 1.3 Hello World
创建 `main.go`:
```go
package main
import "fmt"
func main() {
fmt.Println("hello, streambox")
}
```
运行 `go run main.go`。`package main` + `func main()` 是可执行程序入口。
## 1.4 变量与常量
```go
// 显式声明
var name string = "streambox"
var port int = 8080
var debug bool
// 类型推导
var addr = ":8080"
// 短声明(函数内常用)
count := 42
msg := "ok"
// 批量声明
var (
host = "0.0.0.0"
timeout = 10
)
// 常量
const AppName = "streambox"
const (
ModeDebug = "debug"
ModeRelease = "release"
)
// iota 枚举
const (
CodeSuccess = iota // 0
CodeBadRequest // 1
CodeInternal // 2
)
```
零值:`int` 为 0,`string` 为 "",`bool` 为 false,指针/slice/map/channel/interface 为 nil。
## 1.5 基础类型
| 类型 | 说明 | 示例 |
|------|------|------|
| `bool` | 布尔 | `true/false` |
| `int/int8/int16/int32/int64` | 有符号整型 | `int` 随平台 32/64 位 |
| `uint/...` | 无符号整型 | `uint port = 8080` |
| `float32/float64` | 浮点 | `float64` 常用 |
| `string` | 字符串,不可变,UTF-8 | `"hello"` |
| `byte` (alias uint8) | 字节 | `'a'` |
| `rune` (alias int32) | Unicode 码点 | `'中'` |
字符串操作:
```go
s := "hello"
len(s) // 字节长度 5
s[1] // byte 'e'
for _, r := range s { // 按 rune 遍历
fmt.Printf("%c", r)
}
s + " world" // 拼接
fmt.Sprintf("port=%d", 8080)
```
## 1.6 复合类型:数组、切片、Map
```go
// 数组(定长,很少直接用)
var a [3]int = [3]int{1,2,3}
// 切片(动态数组,最常用)
s := []int{1, 2, 3}
s = append(s, 4)
sub := s[1:3] // [2,3] 左闭右开
s2 := make([]int, 0, 8) // len 0 cap 8
// Map
m := map[string]int{"a": 1}
m["b"] = 2
v, ok := m["c"] // ok 判断是否存在
delete(m, "a")
for k, v := range m { fmt.Println(k, v) }
m2 := make(map[string]string)
```
注意:slice/map 作为参数传递时是引用语义;nil slice 可 append,nil map 不可写入。
## 1.7 流程控制
```go
// if
if err := do(); err != nil {
log.Fatal(err)
} else {
fmt.Println("ok")
}
// for 只有 for
for i := 0; i < 3; i++ {}
for range s {}
for { break } // 死循环
// switch
switch mode {
case "debug":
fmt.Println("debug")
case "release":
fmt.Println("release")
default:
fmt.Println("unknown")
}
// switch 带表达式
switch {
case port < 1024:
fmt.Println("privileged")
default:
fmt.Println("ok")
}
// defer 延迟执行(LIFO),常用于资源释放
f, _ := os.Open("config.yaml")
defer f.Close()
defer logger.Sync() // 本项目 main.go 中就有
```
## 1.8 函数
```go
// 普通函数
func Add(a, b int) int { return a + b }
// 多返回值(Go 错误处理的核心)
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("divide by zero")
}
return a / b, nil
}
// 命名返回值
func Split(s string) (first, second string) {
// ...
return
}
// 可变参数
func Sum(nums ...int) int {
n := 0
for _, v := range nums { n += v }
return n
}
// 函数作为值
fn := func(s string) string { return s + "!" }
type HandlerFunc func(*gin.Context) // Gin 中大量使用
```
## 1.9 结构体与方法
```go
// 定义
type ServerConfig struct {
Port int `yaml:"port" mapstructure:"port"`
Mode string `yaml:"mode"`
ReadTimeout string `yaml:"read_timeout"`
WriteTimeout string `yaml:"write_timeout"`
}
// 方法(接收者为值或指针)
func (c ServerConfig) Addr() string {
return fmt.Sprintf(":%d", c.Port)
}
func (c *ServerConfig) SetPort(p int) { c.Port = p }
// 初始化
cfg := ServerConfig{Port: 8080, Mode: "debug"}
cfg2 := &ServerConfig{Port: 8080} // 指针
// 嵌入(组合,非继承)
type Config struct {
Server ServerConfig `mapstructure:"server"`
Log LogConfig
}
```
本项目 `config/config.go` 就是典型的结构体 + viper 绑定。
## 1.10 接口
接口是 Go 多态的核心,隐式实现:
```go
type Pinger interface {
Ping() string
}
// 只要实现了 Ping() string,就实现了 Pinger,无需显式声明
type PingService struct{}
func (s *PingService) Ping() string { return "pong" }
var _ Pinger = (*PingService)(nil) // 编译期校验
// 空接口 interface{}(Go 1.18 后别名 any)可表示任意类型
var x any = 42
x = "hello"
// 类型断言
if s, ok := x.(string); ok { fmt.Println(s) }
// type switch
switch v := x.(type) {
case string: fmt.Println("string", v)
case int: fmt.Println("int", v)
}
```
Gin 的 `gin.HandlerFunc` 本质是 `func(*gin.Context)`,也是一种函数类型接口。
## 1.11 错误处理
Go 无异常,错误即值:
```go
f, err := os.Open("config.yaml")
if err != nil {
return fmt.Errorf("open config: %w", err) // %w 包装,支持 errors.Is/As
}
defer f.Close()
// 自定义错误
type AppError struct {
Code int
Msg string
}
func (e *AppError) Error() string { return e.Msg }
// 判断
if errors.Is(err, os.ErrNotExist) { ... }
var appErr *AppError
if errors.As(err, &appErr) { fmt.Println(appErr.Code) }
// panic/recover 仅用于不可恢复错误,业务错误不要用
// 本项目 middleware/recovery.go 用 recover 捕获 panic,避免进程崩溃
```
## 1.12 包与可见性
- 同目录同 `package` 名
- 大写开头导出(Public),小写私有:`logger.Log` 可导出,`join` 不可(见 `middleware/cors.go`)
- 导入:`import "streambox/pkg/logger"`,`import gin "github.com/gin-gonic/gin"`
## 1.13 动手练习
1. 编写 `fib(n int) int` 递归与迭代两版。
2. 定义 `User` 结构体,实现 `String() string` 方法。
3. 模仿 `pkg/response` 封装 `Result`,编写 `Success`/`Fail` 并用 `encoding/json` 序列化验证。
下一章将学习并发与工程化。

View File

@ -0,0 +1,199 @@
# 第 2 章 Go 进阶:并发与工程化
> 学完本章你将:用 goroutine/channel 写并发、用 context 控制生命周期、用 sync 解决共享状态、会写测试并掌握常用工具链。
## 2.1 goroutine 轻量线程
Go 并发的最小单元是 `goroutine`,由运行时调度,创建成本约几 KB。
```go
// 启动一个 goroutine
go func() {
fmt.Println("in goroutine")
}()
// 本项目 cmd/server/main.go 启动 HTTP 服务就是 goroutine
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen failed: %v", err)
}
}()
```
主 goroutine 结束则程序退出,所以 `main` 中用 `signal.Notify` 阻塞等待退出信号。
## 2.2 channel 通道
channel 是 goroutine 间通信的管道,语义是“不要通过共享内存来通信,通过通信来共享内存”。
```go
// 无缓冲:发送阻塞直到有人接收
ch := make(chan string)
go func() { ch <- "pong" }()
msg := <-ch // 接收
// 有缓冲
ch2 := make(chan int, 2)
ch2 <- 1
ch2 <- 2
// ch2 <- 3 // 阻塞,缓冲满
// 关闭与遍历
close(ch2)
for v := range ch2 { fmt.Println(v) }
// 单向 channel(函数签名约束)
func producer(ch chan<- int) { ch <- 42 } // 只发
func consumer(ch <-chan int) { fmt.Println(<-ch) } // 只收
// select 多路复用
select {
case v := <-ch:
fmt.Println(v)
case <-time.After(2 * time.Second):
fmt.Println("timeout")
}
```
典型模式:用 channel 做信号通知,本项目 `quit := make(chan os.Signal, 1)` 就是 buffered channel 接收系统信号。
## 2.3 context 上下文
`context` 用于传递取消信号、超时和请求级数据,Gin 的 `c.Request.Context()` 即标准 context。
```go
// 超时控制
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 本项目优雅关闭
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)
// 取消
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-ctx.Done()
fmt.Println("canceled:", ctx.Err())
}()
cancel()
// 传值(仅放请求级数据如 traceID,不要放业务大对象)
ctx = context.WithValue(ctx, "userID", 123)
```
规则:context 作为函数第一个参数,命名 `ctx`,不要存入结构体。
## 2.4 sync 同步原语
### Mutex 互斥锁
```go
var mu sync.Mutex
var count int
mu.Lock()
count++
mu.Unlock()
// RWMutex 读写锁
var rw sync.RWMutex
rw.RLock(); v := data; rw.RUnlock()
rw.Lock(); data = newVal; rw.Unlock()
```
### WaitGroup 等待组
```go
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println(n)
}(i)
}
wg.Wait()
```
### Once 单次执行
```go
var once sync.Once
var inst *Service
once.Do(func() { inst = &Service{} })
```
本项目 `logger.Init` 虽未用 Once,但生产中常用来保证日志单例初始化一次。
## 2.5 并发模式示例
```go
// Worker Pool
jobs := make(chan int, 10)
results := make(chan int, 10)
for w := 0; w < 3; w++ {
go func() {
for j := range jobs { results <- j * 2 }
}()
}
for i := 0; i < 5; i++ { jobs <- i }
close(jobs)
```
## 2.6 错误与 panic 边界
- 业务错误用 `error` 返回
- 不可恢复错误才 `panic`,并在最外层 `recover`(见 `middleware/recovery.go`)
- goroutine 内的 panic 不会被外层 recover 捕获,需在 goroutine 内部 recover
## 2.7 测试
```go
// ping_test.go
package service
import "testing"
func TestPing(t *testing.T) {
svc := NewPingService(nil)
if got := svc.Ping(); got != "pong" {
t.Fatalf("want pong got %s", got)
}
}
// 表驱动测试
func TestAdd(t *testing.T) {
cases := []struct{ a, b, want int }{{1,2,3},{0,0,0}}
for _, c := range cases {
if got := Add(c.a, c.b); got != c.want {
t.Errorf("Add(%d,%d)=%d want %d", c.a, c.b, got, c.want)
}
}
}
```
```bash
go test ./...
go test -run TestPing -v ./internal/service
go test -race ./... # 检测数据竞争
go test -cover ./... # 覆盖率
```
## 2.8 工具链
| 命令 | 用途 |
|------|------|
| `go fmt ./...` | 格式化,对应 `make fmt` |
| `go vet ./...` | 静态检查 |
| `golangci-lint run` | 综合 lint(需安装) |
| `go mod tidy` | 整理依赖,对应 `make tidy` |
| `go run ./cmd/server` | 运行,对应 `make run` |
| `go build -o bin/streambox ./cmd/server` | 编译,对应 `make build` |
## 2.9 动手练习
1. 用 goroutine + channel 实现并发求 1..100 的和,对比串行版本。
2. 用 `context.WithTimeout` 实现一个 2 秒超时的 HTTP 请求。
3. 给 `internal/service/ping.go` 补充单测并用 `go test -race` 验证。

108
docs/03-gin-入门.md Normal file
View File

@ -0,0 +1,108 @@
# 第 3 章 Gin 入门
> 学完本章你将:理解 Gin 的定位与优势、跑通第一个 Gin 服务、看懂 Gin 与标准库 net/http 的关系。
## 3.1 为什么选 Gin
- **快**:基于 httprouter 的前缀树路由,性能居 Go Web 框架前列
- **简洁**:`gin.Default()` 一行起服务,`c.JSON` 一行返回
- **生态**:中间件、绑定、验证、分组等开箱即用
- **本项目已选型**:`go.mod` 中 `github.com/gin-gonic/gin v1.12.0`
对比:
| 方案 | 特点 | 适合 |
|------|------|------|
| `net/http` | 标准库,无依赖,可控 | 小服务、学习 |
| Gin | 高性能、API 友好 | RESTful API、中大型项目 |
| Echo/Fiber | 类似 Gin,Fiber 基于 fasthttp | 追求极致性能或特定风格 |
## 3.2 安装与初始化
本项目已初始化,可直接 `make tidy`。从零新建:
```bash
mkdir myapp && cd myapp
go mod init myapp
go get github.com/gin-gonic/gin
```
## 3.3 Hello World
```go
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default() // 含 Logger + Recovery 中间件
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
r.Run(":8080") // 监听 0.0.0.0:8080
}
```
运行 `go run main.go`,访问 `http://localhost:8080/ping`。
- `gin.H` 是 `map[string]any` 的别名,方便构造 JSON
- `c.JSON(code, obj)` 自动设置 `Content-Type: application/json`
## 3.4 Gin 与 net/http 的关系
Gin 不是另起炉灶,而是对 `net/http` 的封装:
```go
// Gin 实现了 http.Handler 接口,所以可直接给 http.Server
srv := &http.Server{
Addr: ":8080",
Handler: r, // r 是 *gin.Engine,实现了 ServeHTTP
}
srv.ListenAndServe()
```
本项目 `cmd/server/main.go` 正是这种写法,以便支持优雅关闭(`srv.Shutdown`)。若用 `r.Run()` 则无法优雅关闭,仅适合 demo。
`gin.Context` 包装了 `http.Request` 与 `http.ResponseWriter`,提供 `Param`/`Query`/`Bind`/`JSON` 等快捷方法。
## 3.5 gin.Default() vs gin.New()
```go
// Default 自带 Logger + Recovery
r := gin.Default()
// New 空引擎,需手动注册(本项目采用,便于替换为 Zap)
r := gin.New()
r.Use(middleware.Recovery())
r.Use(middleware.ZapLogger())
```
本项目 `internal/router/router.go` 使用 `gin.New()` 并注册自定义 Zap 日志与 Recovery,生产更可控。
## 3.6 运行模式
```go
gin.SetMode(gin.DebugMode) // 开发:详细日志
gin.SetMode(gin.ReleaseMode) // 生产:精简日志
gin.SetMode(gin.TestMode) // 测试
```
本项目由 `config.yaml` 的 `server.mode` 控制,在 `router.New` 中 `gin.SetMode(cfg.Server.Mode)`。
## 3.7 项目初始化对照
本项目入口 `cmd/server/main.go` 关键步骤:
1. `config.Load()` 加载配置
2. `logger.Init` 初始化 Zap
3. `router.New(cfg)` 创建 Gin 引擎
4. `http.Server` + goroutine 启动
5. `signal.Notify` 等待退出信号,`srv.Shutdown` 优雅关闭
这是生产级 Gin 服务的标准启动模板,建议熟记。
## 3.8 动手练习
1. 用 `gin.Default()` 写一个返回当前时间的 `/time` 接口。
2. 改为 `gin.New()` + 自定义 Logger,观察差异。
3. 将启动方式从 `r.Run` 改为 `http.Server` + 优雅关闭。

View File

@ -0,0 +1,154 @@
# 第 4 章 Gin 路由与分组
> 学完本章你将:熟练定义路由、处理路径/查询参数、组织分组、设计 RESTful API。
## 4.1 路由基础
Gin 支持所有 HTTP 方法:
```go
r.GET("/ping", handler)
r.POST("/users", handler)
r.PUT("/users/:id", handler)
r.DELETE("/users/:id", handler)
r.PATCH("/users/:id", handler)
r.OPTIONS("/*any", handler)
r.Any("/any", handler) // 匹配任意方法
```
本项目示例:
```go
r.GET("/health", health.Check) // 健康检查
v1 := r.Group("/api/v1")
v1.GET("/ping", pingHandler.Ping) // 分组路由
```
## 4.2 路径参数
```go
// 定义
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id") // "/users/42" => "42"
})
// 多参数
r.GET("/users/:id/books/:bookID", func(c *gin.Context) {
id := c.Param("id")
bookID := c.Param("bookID")
})
// 通配符(匹配剩余路径)
r.GET("/files/*filepath", func(c *gin.Context) {
fp := c.Param("filepath") // "/files/a/b.txt" => "/a/b.txt"
})
```
## 4.3 查询参数与表单
```go
// GET /search?q=gin&page=2
r.GET("/search", func(c *gin.Context) {
q := c.Query("q") // "gin"
page := c.DefaultQuery("page", "1") // 带默认值
arr := c.QueryArray("tag") // ?tag=a&tag=b => [a b]
m := c.QueryMap("filter") // ?filter[name]=a => map[name:a]
})
// POST form
r.POST("/form", func(c *gin.Context) {
name := c.PostForm("name")
file, _ := c.FormFile("upload")
c.SaveUploadedFile(file, "./"+file.Filename)
})
```
## 4.4 分组路由
分组用于版本、权限、模块划分:
```go
v1 := r.Group("/api/v1")
{
v1.GET("/ping", pingHandler.Ping)
v1.GET("/users", listUsers)
v1.POST("/users", createUser)
}
// 嵌套分组
admin := v1.Group("/admin", authMiddleware())
{
admin.GET("/stats", statsHandler)
}
```
本项目 `internal/router/router.go` 即采用分组:`r.GET("/health")` 独立于 `v1.Group("/api/v1")`,便于探活不走鉴权。
## 4.5 RESTful 设计示例
以用户资源为例:
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/users | 列表 |
| POST | /api/v1/users | 创建 |
| GET | /api/v1/users/:id | 详情 |
| PUT | /api/v1/users/:id | 全量更新 |
| PATCH | /api/v1/users/:id | 部分更新 |
| DELETE | /api/v1/users/:id | 删除 |
```go
users := v1.Group("/users")
{
users.GET("", listUsers)
users.POST("", createUser)
users.GET("/:id", getUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
}
```
## 4.6 路由注册实战(对照本项目)
`internal/router/router.go` 完整模式:
```go
func New(cfg *config.Config) *gin.Engine {
gin.SetMode(cfg.Server.Mode)
r := gin.New()
r.Use(middleware.Recovery())
r.Use(middleware.ZapLogger())
r.Use(middleware.Cors(cfg.Cors))
health := handler.NewHealthHandler()
r.GET("/health", health.Check)
pingRepo := repo.NewPingRepo()
pingSvc := service.NewPingService(pingRepo)
pingHandler := handler.NewPingHandler(pingSvc)
v1 := r.Group("/api/v1")
{ v1.GET("/ping", pingHandler.Ping) }
return r
}
```
依赖注入顺序:`repo -> service -> handler -> router`,各层职责清晰,新增模块时复制此模式即可。
## 4.7 重定向与 NoRoute
```go
r.GET("/old", func(c *gin.Context) {
c.Redirect(301, "/new")
})
r.NoRoute(func(c *gin.Context) {
c.JSON(404, gin.H{"code": 404, "msg": "not found"})
})
```
## 4.8 动手练习
1. 新增 `GET /api/v1/users/:id`,返回 `{"id": "xxx"}`。
2. 实现 `GET /search` 支持 `q` 与 `page` 查询参数,`page` 默认 1。
3. 将 `/health` 与 `/api/v1/*` 分为两组,给后者单独加一个日志中间件。

166
docs/05-gin-中间件.md Normal file
View File

@ -0,0 +1,166 @@
# 第 5 章 Gin 中间件
> 学完本章你将:理解中间件洋葱模型、编写自定义中间件、掌握 Recovery/Logger/CORS/鉴权等常用中间件。
## 5.1 中间件原理
中间件是 `func(*gin.Context)`,通过 `c.Next()` 控制调用链,形成洋葱模型:
```
请求 -> MW1 -> MW2 -> Handler -> MW2 -> MW1 -> 响应
```
```go
func MW1() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("MW1 before")
c.Next() // 调用后续
fmt.Println("MW1 after")
}
}
r.Use(MW1(), MW2())
```
若不调用 `c.Next()` 或调用 `c.Abort()`,则中断后续。`c.AbortWithStatusJSON` 直接返回响应。
## 5.2 注册级别
```go
// 全局
r.Use(middleware.ZapLogger())
// 分组
v1 := r.Group("/api/v1", authMiddleware())
authed := r.Group("/admin")
authed.Use(authMiddleware())
// 单路由
r.GET("/secret", authMiddleware(), secretHandler)
```
本项目 `router.New` 中三件套全局注册:`Recovery`、`ZapLogger`、`Cors`。
## 5.3 Recovery 恢复 panic
`internal/middleware/recovery.go` 源码精读:
```go
func Recovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
logger.Log.Error("panic recovered", ...)
c.AbortWithStatusJSON(500, gin.H{"code":500,"msg":"internal server error"})
}
}()
c.Next()
}
}
```
- 防止单请求 panic 导致进程崩溃
- 记录错误日志并返回 500
- 业务代码中不要滥用 panic
## 5.4 ZapLogger 请求日志
`internal/middleware/logger.go`:
```go
func ZapLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
c.Next()
latency := time.Since(start)
logger.Log.Info("request",
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.Int("status", c.Writer.Status()),
zap.Duration("latency", latency),
)
}
}
```
记录方法、路径、状态码、延迟、IP、User-Agent。生产可在此追加 traceID。
## 5.5 CORS 跨域
`internal/middleware/cors.go` 核心逻辑:
- 根据 `config.CorsConfig.AllowOrigins` 判断是否允许 Origin
- 设置 `Access-Control-Allow-Origin` / `Methods` / `Headers`
- `OPTIONS` 预检请求直接 `204` 返回
配置见 `config/config.yaml`:
```yaml
cors:
allow_origins: ["*"]
allow_methods: ["GET","POST","PUT","DELETE","OPTIONS"]
```
生产建议收紧为具体域名,避免 `*` + `Allow-Credentials: true` 的组合(浏览器会拦截)。
## 5.6 自定义鉴权中间件示例
```go
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.AbortWithStatusJSON(401, gin.H{"code": 401, "msg": "unauthorized"})
return
}
// 校验 token,解析用户
c.Set("userID", 123) // 传递给后续 handler
c.Next()
}
}
// handler 中获取
func Me(c *gin.Context) {
uid, _ := c.Get("userID")
c.JSON(200, gin.H{"userID": uid})
}
```
本项目已引入 `github.com/golang-jwt/jwt/v5` 与 `casbin`,可在鉴权 middleware 中集成 JWT 校验 + Casbin 鉴权。
## 5.7 限流/超时/请求ID(扩展)
```go
// 请求ID
func RequestID() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.GetHeader("X-Request-ID")
if id == "" { id = uuid.NewString() }
c.Set("requestID", id)
c.Header("X-Request-ID", id)
c.Next()
}
}
// 超时
func Timeout(d time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), d)
defer cancel()
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
```
## 5.8 常见坑
- 中间件顺序重要:Recovery 应最先注册,保证捕获所有后续 panic
- `c.Abort()` 后仍会执行已进入的中间件的 `c.Next()` 之后代码,需注意逻辑
- 不要在中间件中做过重同步操作,会阻塞所有请求
## 5.9 动手练习
1. 编写 `RequestID` 中间件并在日志中打印。
2. 编写 `Auth` 中间件,校验 `Authorization: Bearer <token>`,失败返回 401。
3. 调整本项目 `router.New` 中间件顺序,观察 panic 时日志差异。

View File

@ -0,0 +1,164 @@
# 第 6 章 Gin 请求绑定与响应
> 学完本章你将:优雅地绑定与校验请求参数,统一响应格式,处理错误。
## 6.1 参数绑定
Gin 提供 `ShouldBind` 系列,自动根据 Content-Type 选择绑定器:
```go
// JSON: POST {"name":"alice","age":18}
type CreateUserReq struct {
Name string `json:"name" binding:"required"`
Age int `json:"age" binding:"gte=0,lte=120"`
}
var req CreateUserReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
// Query: GET /users?page=1&size=10
type ListReq struct {
Page int `form:"page" binding:"gte=1"`
Size int `form:"size" binding:"gte=1,lte=100"`
}
var q ListReq
c.ShouldBindQuery(&q)
// Path + Query + Form 混合:ShouldBind 自动识别
c.ShouldBind(&req)
// Header
token := c.GetHeader("Authorization")
// 默认值
page := c.DefaultQuery("page", "1")
```
校验标签基于 `go-playground/validator`,本项目已间接依赖 `validator/v10`。
## 6.2 常用校验标签
| 标签 | 说明 |
|------|------|
| `required` | 必填 |
| `gte=0` `lte=120` | 数值范围 |
| `oneof=debug release` | 枚举 |
| `email` `url` | 格式 |
| `min=3` `max=20` | 字符串长度 |
| `len=6` | 定长 |
```go
type LoginReq struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
```
自定义校验:
```go
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("port", func(fl validator.FieldLevel) bool {
p := fl.Field().Int()
return p > 0 && p < 65535
})
}
```
## 6.3 统一响应
本项目 `pkg/response/response.go` 已封装:
```go
type Result struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"`
}
func Success(c *gin.Context, data interface{}) {
c.JSON(200, Result{Code: 0, Msg: "success", Data: data})
}
func BadRequest(c *gin.Context, msg string) {
c.JSON(400, Result{Code: 400, Msg: msg})
}
```
使用:
```go
// handler 中
func (h *PingHandler) Ping(c *gin.Context) {
msg := h.svc.Ping()
response.Success(c, gin.H{"message": msg})
}
// => {"code":0,"msg":"success","data":{"message":"pong"}}
func CreateUser(c *gin.Context) {
var req CreateUserReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
response.Success(c, gin.H{"id": 1})
}
```
约定:`Code 0` 为成功,非 0 为业务错误码;HTTP 状态码反映传输层语义(400 参数错误、401 未认证、500 服务错误)。
## 6.4 错误处理策略
- 参数错误:`400 BadRequest`,返回校验信息
- 业务错误:按需定义错误码,如 `10001 用户不存在`
- 系统错误:`500 InternalError`,日志记录详情但不暴露给客户端
```go
func Fail(c *gin.Context, httpCode, code int, msg string) {
c.JSON(httpCode, Result{Code: code, Msg: msg})
}
```
建议结合 `errors.Is/As` 判断错误类型,在 service 层返回 `error`,handler 层映射为响应。
## 6.5 文件上传与下载
```go
// 上传
r.POST("/upload", func(c *gin.Context) {
file, _ := c.FormFile("file")
c.SaveUploadedFile(file, "./uploads/"+file.Filename)
response.Success(c, gin.H{"filename": file.Filename})
})
// 下载/返回文件
r.GET("/download/:name", func(c *gin.Context) {
c.File("./uploads/" + c.Param("name"))
})
```
## 6.6 完整示例
```go
type CreateBookReq struct {
Title string `json:"title" binding:"required"`
Author string `json:"author" binding:"required"`
Price int `json:"price" binding:"gte=0"`
}
func CreateBook(c *gin.Context) {
var req CreateBookReq
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
// 调用 service...
response.Success(c, gin.H{"title": req.Title})
}
```
## 6.7 动手练习
1. 新增 `POST /api/v1/users`,用 `ShouldBindJSON` + `required/email` 校验。
2. 实现 `GET /api/v1/users?page=&size=` 分页查询,用 `ShouldBindQuery`。
3. 为 `BadRequest` 补充单元测试,验证错误信息是否友好。

View File

@ -0,0 +1,210 @@
# 第 7 章 实战:StreamBox 项目解析
> 学完本章你将:彻底看懂本项目分层架构,能独立新增一个 CRUD 模块,并理解配置/日志/优雅关闭等生产要素。
## 7.1 项目结构总览
```
StreamBox/
├── cmd/server/main.go # 入口:配置→日志→路由→启动→优雅关闭
├── config/
│ ├── config.go # Viper 加载逻辑
│ └── config.yaml # 默认配置
├── internal/
│ ├── handler/ # HTTP 层:参数绑定、调用 service、返回 response
│ ├── service/ # 业务层:核心逻辑,可被多 handler 复用
│ ├── repo/ # 数据层:DB/缓存/外部接口(当前为内存 demo)
│ ├── model/ # 数据模型
│ ├── middleware/ # Recovery/ZapLogger/Cors
│ └── router/router.go # 路由组装与依赖注入
├── pkg/
│ ├── logger/ # Zap 封装
│ └── response/ # 统一 Result
├── api/ # Swagger/API 文档(待扩展)
└── Makefile # run/build/tidy/fmt
```
分层原则:`handler → service → repo` 单向依赖,`pkg` 为可复用基础库,`internal` 禁止外部 import。
## 7.2 入口:cmd/server/main.go 逐行解析
```go
cfg, err := config.Load() // 1. 加载配置
logger.Init(cfg.Log.Level, cfg.Log.Encoding) // 2. 初始化日志
r := router.New(cfg) // 3. 构建 Gin 引擎
srv := &http.Server{Addr: addr, Handler: r} // 4. 用标准 http.Server 包装
go srv.ListenAndServe() // 5. goroutine 启动
<-quit // 6. 阻塞等待 SIGINT/SIGTERM
srv.Shutdown(ctx) // 7. 10 秒优雅关闭
```
为什么不用 `r.Run()`:`r.Run()` 内部直接 `ListenAndServe` 且无法捕获信号做 `Shutdown`,会丢失正在处理的请求。生产务必用 `http.Server + Shutdown`。
## 7.3 配置:config/config.go + viper
`config.yaml` 示例:
```yaml
server:
port: 8080
mode: debug
read_timeout: 10s
write_timeout: 10s
log:
level: debug
encoding: console
cors:
allow_origins: ["*"]
```
`config.Load()` 逻辑:
1. `viper.SetConfigFile("config/config.yaml")` + `ReadInConfig`
2. `viper.AutomaticEnv()` 环境变量覆盖(`SERVER_PORT=9090` 可覆盖 `server.port`)
3. `Unmarshal` 到 `Config` 结构体
新增配置:结构体加字段 → yaml 加默认值 → 代码中 `cfg.NewField` 使用。
## 7.4 日志:pkg/logger
```go
// 初始化
logger.Init(cfg.Log.Level, cfg.Log.Encoding)
// 使用
logger.Log.Info("request", zap.String("path", path))
logger.Sugar.Infow("server starting", "addr", addr)
```
- `console` 适合开发,`json` 适合生产(便于 ELK 收集)
- `level` 动态控制:debug/info/warn/error
- `defer logger.Sync()` 刷盘,避免日志丢失
## 7.5 路由与依赖注入:internal/router/router.go
```go
func New(cfg *config.Config) *gin.Engine {
gin.SetMode(cfg.Server.Mode)
r := gin.New()
r.Use(middleware.Recovery())
r.Use(middleware.ZapLogger())
r.Use(middleware.Cors(cfg.Cors))
health := handler.NewHealthHandler()
r.GET("/health", health.Check)
pingRepo := repo.NewPingRepo()
pingSvc := service.NewPingService(pingRepo)
pingHandler := handler.NewPingHandler(pingSvc)
v1 := r.Group("/api/v1")
{ v1.GET("/ping", pingHandler.Ping) }
return r
}
```
新增模块时复制这三行:`repo → service → handler`,然后挂到分组。
## 7.6 Handler/Service/Repo 职责
- **handler**(`internal/handler/ping.go`):只做 HTTP 相关——取参、校验、调 service、组响应。不写业务逻辑。
- **service**(`internal/service/ping.go`):业务逻辑,可组合多个 repo,返回 `error` 由 handler 转为响应。
- **repo**(`internal/repo/ping.go`):数据访问,当前为内存实现,未来可替换为 GORM/SQLite。
示例 `PingService`:
```go
type PingService struct { repo *repo.PingRepo }
func (s *PingService) Ping() string { return s.repo.Ping() }
```
## 7.7 完整 CRUD 实战(以 Book 为例)
按分层新增一个资源,需 4 个文件 + 1 处路由:
**1. model** `internal/model/book.go`
```go
package model
type Book struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title" binding:"required"`
Author string `json:"author" binding:"required"`
}
```
**2. repo** `internal/repo/book.go`
```go
package repo
import "streambox/internal/model"
type BookRepo struct { db map[uint]*model.Book; nextID uint }
func NewBookRepo() *BookRepo { return &BookRepo{db: make(map[uint]*model.Book), nextID: 1} }
func (r *BookRepo) Create(b *model.Book) *model.Book { b.ID = r.nextID; r.nextID++; r.db[b.ID] = b; return b }
func (r *BookRepo) Get(id uint) (*model.Book, bool) { b, ok := r.db[id]; return b, ok }
func (r *BookRepo) List() []*model.Book { // ...
return nil
}
```
**3. service** `internal/service/book.go`
```go
package service
import "streambox/internal/model"
type BookService struct { repo *repo.BookRepo }
func NewBookService(repo *repo.BookRepo) *BookService { return &BookService{repo: repo} }
func (s *BookService) Create(b *model.Book) *model.Book { return s.repo.Create(b) }
```
**4. handler** `internal/handler/book.go`
```go
type BookHandler struct { svc *service.BookService }
func NewBookHandler(svc *service.BookService) *BookHandler { return &BookHandler{svc: svc} }
func (h *BookHandler) Create(c *gin.Context) {
var req model.Book
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error()); return
}
book := h.svc.Create(&req)
response.Success(c, book)
}
func (h *BookHandler) Get(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if b, ok := h.svc.Get(uint(id)); ok {
response.Success(c, b)
} else {
response.Fail(c, 404, 404, "book not found")
}
}
```
**5. router** `internal/router/router.go` 追加
```go
bookRepo := repo.NewBookRepo()
bookSvc := service.NewBookService(bookRepo)
bookHandler := handler.NewBookHandler(bookSvc)
v1.POST("/books", bookHandler.Create)
v1.GET("/books/:id", bookHandler.Get)
v1.GET("/books", bookHandler.List)
```
重启 `make run`,`curl -X POST localhost:8080/api/v1/books -H 'Content-Type: application/json' -d '{"title":"Go","author":"A"}'` 验证。
> 替换为真实数据库时,只需将 `BookRepo` 内部改为 `*gorm.DB`,service/handler 无需改动,这就是分层的价值。
## 7.8 统一响应与错误
所有 handler 均通过 `pkg/response` 返回,避免 `c.JSON` 散落各处,便于前端统一处理。
## 7.9 动手练习
1. 按上述步骤完整实现 Book 的 List/Update/Delete。
2. 将 BookRepo 替换为 GORM + SQLite(项目已依赖 `glebarez/sqlite`)。
3. 为 Book 补充 `go test` 单测。

120
docs/08-部署与进阶.md Normal file
View File

@ -0,0 +1,120 @@
# 第 8 章 部署与进阶
> 学完本章你将:编译与部署 Gin 服务,用 Docker 容器化,掌握生产配置与后续学习路线。
## 8.1 编译与运行
```bash
make build # go build -o bin/streambox ./cmd/server
./bin/streambox # 直接运行二进制
# 交叉编译
GOOS=linux GOARCH=amd64 go build -o bin/streambox-linux ./cmd/server
# 运行时配置
SERVER_PORT=9090 LOG_LEVEL=info ./bin/streambox
# 或修改 config/config.yaml
```
二进制无依赖,拷贝到服务器即可运行。
## 8.2 优雅关闭回顾
`cmd/server/main.go` 已实现:
```go
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)
```
`Shutdown` 会等待正在处理的请求完成(最多 10 秒),再退出进程,避免 k8s 滚动更新时丢请求。需配合 `ReadTimeout`/`WriteTimeout`(见 `config.yaml`)。
## 8.3 生产配置
| 项 | 建议 |
|----|------|
| `server.mode` | `release`(关闭 Gin debug 日志) |
| `log.level` | `info` 或 `warn` |
| `log.encoding` | `json`(便于日志收集) |
| `cors.allow_origins` | 具体域名,不用 `*` |
| 端口 | 环境变量 `SERVER_PORT` 覆盖,避免改文件 |
敏感信息(DB 密码、JWT 密钥)务必走环境变量或 Secret,不提交到 Git。参考 `.env.example`。
## 8.4 Dockerfile
```dockerfile
# 构建阶段
FROM golang:1.26-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /streambox ./cmd/server
# 运行阶段
FROM alpine:3.20
WORKDIR /app
COPY --from=builder /streambox /app/streambox
COPY config/config.yaml /app/config/config.yaml
EXPOSE 8080
CMD ["/app/streambox"]
```
```bash
docker build -t streambox:latest .
docker run -p 8080:8080 -e SERVER_PORT=8080 streambox:latest
curl http://localhost:8080/health
```
> 若使用 `glebarez/sqlite`(纯 Go 驱动),`CGO_ENABLED=0` 可用;若用 `mattn/go-sqlite3` 需 `CGO_ENABLED=1`。
## 8.5 反向代理与多实例
生产常在 Gin 前加 Nginx/Caddy 做 TLS、静态资源、限流:
```nginx
upstream streambox { server 127.0.0.1:8080; }
server {
listen 80;
location / { proxy_pass http://streambox; }
}
```
多实例用 k8s Deployment + Service,或 systemd 多进程 + LB。
## 8.6 性能与可观测性
- **pprof**:`import _ "net/http/pprof"` + `r.GET("/debug/pprof/*any", gin.WrapH(http.DefaultServeMux))`(仅内网暴露)
- **压测**:`go test -bench`、`wrk`、`hey`
- **追踪**:日志中加入 `requestID`(见第 5 章中间件)
- **指标**:可接入 Prometheus(`ginprom` 中间件)
## 8.7 下一步学习路线
1. **数据库**:GORM + SQLite/PostgreSQL,事务与迁移
2. **鉴权**:JWT(`golang-jwt/jwt`)+ Casbin(本项目已引入)
3. **文档**:Swagger(`swaggo/gin-swagger` 本项目已引入,`swag init` 生成)
4. **验证**:深入 `validator` 自定义规则
5. **测试**:`httptest` + `testify` 做 handler 集成测试
6. **进阶**:依赖注入(Wire/Fx)、配置中心、消息队列
## 8.8 常用命令速查
```bash
make run # 开发运行
make build # 编译
make tidy # 整理依赖
make fmt # 格式化
go test ./... # 测试
go vet ./... # 静态检查
curl http://localhost:8080/health
```
---
至此,Go + Gin 从入门到部署已全部覆盖。建议从第 7 章的 Book CRUD 开始,亲手扩展一个完整模块,遇到问题回查对应章节。

47
docs/README.md Normal file
View File

@ -0,0 +1,47 @@
# Go + Gin 开发教程
> 基于本项目 StreamBox(Go 1.26.2 + Gin v1.12.0)编写的系统化教程,从零搭建到生产部署。
## 适用人群
- 有任意一门编程语言基础,想系统学习 Go
- 想用 Gin 快速开发 RESTful API 的后端开发者
- 想看懂/维护本项目 `StreamBox` 代码的同学
## 目录
| 章节 | 主题 | 核心内容 |
|------|------|----------|
| [第 1 章](01-go-环境与基础语法.md) | Go 环境与基础语法 | 安装、go mod、变量/类型/流程/函数/结构体/接口/错误处理 |
| [第 2 章](02-go-进阶-并发与工程化.md) | Go 进阶:并发与工程化 | goroutine/channel/context/sync、测试、常用工具链 |
| [第 3 章](03-gin-入门.md) | Gin 入门 | 为什么选 Gin、Hello World、与 net/http 对比、项目初始化 |
| [第 4 章](04-gin-路由与分组.md) | Gin 路由与分组 | 路由定义、路径参数、查询参数、分组、RESTful 设计 |
| [第 5 章](05-gin-中间件.md) | Gin 中间件 | 原理、自定义、全局/分组/路由级、Recovery/Logger/CORS、鉴权 |
| [第 6 章](06-gin-请求绑定与响应.md) | 请求绑定与响应 | 参数绑定、校验、统一响应、错误处理 |
| [第 7 章](07-实战-StreamBox项目解析.md) | 实战:StreamBox 项目解析 | 分层架构、配置、日志、优雅关闭、完整 CRUD 示例 |
| [第 8 章](08-部署与进阶.md) | 部署与进阶 | 编译、Docker、生产配置、性能与下一阶段学习路线 |
## 如何阅读
1. 按顺序阅读,第 1-2 章打 Go 基础,第 3-6 章掌握 Gin,第 7 章对照本项目源码,第 8 章上线。
2. 每章末尾有“动手练习”,建议敲一遍。
3. 本教程所有代码均可在本仓库直接运行:`make run` 启动,`curl http://localhost:8080/health` 验证。
## 快速开始
```bash
# 克隆后直接运行
make tidy
make run
# 验证
curl http://localhost:8080/health
# => {"code":0,"msg":"success","data":{"status":"ok"}}
curl http://localhost:8080/api/v1/ping
# => {"code":0,"msg":"success","data":{"message":"pong"}}
```
---
> 约定:教程中 `streambox` 为 module 名(见 `go.mod`),示例代码可直接复制到本项目中运行。

79
go.mod Normal file
View File

@ -0,0 +1,79 @@
module streambox
go 1.26.2
require (
github.com/casbin/casbin/v2 v2.135.0
github.com/gin-gonic/gin v1.12.0
github.com/glebarez/sqlite v1.6.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/spf13/viper v1.21.0
github.com/swaggo/gin-swagger v1.6.1
go.uber.org/zap v1.28.0
golang.org/x/crypto v0.55.0
)
require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/casbin/govaluate v1.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/glebarez/go-sqlite v1.20.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggo/swag v1.8.12 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/tools v0.48.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gorm.io/gorm v1.24.2 // indirect
modernc.org/libc v1.21.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.4.0 // indirect
modernc.org/sqlite v1.20.0 // indirect
)

273
go.sum Normal file
View File

@ -0,0 +1,273 @@
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18=
github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc=
github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/glebarez/go-sqlite v1.20.0 h1:6D9uRXq3Kd+W7At+hOU2eIAeahv6qcYfO8jzmvb4Dr8=
github.com/glebarez/go-sqlite v1.20.0/go.mod h1:uTnJoqtwMQjlULmljLT73Cg7HB+2X6evsBHODyyq1ak=
github.com/glebarez/sqlite v1.6.0 h1:ZpvDLv4zBi2cuuQPitRiVz/5Uh6sXa5d8eBu0xNTpAo=
github.com/glebarez/sqlite v1.6.0/go.mod h1:6D6zPU/HTrFlYmVDKqBJlmQvma90P6r7sRRdkUUZOYk=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w=
github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.24.2 h1:9wR6CFD+G8nOusLdvkZelOEhpJVwwHzpQOUM+REd6U0=
gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI=
modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0=
modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA=
modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0=
modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
modernc.org/libc v1.21.5 h1:xBkU9fnHV+hvZuPSRszN0AXDG4M7nwPLwTWwkYcvLCI=
modernc.org/libc v1.21.5/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.20.0 h1:80zmD3BGkm8BZ5fUi/4lwJQHiO3GXgIUvZRXpoIfROY=
modernc.org/sqlite v1.20.0/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=

View File

@ -0,0 +1,15 @@
package handler
import (
"github.com/gin-gonic/gin"
"streambox/pkg/response"
)
type HealthHandler struct{}
func NewHealthHandler() *HealthHandler { return &HealthHandler{} }
func (h *HealthHandler) Check(c *gin.Context) {
response.Success(c, gin.H{"status": "ok"})
}

21
internal/handler/ping.go Normal file
View File

@ -0,0 +1,21 @@
package handler
import (
"github.com/gin-gonic/gin"
"streambox/internal/service"
"streambox/pkg/response"
)
type PingHandler struct {
svc *service.PingService
}
func NewPingHandler(svc *service.PingService) *PingHandler {
return &PingHandler{svc: svc}
}
func (h *PingHandler) Ping(c *gin.Context) {
msg := h.svc.Ping()
response.Success(c, gin.H{"message": msg})
}

View File

@ -0,0 +1,51 @@
package middleware
import (
"github.com/gin-gonic/gin"
"streambox/config"
)
func Cors(cfg config.CorsConfig) gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.GetHeader("Origin")
allowed := false
for _, o := range cfg.AllowOrigins {
if o == "*" || o == origin {
allowed = true
break
}
}
if allowed {
if len(cfg.AllowOrigins) == 1 && cfg.AllowOrigins[0] == "*" {
c.Header("Access-Control-Allow-Origin", "*")
} else if origin != "" {
c.Header("Access-Control-Allow-Origin", origin)
}
c.Header("Vary", "Origin")
}
c.Header("Access-Control-Allow-Credentials", "true")
if len(cfg.AllowMethods) > 0 {
c.Header("Access-Control-Allow-Methods", join(cfg.AllowMethods))
}
if len(cfg.AllowHeaders) > 0 {
c.Header("Access-Control-Allow-Headers", join(cfg.AllowHeaders))
}
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
func join(a []string) string {
s := ""
for i, v := range a {
if i > 0 {
s += ", "
}
s += v
}
return s
}

View File

@ -0,0 +1,35 @@
package middleware
import (
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"streambox/pkg/logger"
)
func ZapLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
latency := time.Since(start)
if logger.Log != nil {
fields := []zap.Field{
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.Int("status", c.Writer.Status()),
zap.Duration("latency", latency),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
}
if len(c.Errors) > 0 {
fields = append(fields, zap.String("errors", c.Errors.String()))
}
logger.Log.Info("request", fields...)
}
}
}

View File

@ -0,0 +1,27 @@
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"streambox/pkg/logger"
)
func Recovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
if logger.Log != nil {
logger.Log.Error("panic recovered", zap.Any("panic", r), zap.String("path", c.Request.URL.Path))
}
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "internal server error",
})
}
}()
c.Next()
}
}

25
internal/model/common.go Normal file
View File

@ -0,0 +1,25 @@
package model
import "time"
type Pagination struct {
Page int `form:"page" binding:"omitempty,min=1"`
PageSize int `form:"page_size" binding:"omitempty,min=1,max=100"`
}
func (p *Pagination) Normalize() {
if p.Page <= 0 {
p.Page = 1
}
if p.PageSize <= 0 {
p.PageSize = 10
}
}
func (p Pagination) Offset() int { return (p.Page - 1) * p.PageSize }
type BaseModel struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

22
internal/repo/demo.go Normal file
View File

@ -0,0 +1,22 @@
package repo
import (
"context"
"streambox/internal/model"
)
// DemoRepo 演示带接口抽象的仓储写法,供后续业务直接扩展
type DemoRepo interface {
List(ctx context.Context, p model.Pagination) ([]string, error)
}
type demoRepo struct{}
func NewDemoRepo() DemoRepo { return &demoRepo{} }
func (r *demoRepo) List(_ context.Context, p model.Pagination) ([]string, error) {
p.Normalize()
// 占位实现,后续接入 GORM/sqlx 等
return []string{}, nil
}

8
internal/repo/ping.go Normal file
View File

@ -0,0 +1,8 @@
package repo
// PingRepo 示例仓储层,后续可替换为真实 DB/缓存实现
type PingRepo struct{}
func NewPingRepo() *PingRepo { return &PingRepo{} }
func (r *PingRepo) GetMessage() string { return "pong" }

35
internal/router/router.go Normal file
View File

@ -0,0 +1,35 @@
package router
import (
"github.com/gin-gonic/gin"
"streambox/config"
"streambox/internal/handler"
"streambox/internal/middleware"
"streambox/internal/repo"
"streambox/internal/service"
)
func New(cfg *config.Config) *gin.Engine {
gin.SetMode(cfg.Server.Mode)
r := gin.New()
r.Use(middleware.Recovery())
r.Use(middleware.ZapLogger())
r.Use(middleware.Cors(cfg.Cors))
// 健康检查(不带前缀,方便探活)
health := handler.NewHealthHandler()
r.GET("/health", health.Check)
// 依赖注入:repo -> service -> handler
pingRepo := repo.NewPingRepo()
pingSvc := service.NewPingService(pingRepo)
pingHandler := handler.NewPingHandler(pingSvc)
v1 := r.Group("/api/v1")
{
v1.GET("/ping", pingHandler.Ping)
}
return r
}

11
internal/service/ping.go Normal file
View File

@ -0,0 +1,11 @@
package service
import "streambox/internal/repo"
type PingService struct {
repo *repo.PingRepo
}
func NewPingService(r *repo.PingRepo) *PingService { return &PingService{repo: r} }
func (s *PingService) Ping() string { return s.repo.GetMessage() }

31
pkg/logger/logger.go Normal file
View File

@ -0,0 +1,31 @@
package logger
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var Log *zap.Logger
var Sugar *zap.SugaredLogger
func Init(level, encoding string) error {
cfg := zap.NewProductionConfig()
if encoding == "console" {
cfg.Encoding = "console"
cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
} else {
cfg.Encoding = "json"
}
if lvl, err := zapcore.ParseLevel(level); err == nil {
cfg.Level = zap.NewAtomicLevelAt(lvl)
}
l, err := cfg.Build(zap.AddCaller(), zap.AddCallerSkip(1))
if err != nil {
return err
}
Log = l
Sugar = l.Sugar()
return nil
}
func Sync() { _ = Log.Sync() }

36
pkg/response/response.go Normal file
View File

@ -0,0 +1,36 @@
package response
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Result struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data,omitempty"`
}
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Result{Code: 0, Msg: "success", Data: data})
}
func SuccessWithMsg(c *gin.Context, msg string, data interface{}) {
c.JSON(http.StatusOK, Result{Code: 0, Msg: msg, Data: data})
}
func Fail(c *gin.Context, httpCode, code int, msg string) {
c.JSON(httpCode, Result{Code: code, Msg: msg})
}
func BadRequest(c *gin.Context, msg string) {
Fail(c, http.StatusBadRequest, 400, msg)
}
func InternalError(c *gin.Context, msg string) {
if msg == "" {
msg = "internal server error"
}
Fail(c, http.StatusInternalServerError, 500, msg)
}

11
tools.go Normal file
View File

@ -0,0 +1,11 @@
//go:build tools
package tools
import (
_ "github.com/casbin/casbin/v2"
_ "github.com/glebarez/sqlite"
_ "github.com/golang-jwt/jwt/v5"
_ "github.com/swaggo/gin-swagger"
_ "golang.org/x/crypto/bcrypt"
)