feat: initial commit - StreamBox Go+Gin framework
This commit is contained in:
commit
c400394ed6
|
|
@ -0,0 +1,2 @@
|
||||||
|
PORT=8080
|
||||||
|
GIN_MODE=debug
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
bin/
|
||||||
|
vendor/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
tmp/
|
||||||
|
|
@ -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/
|
||||||
|
|
@ -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` 示例接口
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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` 序列化验证。
|
||||||
|
|
||||||
|
下一章将学习并发与工程化。
|
||||||
|
|
||||||
|
|
@ -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` 验证。
|
||||||
|
|
@ -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` + 优雅关闭。
|
||||||
|
|
@ -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/*` 分为两组,给后者单独加一个日志中间件。
|
||||||
|
|
@ -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 时日志差异。
|
||||||
|
|
@ -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` 补充单元测试,验证错误信息是否友好。
|
||||||
|
|
@ -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` 单测。
|
||||||
|
|
@ -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 开始,亲手扩展一个完整模块,遇到问题回查对应章节。
|
||||||
|
|
@ -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`),示例代码可直接复制到本项目中运行。
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
@ -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=
|
||||||
|
|
@ -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"})
|
||||||
|
}
|
||||||
|
|
@ -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})
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"`
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package repo
|
||||||
|
|
||||||
|
// PingRepo 示例仓储层,后续可替换为真实 DB/缓存实现
|
||||||
|
type PingRepo struct{}
|
||||||
|
|
||||||
|
func NewPingRepo() *PingRepo { return &PingRepo{} }
|
||||||
|
|
||||||
|
func (r *PingRepo) GetMessage() string { return "pong" }
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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() }
|
||||||
|
|
@ -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() }
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue