feat(sidebar): 添加 Web开发数据库 侧边栏模块
- 在侧边栏配置中新增 Web开发数据库分类 - 增加相关文档链接,包括 Gin 框架、GORM、Viper、Zap 等内容 - 设置分类可折叠,提升导航体验 refactor(go): 精简 Go 示例代码导入包 - 移除未使用的 runtime 和 time 包 - 保留 fmt 和 sync 包,优化依赖 docs(linux): 删除 Nginx 基础入门文档 - 移除包含 Nginx 基本概念、安装、配置和常用命令的完整文档 - 清理废弃内容,简化项目文档结构
This commit is contained in:
@@ -55,9 +55,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
793
src/programming/backend/go/Web开发数据库/15Gin基础入门.md
Normal file
793
src/programming/backend/go/Web开发数据库/15Gin基础入门.md
Normal file
@@ -0,0 +1,793 @@
|
||||
---
|
||||
title: Gin基础入门
|
||||
icon: mdi:web
|
||||
date: 2025-12-23
|
||||
category:
|
||||
- Go
|
||||
- 后端
|
||||
- Web框架
|
||||
- 工程化
|
||||
- Gin
|
||||
tag:
|
||||
- Gin
|
||||
- 路由
|
||||
- Handler
|
||||
- 参数绑定
|
||||
- REST API
|
||||
---
|
||||
|
||||
Go 生态中最流行的 Web 框架是 Gin,以高性能和易用性著称。本文通过对比 Java Spring Boot,帮助你快速掌握 Gin 的核心概念:路由、处理器、参数绑定。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
---
|
||||
|
||||
# Go Web 框架:Gin 基础入门(路由 + Handler + 参数绑定)
|
||||
|
||||
> 如果你熟悉 Java 的 Spring Boot,Gin 可以视为其轻量级的 Go 版本。本文以对比方式讲解。
|
||||
|
||||
---
|
||||
|
||||
## 一、项目初始化
|
||||
|
||||
### 1.1 创建项目和安装 Gin
|
||||
|
||||
```bash
|
||||
cd ~/GolandProjects
|
||||
mkdir go-gin-demo && cd go-gin-demo
|
||||
go mod init go-gin-demo
|
||||
|
||||
# 安装 Gin(会自动下载到 go.mod 和 go.sum)
|
||||
go get -u github.com/gin-gonic/gin
|
||||
```
|
||||
|
||||
> **版本说明**:如果想指定特定版本(如 v1.9.x),可使用:
|
||||
> ```bash
|
||||
> go get github.com/gin-gonic/gin@v1.9.1
|
||||
> ```
|
||||
> 所有示例代码都兼容 Go 1.18+ 版本
|
||||
|
||||
### 1.2 验证安装
|
||||
|
||||
```bash
|
||||
cat go.mod
|
||||
# 应该看到类似:
|
||||
# require github.com/gin-gonic/gin v1.9.x
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、最小化 Gin 应用
|
||||
|
||||
### 2.1 编写 main.go
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建 Gin 引擎(类似 Spring 的 ApplicationContext)
|
||||
r := gin.Default()
|
||||
|
||||
// 注册路由和处理器
|
||||
r.GET("/hello", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"message": "Hello, Gin!",
|
||||
})
|
||||
})
|
||||
|
||||
// 启动服务器,监听 8080 端口
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 运行应用
|
||||
|
||||
```bash
|
||||
go run .
|
||||
```
|
||||
|
||||
输出:
|
||||
```
|
||||
[GIN-debug] Loaded HTML Templates (0):
|
||||
[GIN-debug] Loaded HTML Templates (0):
|
||||
[GIN-debug] GET /hello --> main.main.func1 (3 handlers)
|
||||
[GIN-debug] [WARNING] Running in "debug" mode. Switch to release mode in production.
|
||||
[GIN-debug] listening on [::]:8080
|
||||
```
|
||||
|
||||
### 2.3 测试接口
|
||||
|
||||
在另一个终端运行:
|
||||
```bash
|
||||
curl http://localhost:8080/hello
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{"message":"Hello, Gin!"}
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `gin.Default()` ≈ Spring 的 `@SpringBootApplication`
|
||||
> - `r.GET("/hello", handler)` ≈ `@GetMapping("/hello")`
|
||||
> - `gin.Context` ≈ Spring 的 `HttpServletRequest` + `HttpServletResponse`
|
||||
|
||||
---
|
||||
|
||||
## 三、核心概念详解
|
||||
|
||||
### 3.1 Context(请求上下文)
|
||||
```bash
|
||||
nano context_demo.go
|
||||
```
|
||||
每个请求都对应一个 `*gin.Context`,包含所有请求信息和响应方法。创建 `context_demo.go`:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
r.GET("/user", func(c *gin.Context) {
|
||||
// 获取请求信息
|
||||
method := c.Request.Method // GET
|
||||
path := c.Request.URL.Path // /user
|
||||
|
||||
// 响应数据
|
||||
c.JSON(200, gin.H{"name": "Alice"})
|
||||
})
|
||||
|
||||
// 支持多种响应格式
|
||||
r.GET("/text", func(c *gin.Context) {
|
||||
c.String(200, "Hello, Gin!") // 纯文本
|
||||
})
|
||||
|
||||
r.GET("/yaml", func(c *gin.Context) {
|
||||
c.YAML(200, gin.H{
|
||||
"name": "Bob",
|
||||
"age": 30,
|
||||
}) // YAML 格式
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run context_demo.go
|
||||
|
||||
# 在另一个终端
|
||||
curl http://localhost:8080/user
|
||||
# {"name":"Alice"}
|
||||
|
||||
curl http://localhost:8080/text
|
||||
# Hello, Gin!
|
||||
|
||||
curl http://localhost:8080/yaml
|
||||
# name: Bob
|
||||
# age: 30
|
||||
```
|
||||
|
||||
### 3.2 路由(Routes)
|
||||
```bash
|
||||
nano routes_demo.go
|
||||
```
|
||||
路由是将 HTTP 请求映射到处理器的方式。创建 `routes_demo.go`:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// 基础 CRUD 路由
|
||||
r.GET("/products", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "Get all products"})
|
||||
})
|
||||
|
||||
r.POST("/products", func(c *gin.Context) {
|
||||
c.JSON(201, gin.H{"message": "Product created"})
|
||||
})
|
||||
|
||||
r.PUT("/products/:id", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
c.JSON(200, gin.H{"message": "Product updated", "id": id})
|
||||
})
|
||||
|
||||
r.DELETE("/products/:id", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
c.JSON(200, gin.H{"message": "Product deleted", "id": id})
|
||||
})
|
||||
|
||||
// 路由分组(推荐)
|
||||
api := r.Group("/api/v1")
|
||||
{
|
||||
api.GET("/users", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "Get all users"})
|
||||
})
|
||||
|
||||
api.POST("/users", func(c *gin.Context) {
|
||||
c.JSON(201, gin.H{"message": "User created"})
|
||||
})
|
||||
|
||||
// 嵌套分组
|
||||
users := api.Group("/users")
|
||||
{
|
||||
users.GET("/:id", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
c.JSON(200, gin.H{"message": "Get user", "id": id})
|
||||
})
|
||||
|
||||
users.PUT("/:id", func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
c.JSON(200, gin.H{"message": "User updated", "id": id})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run routes_demo.go
|
||||
|
||||
# 基础路由
|
||||
curl http://localhost:8080/products
|
||||
curl -X POST http://localhost:8080/products
|
||||
curl -X PUT http://localhost:8080/products/1
|
||||
curl -X DELETE http://localhost:8080/products/1
|
||||
|
||||
# 路由分组
|
||||
curl http://localhost:8080/api/v1/users
|
||||
curl http://localhost:8080/api/v1/users/123
|
||||
curl -X PUT http://localhost:8080/api/v1/users/123
|
||||
```
|
||||
|
||||
### 3.3 Handler(处理器)
|
||||
```bash
|
||||
nano handler_demo.go
|
||||
```
|
||||
Handler 就是响应请求的函数。我们演示三种定义方式。创建 `handler_demo.go`:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 方式 2:独立函数(推荐,便于单元测试)
|
||||
func getProduct(c *gin.Context) {
|
||||
productID := c.Param("id")
|
||||
c.JSON(200, gin.H{
|
||||
"id": productID,
|
||||
"name": "Product",
|
||||
})
|
||||
}
|
||||
|
||||
// 方式 3:结构体方法(便于依赖注入)
|
||||
type ProductService struct {
|
||||
name string
|
||||
}
|
||||
|
||||
type ProductHandler struct {
|
||||
service *ProductService
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Get(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
// 使用 h.service 调用业务逻辑
|
||||
c.JSON(200, gin.H{
|
||||
"id": id,
|
||||
"service_name": h.service.name,
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// 方式 1:直接定义
|
||||
r.GET("/method1", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"method": "inline"})
|
||||
})
|
||||
|
||||
// 方式 2:使用独立函数
|
||||
r.GET("/products/:id", getProduct)
|
||||
|
||||
// 方式 3:使用结构体方法
|
||||
service := &ProductService{name: "ProductService"}
|
||||
handler := &ProductHandler{service: service}
|
||||
r.GET("/products-struct/:id", handler.Get)
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run handler_demo.go
|
||||
|
||||
# 在另一个终端
|
||||
# 方式 1
|
||||
curl http://localhost:8080/method1
|
||||
# {"method":"inline"}
|
||||
|
||||
# 方式 2
|
||||
curl http://localhost:8080/products/123
|
||||
# {"id":"123","name":"Product"}
|
||||
|
||||
# 方式 3
|
||||
curl http://localhost:8080/products-struct/456
|
||||
# {"id":"456","service_name":"ProductService"}
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - Gin 的 Handler ≈ Spring 的 `@RestController` 方法
|
||||
> - 直接函数更符合 Go 的函数式编程风格
|
||||
|
||||
---
|
||||
|
||||
## 四、参数绑定详解
|
||||
|
||||
### 4.1 URL 路径参数
|
||||
创建 `params_path_demo.go`:
|
||||
```bash
|
||||
nano params_path_demo.go
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// 单个参数
|
||||
r.GET("/users/:id", func(c *gin.Context) {
|
||||
id := c.Param("id") // 获取参数
|
||||
c.JSON(200, gin.H{"user_id": id})
|
||||
})
|
||||
|
||||
// 多个参数
|
||||
r.GET("/users/:uid/posts/:pid", func(c *gin.Context) {
|
||||
uid := c.Param("uid")
|
||||
pid := c.Param("pid")
|
||||
c.JSON(200, gin.H{"uid": uid, "pid": pid})
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run params_path_demo.go
|
||||
|
||||
curl http://localhost:8080/users/123
|
||||
# {"user_id":"123"}
|
||||
|
||||
curl http://localhost:8080/users/123/posts/456
|
||||
# {"uid":"123","pid":"456"}
|
||||
```
|
||||
|
||||
### 4.2 查询字符串参数(Query)
|
||||
|
||||
创建 `params_query_demo.go`:
|
||||
```bash
|
||||
nano params_query_demo.go
|
||||
```
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
r.GET("/search", func(c *gin.Context) {
|
||||
keyword := c.Query("q") // 获取查询参数
|
||||
limit := c.DefaultQuery("limit", "10") // 带默认值
|
||||
c.JSON(200, gin.H{
|
||||
"keyword": keyword,
|
||||
"limit": limit,
|
||||
})
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run params_query_demo.go
|
||||
|
||||
# 提供了两个查询参数
|
||||
curl 'http://localhost:8080/search?q=golang&limit=20'
|
||||
# {"keyword":"golang","limit":"20"}
|
||||
|
||||
# 只依赖默认值
|
||||
curl 'http://localhost:8080/search?q=go'
|
||||
# {"keyword":"go","limit":"10"}
|
||||
```
|
||||
|
||||
### 4.3 JSON 请求体(Bind)
|
||||
|
||||
创建 `params_json_demo.go`:
|
||||
```bash
|
||||
nano params_json_demo.go
|
||||
```
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 定义数据结构体
|
||||
type User struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
r.POST("/users", func(c *gin.Context) {
|
||||
var user User
|
||||
|
||||
// 方式 1:ShouldBindJSON(推荐,错误时不会中断)
|
||||
if err := c.ShouldBindJSON(&user); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 业务逻辑
|
||||
c.JSON(201, gin.H{
|
||||
"message": "User created",
|
||||
"data": user,
|
||||
})
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run params_json_demo.go
|
||||
|
||||
# 发送 JSON 请求
|
||||
curl -X POST http://localhost:8080/users \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"name":"Alice","email":"alice@example.com","age":30}'
|
||||
|
||||
# 响应
|
||||
# {"message":"User created","data":{"name":"Alice","email":"alice@example.com","age":30}}
|
||||
```
|
||||
|
||||
### 4.4 表单参数(Form)
|
||||
|
||||
创建 `params_form_demo.go`:
|
||||
```bash
|
||||
nano params_form_demo.go
|
||||
```
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
r.POST("/login", func(c *gin.Context) {
|
||||
username := c.PostForm("username") // 获取表单参数
|
||||
password := c.PostForm("password")
|
||||
|
||||
// 简单验证
|
||||
if username == "" || password == "" {
|
||||
c.JSON(400, gin.H{"error": "username and password required"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"message": "Login successful",
|
||||
"username": username,
|
||||
})
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run params_form_demo.go
|
||||
|
||||
# 发送表单数据
|
||||
curl -X POST http://localhost:8080/login \\
|
||||
-d "username=admin&password=123456"
|
||||
|
||||
# 响应
|
||||
# {"message":"Login successful","username":"admin"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、实战:完整的用户管理 API
|
||||
|
||||
### 5.1 数据结构
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 用户结构体
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
}
|
||||
|
||||
// 内存存储(实战应使用数据库)
|
||||
var users = []User{
|
||||
{ID: 1, Name: "Alice", Email: "alice@example.com"},
|
||||
{ID: 2, Name: "Bob", Email: "bob@example.com"},
|
||||
}
|
||||
|
||||
var nextID = 3
|
||||
```
|
||||
|
||||
### 5.2 业务逻辑 Handler
|
||||
|
||||
```go
|
||||
// 获取所有用户
|
||||
func listUsers(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, users)
|
||||
}
|
||||
|
||||
// 获取单个用户
|
||||
func getUser(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
for _, user := range users {
|
||||
if user.ID == intParam(id) {
|
||||
c.JSON(http.StatusOK, user)
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
func createUser(c *gin.Context) {
|
||||
var user User
|
||||
if err := c.ShouldBindJSON(&user); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user.ID = nextID
|
||||
nextID++
|
||||
users = append(users, user)
|
||||
c.JSON(http.StatusCreated, user)
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
func updateUser(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var user User
|
||||
|
||||
if err := c.ShouldBindJSON(&user); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
for i, u := range users {
|
||||
if u.ID == intParam(id) {
|
||||
user.ID = u.ID
|
||||
users[i] = user
|
||||
c.JSON(http.StatusOK, user)
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
func deleteUser(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
for i, user := range users {
|
||||
if user.ID == intParam(id) {
|
||||
users = append(users[:i], users[i+1:]...)
|
||||
c.JSON(http.StatusNoContent, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
func intParam(s string) int {
|
||||
var i int
|
||||
fmt.Sscanf(s, "%d", &i)
|
||||
return i
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 路由配置和启动
|
||||
|
||||
```go
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// API 分组
|
||||
api := r.Group("/api")
|
||||
{
|
||||
users := api.Group("/users")
|
||||
{
|
||||
users.GET("", listUsers)
|
||||
users.POST("", createUser)
|
||||
users.GET("/:id", getUser)
|
||||
users.PUT("/:id", updateUser)
|
||||
users.DELETE("/:id", deleteUser)
|
||||
}
|
||||
}
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 测试 API
|
||||
|
||||
```bash
|
||||
# 获取所有用户
|
||||
curl http://localhost:8080/api/users
|
||||
|
||||
# 创建用户
|
||||
curl -X POST http://localhost:8080/api/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"Charlie","email":"charlie@example.com"}'
|
||||
|
||||
# 获取单个用户
|
||||
curl http://localhost:8080/api/users/1
|
||||
|
||||
# 更新用户
|
||||
curl -X PUT http://localhost:8080/api/users/1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"Alice Updated","email":"alice.new@example.com"}'
|
||||
|
||||
# 删除用户
|
||||
curl -X DELETE http://localhost:8080/api/users/1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、常用配置
|
||||
|
||||
### 6.1 生产模式
|
||||
|
||||
```go
|
||||
// 开发模式(默认)
|
||||
gin.SetMode(gin.DebugMode)
|
||||
|
||||
// 生产模式(关闭调试日志,提高性能)
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
r := gin.Default()
|
||||
```
|
||||
|
||||
### 6.2 自定义端口
|
||||
|
||||
```go
|
||||
r.Run(":9000") // 端口 9000
|
||||
r.Run("0.0.0.0:8080") // 所有网卡监听
|
||||
r.Run("127.0.0.1:8080") // 仅本地
|
||||
```
|
||||
|
||||
### 6.3 自定义日志
|
||||
|
||||
```go
|
||||
r := gin.New()
|
||||
|
||||
// 使用自定义中间件记录日志
|
||||
r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
return fmt.Sprintf("%s %s %d %s\n",
|
||||
param.TimeStamp.Format("2006-01-02 15:04:05"),
|
||||
param.Request.Method,
|
||||
param.StatusCode,
|
||||
param.Request.URL.Path,
|
||||
)
|
||||
}))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、对比总结
|
||||
|
||||
| 概念 | Gin | Spring Boot | 说明 |
|
||||
|------|-----|-------------|------|
|
||||
| 应用入口 | `gin.Default()` | `@SpringBootApplication` | 创建应用引擎 |
|
||||
| 路由注册 | `r.GET("/path", handler)` | `@GetMapping("/path")` | 声明式 vs 注解式 |
|
||||
| Handler | `func(c *gin.Context)` | `public ResponseEntity<T> method()` | Gin 更简洁 |
|
||||
| 参数绑定 | `c.ShouldBindJSON(&obj)` | `@RequestBody User user` | Gin 需手动绑定 |
|
||||
| 响应 | `c.JSON(200, data)` | `return new ResponseEntity<>(data, OK)` | Gin 更简洁 |
|
||||
| 中间件 | `r.Use(middleware)` | `@Component implements Filter` | Gin 更灵活 |
|
||||
|
||||
---
|
||||
|
||||
## 八、常见问题
|
||||
|
||||
**Q: `gin.Default()` 和 `gin.New()` 有什么区别?**
|
||||
|
||||
A:
|
||||
- `gin.Default()` = `gin.New()` + 默认中间件(Logger + Recovery)
|
||||
- 建议开发时使用 `Default()`,生产时按需选择
|
||||
|
||||
**Q: 参数绑定失败时怎么处理?**
|
||||
|
||||
A: 使用 `ShouldBindJSON` 而不是 `BindJSON`:
|
||||
```go
|
||||
if err := c.ShouldBindJSON(&user); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// 继续业务逻辑
|
||||
```
|
||||
|
||||
**Q: 如何获取请求头(Header)?**
|
||||
|
||||
A:
|
||||
```go
|
||||
r.GET("/headers", func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
contentType := c.ContentType()
|
||||
c.JSON(200, gin.H{
|
||||
"auth": authHeader,
|
||||
"type": contentType,
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、下一步
|
||||
|
||||
✅ 已掌握 Gin 基础(路由、Handler、参数绑定)
|
||||
→ 下一章:**中间件**(日志、异常捕获、CORS)
|
||||
→ 再下一章:**GORM**(数据库 ORM)
|
||||
|
||||
祝你编码愉快!🚀
|
||||
|
||||
766
src/programming/backend/go/Web开发数据库/16Gin中间件.md
Normal file
766
src/programming/backend/go/Web开发数据库/16Gin中间件.md
Normal file
@@ -0,0 +1,766 @@
|
||||
---
|
||||
title: Gin中间件
|
||||
icon: mdi:pipe
|
||||
date: 2025-12-23
|
||||
category:
|
||||
- Go
|
||||
- 后端
|
||||
- Web框架
|
||||
- 工程化
|
||||
- Gin
|
||||
tag:
|
||||
- 中间件
|
||||
- 日志
|
||||
- 异常捕获
|
||||
- CORS
|
||||
- 认证
|
||||
---
|
||||
|
||||
Gin 中间件是 Web 应用的核心机制,用于实现日志、认证、CORS、错误处理等跨切面需求。本文通过完整可运行的示例,并结合 Java Filter 对比,帮助你快速掌握中间件开发。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
---
|
||||
|
||||
# Gin 中间件详解:日志、异常捕获、CORS、认证
|
||||
|
||||
> **Java 对比**:Gin 的中间件类似 Java 的 `Filter` 或 Spring 的 `Interceptor`,但更灵活易用。
|
||||
|
||||
---
|
||||
|
||||
## 一、项目初始化
|
||||
|
||||
### 1.1 创建项目
|
||||
|
||||
```bash
|
||||
cd ~/GolandProjects
|
||||
mkdir go-gin-middleware && cd go-gin-middleware
|
||||
go mod init go-gin-middleware
|
||||
|
||||
# 安装 Gin(指定兼容版本)
|
||||
go get github.com/gin-gonic/gin@v1.9.1
|
||||
|
||||
# 安装 CORS 中间件(可选)
|
||||
go get github.com/gin-contrib/cors@v1.4.0
|
||||
```
|
||||
|
||||
> **版本说明**:
|
||||
> - 所有示例代码兼容 Go 1.18-1.22 版本
|
||||
> - Gin v1.9.1 和 CORS v1.4.0 已在 Go 1.22.2 上测试通过
|
||||
> - 如需使用最新版本:`go get -u github.com/gin-gonic/gin`(需 Go 1.23+)
|
||||
|
||||
---
|
||||
|
||||
## 二、中间件基础
|
||||
|
||||
### 2.1 编写简单中间件
|
||||
|
||||
创建 `middleware_basic.go`:
|
||||
|
||||
```bash
|
||||
# 创建文件
|
||||
nano middleware_basic.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 最小化中间件
|
||||
func MyMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 请求前处理
|
||||
fmt.Println("Before request")
|
||||
|
||||
// 继续下一个中间件/Handler
|
||||
c.Next()
|
||||
|
||||
// 响应后处理
|
||||
fmt.Println("After request")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// 使用中间件
|
||||
r.Use(MyMiddleware())
|
||||
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "Hello"})
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run middleware_basic.go
|
||||
|
||||
# 在另一个终端
|
||||
curl http://localhost:8080/
|
||||
# 控制台输出:
|
||||
# Before request
|
||||
# After request
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - Gin 中间件 ≈ `Filter.doFilter(request, response, chain)`
|
||||
> - `c.Next()` ≈ `chain.doFilter(request, response)`
|
||||
|
||||
---
|
||||
|
||||
## 三、中间件中止执行
|
||||
|
||||
### 3.1 Token 认证中间件
|
||||
|
||||
创建 `middleware_abort.go`:
|
||||
|
||||
```bash
|
||||
nano middleware_abort.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TokenAuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 检查 token
|
||||
if c.Query("token") == "" {
|
||||
c.JSON(401, gin.H{"error": "Unauthorized"})
|
||||
c.Abort() // 中止后续处理
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// 使用认证中间件
|
||||
r.Use(TokenAuthMiddleware())
|
||||
|
||||
r.GET("/protected", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "Access granted"})
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run middleware_abort.go
|
||||
|
||||
# 没有 token,被拦截
|
||||
curl http://localhost:8080/protected
|
||||
# {"error":"Unauthorized"}
|
||||
|
||||
# 带 token,通过
|
||||
curl 'http://localhost:8080/protected?token=abc123'
|
||||
# {"message":"Access granted"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、日志中间件
|
||||
|
||||
### 4.1 自定义日志中间件
|
||||
|
||||
创建 `middleware_logger.go`:
|
||||
|
||||
```bash
|
||||
nano middleware_logger.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 日志中间件
|
||||
func LoggerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 记录请求信息
|
||||
startTime := time.Now()
|
||||
method := c.Request.Method
|
||||
url := c.Request.URL.Path
|
||||
|
||||
// 继续处理
|
||||
c.Next()
|
||||
|
||||
// 记录响应信息
|
||||
statusCode := c.Writer.Status()
|
||||
duration := time.Since(startTime)
|
||||
|
||||
fmt.Printf("[%s] %s %d (%v)\n",
|
||||
method, url, statusCode, duration)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.New() // 不使用默认中间件
|
||||
|
||||
// 使用自定义日志中间件
|
||||
r.Use(LoggerMiddleware())
|
||||
r.Use(gin.Recovery())
|
||||
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "Hello"})
|
||||
})
|
||||
|
||||
r.GET("/slow", func(c *gin.Context) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
c.JSON(200, gin.H{"message": "Slow endpoint"})
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run middleware_logger.go
|
||||
|
||||
# 访问接口
|
||||
curl http://localhost:8080/
|
||||
# 控制台输出:[GET] / 200 (123.456µs)
|
||||
|
||||
curl http://localhost:8080/slow
|
||||
# 控制台输出:[GET] /slow 200 (100.234ms)
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - Gin 日志中间件 ≈ Logback 的 `Filter` + `MDC`
|
||||
> - `c.Next()` 前后处理 ≈ Filter 的 `doFilter` 前后
|
||||
|
||||
---
|
||||
|
||||
## 五、异常捕获中间件
|
||||
|
||||
### 5.1 自定义 Recovery 中间件
|
||||
|
||||
创建 `middleware_recovery.go`:
|
||||
|
||||
```bash
|
||||
nano middleware_recovery.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 自定义 Recovery 中间件
|
||||
func CustomRecovery() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// 记录错误
|
||||
fmt.Printf("Panic: %v\n", err)
|
||||
|
||||
// 返回友好的错误响应
|
||||
c.JSON(500, gin.H{
|
||||
"error": "Internal Server Error",
|
||||
"message": fmt.Sprintf("%v", err),
|
||||
})
|
||||
|
||||
// 中止处理链
|
||||
c.Abort()
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.New()
|
||||
|
||||
// 使用自定义 Recovery
|
||||
r.Use(CustomRecovery())
|
||||
r.Use(gin.Logger())
|
||||
|
||||
// 正常路由
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "OK"})
|
||||
})
|
||||
|
||||
// 故意触发 panic
|
||||
r.GET("/panic", func(c *gin.Context) {
|
||||
panic("Something went wrong!")
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run middleware_recovery.go
|
||||
|
||||
# 正常请求
|
||||
curl http://localhost:8080/
|
||||
# {"message":"OK"}
|
||||
|
||||
# 触发 panic,被捕获
|
||||
curl http://localhost:8080/panic
|
||||
# {"error":"Internal Server Error","message":"Something went wrong!"}
|
||||
# 控制台输出:Panic: Something went wrong!
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - Custom Recovery ≈ Spring 的 `@ControllerAdvice` + `@ExceptionHandler`
|
||||
> - `defer + recover()` ≈ Java 的 `try-catch`
|
||||
|
||||
---
|
||||
|
||||
## 六、认证中间件
|
||||
|
||||
### 6.1 基础认证中间件
|
||||
|
||||
创建 `middleware_auth.go`:
|
||||
|
||||
```bash
|
||||
nano middleware_auth.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 基础认证中间件
|
||||
func BasicAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
username, password, ok := c.Request.BasicAuth()
|
||||
|
||||
if !ok || username != "admin" || password != "password123" {
|
||||
c.JSON(401, gin.H{"error": "Unauthorized"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 认证成功,继续
|
||||
c.Set("username", username) // 将用户信息存储在 context 中
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// 公开路由
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "Public endpoint"})
|
||||
})
|
||||
|
||||
// 需要认证的路由
|
||||
r.GET("/protected", BasicAuth(), func(c *gin.Context) {
|
||||
username := c.GetString("username")
|
||||
c.JSON(200, gin.H{"message": "Hello " + username})
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run middleware_auth.go
|
||||
|
||||
# 无认证信息
|
||||
curl http://localhost:8080/protected
|
||||
# {"error":"Unauthorized"}
|
||||
|
||||
# 使用基础认证
|
||||
curl -u admin:password123 http://localhost:8080/protected
|
||||
# {"message":"Hello admin"}
|
||||
```
|
||||
|
||||
### 6.2 Token 认证(模拟 JWT)
|
||||
|
||||
创建 `middleware_token.go`:
|
||||
|
||||
```bash
|
||||
nano middleware_token.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Token 认证中间件
|
||||
func TokenAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 从请求头获取 token
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
|
||||
if authHeader == "" {
|
||||
c.JSON(401, gin.H{"error": "Missing Authorization header"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 验证 token 格式(Bearer xxx)
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
c.JSON(401, gin.H{"error": "Invalid Authorization header"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
token := parts[1]
|
||||
|
||||
// 验证 token 有效性(简化版,实际应使用 JWT)
|
||||
if !isValidToken(token) {
|
||||
c.JSON(401, gin.H{"error": "Invalid token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 提取用户信息(从 token)
|
||||
userID := extractUserID(token)
|
||||
c.Set("user_id", userID)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 验证 token(示例)
|
||||
func isValidToken(token string) bool {
|
||||
return token == "valid_token_123"
|
||||
}
|
||||
|
||||
// 提取用户ID(示例)
|
||||
func extractUserID(token string) string {
|
||||
return "user123"
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// 需要 token 的路由
|
||||
r.GET("/profile", TokenAuth(), func(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
c.JSON(200, gin.H{
|
||||
"user_id": userID,
|
||||
"profile": "User profile data",
|
||||
})
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run middleware_token.go
|
||||
|
||||
# 无 token - 失败
|
||||
curl http://localhost:8080/profile
|
||||
# {"error":"Missing Authorization header"}
|
||||
|
||||
# 有效 token - 成功
|
||||
curl -H "Authorization: Bearer valid_token_123" http://localhost:8080/profile
|
||||
# {"user_id":"user123","profile":"User profile data"}
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - Token Auth ≈ Spring Security 的 `OncePerRequestFilter` + JWT
|
||||
> - Bearer Token ≈ Spring Security 的 `BearerTokenAuthenticationFilter`
|
||||
|
||||
---
|
||||
|
||||
## 七、CORS 中间件
|
||||
|
||||
### 7.1 使用 gin-contrib/cors
|
||||
|
||||
创建 `middleware_cors.go`:
|
||||
|
||||
```bash
|
||||
nano middleware_cors.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-contrib/cors"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
// 配置 CORS
|
||||
r.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{"http://localhost:3000"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 12 * time.Hour,
|
||||
}))
|
||||
|
||||
r.GET("/api/data", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "CORS enabled"})
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run middleware_cors.go
|
||||
|
||||
# 测试 CORS
|
||||
curl -H "Origin: http://localhost:3000" \
|
||||
-H "Access-Control-Request-Method: GET" \
|
||||
-X OPTIONS http://localhost:8080/api/data
|
||||
```
|
||||
|
||||
### 7.2 自定义 CORS 中间件
|
||||
|
||||
创建 `middleware_cors_custom.go`:
|
||||
|
||||
```bash
|
||||
nano middleware_cors_custom.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func CORSMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.Default()
|
||||
|
||||
r.Use(CORSMiddleware())
|
||||
|
||||
r.GET("/api/data", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "Custom CORS enabled"})
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run middleware_cors_custom.go
|
||||
|
||||
curl http://localhost:8080/api/data
|
||||
# {"message":"Custom CORS enabled"}
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - CORS 中间件 ≈ Spring 的 `CorsFilter` 或 `@CrossOrigin`
|
||||
> - `AllowOrigins` ≈ `@CrossOrigin(origins = "...")`
|
||||
|
||||
---
|
||||
|
||||
## 八、中间件组合使用
|
||||
|
||||
### 8.1 完整示例
|
||||
|
||||
创建 `middleware_complete.go`:
|
||||
|
||||
```bash
|
||||
nano middleware_complete.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 日志中间件
|
||||
func Logger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
c.Next()
|
||||
duration := time.Since(start)
|
||||
fmt.Printf("[%s] %s %d (%v)\n",
|
||||
c.Request.Method, c.Request.URL.Path, c.Writer.Status(), duration)
|
||||
}
|
||||
}
|
||||
|
||||
// 认证中间件
|
||||
func Auth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token := c.GetHeader("Authorization")
|
||||
if token != "Bearer secret" {
|
||||
c.JSON(401, gin.H{"error": "Unauthorized"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Recovery 中间件
|
||||
func Recovery() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
c.JSON(500, gin.H{"error": "Internal Server Error"})
|
||||
c.Abort()
|
||||
}
|
||||
}()
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
r := gin.New()
|
||||
|
||||
// 全局中间件
|
||||
r.Use(Logger())
|
||||
r.Use(Recovery())
|
||||
|
||||
// 公开路由
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "Public"})
|
||||
})
|
||||
|
||||
// 需要认证的路由组
|
||||
auth := r.Group("/api")
|
||||
auth.Use(Auth())
|
||||
{
|
||||
auth.GET("/users", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"users": []string{"Alice", "Bob"}})
|
||||
})
|
||||
|
||||
auth.POST("/users", func(c *gin.Context) {
|
||||
c.JSON(201, gin.H{"message": "User created"})
|
||||
})
|
||||
}
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run middleware_complete.go
|
||||
|
||||
# 公开路由
|
||||
curl http://localhost:8080/
|
||||
# {"message":"Public"}
|
||||
|
||||
# 需要认证的路由 - 无 token
|
||||
curl http://localhost:8080/api/users
|
||||
# {"error":"Unauthorized"}
|
||||
|
||||
# 需要认证的路由 - 有 token
|
||||
curl -H "Authorization: Bearer secret" http://localhost:8080/api/users
|
||||
# {"users":["Alice","Bob"]}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、对比总结
|
||||
|
||||
| 概念 | Gin | Spring/Java | 说明 |
|
||||
|------|-----|------------|------|
|
||||
| 中间件 | `gin.HandlerFunc` | `Filter` / `Interceptor` | Gin 更轻量 |
|
||||
| 执行链 | `c.Next()` | `chain.doFilter()` | 类似概念 |
|
||||
| 异常处理 | `defer + recover()` | `@ExceptionHandler` | Gin 更灵活 |
|
||||
| CORS | `cors.Config` | `@CrossOrigin` | 配置方式不同 |
|
||||
| 认证 | 自定义中间件 | `Spring Security` | Gin 需手动实现 |
|
||||
|
||||
---
|
||||
|
||||
## 十、常见问题
|
||||
|
||||
**Q: 中间件的执行顺序是什么?**
|
||||
|
||||
A: 按照注册顺序执行。`c.Next()` 之前的代码按顺序执行,之后的代码逆序执行。
|
||||
|
||||
**Q: 全局中间件和路由级中间件有什么区别?**
|
||||
|
||||
A:
|
||||
- 全局中间件:`r.Use(middleware)` 对所有路由生效
|
||||
- 路由级中间件:`r.GET("/path", middleware, handler)` 仅对该路由生效
|
||||
- 分组中间件:`group.Use(middleware)` 对该分组下的所有路由生效
|
||||
|
||||
**Q: 如何在中间件之间传递数据?**
|
||||
|
||||
A: 使用 `c.Set(key, value)` 和 `c.Get(key)` 或 `c.GetString(key)`
|
||||
|
||||
---
|
||||
|
||||
## 十一、下一步
|
||||
|
||||
✅ 已掌握 Gin 中间件开发
|
||||
→ 下一章:**GORM 数据库**(ORM、CRUD、事务)
|
||||
→ 再下一章:**配置管理**(Viper)
|
||||
|
||||
祝你编码愉快!🚀
|
||||
810
src/programming/backend/go/Web开发数据库/17GORM数据库.md
Normal file
810
src/programming/backend/go/Web开发数据库/17GORM数据库.md
Normal file
@@ -0,0 +1,810 @@
|
||||
---
|
||||
title: GORM数据库
|
||||
icon: mdi:database
|
||||
date: 2025-12-23
|
||||
category:
|
||||
- Go
|
||||
- 后端
|
||||
- 数据库
|
||||
- ORM
|
||||
- GORM
|
||||
tag:
|
||||
- GORM
|
||||
- SQLite
|
||||
- MySQL
|
||||
- PostgreSQL
|
||||
- CRUD
|
||||
---
|
||||
|
||||
GORM 是 Go 最流行的 ORM 库,提供完善的数据库操作能力。本文通过完整可运行的示例,并结合 JPA/Hibernate 对比,帮助你快速掌握 GORM 使用。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
---
|
||||
|
||||
# GORM 数据库操作:模型定义、CRUD、查询
|
||||
|
||||
> **Java 对比**:GORM 类似 JPA/Hibernate,但更轻量且代码更简洁。
|
||||
|
||||
---
|
||||
|
||||
## 一、项目初始化
|
||||
|
||||
### 1.1 创建项目
|
||||
|
||||
```bash
|
||||
cd ~/GolandProjects
|
||||
mkdir go-gorm-demo && cd go-gorm-demo
|
||||
go mod init go-gorm-demo
|
||||
|
||||
# 安装 GORM 和驱动(指定兼容版本)
|
||||
go get gorm.io/gorm@v1.25.5
|
||||
go get gorm.io/driver/sqlite@v1.5.4 # SQLite
|
||||
go get gorm.io/driver/mysql@v1.5.2 # MySQL(可选)
|
||||
go get gorm.io/driver/postgres@v1.5.4 # PostgreSQL(可选)
|
||||
```
|
||||
|
||||
> **版本说明**:
|
||||
> - 所有示例代码兼容 Go 1.18-1.22 版本
|
||||
> - GORM v1.25.5 已在 Go 1.22.2 上测试通过
|
||||
> - 如需使用最新版本:`go get -u gorm.io/gorm`
|
||||
|
||||
---
|
||||
|
||||
## 二、数据库连接
|
||||
|
||||
### 2.1 连接 SQLite
|
||||
|
||||
创建 `db_connect.go`:
|
||||
|
||||
```bash
|
||||
nano db_connect.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 连接 SQLite(文件数据库)
|
||||
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic("failed to connect database")
|
||||
}
|
||||
|
||||
fmt.Println("Database connected successfully!")
|
||||
|
||||
// 获取底层的 *sql.DB
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 设置连接池
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
sqlDB.SetMaxOpenConns(100)
|
||||
|
||||
fmt.Println("Connection pool configured")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run db_connect.go
|
||||
# Database connected successfully!
|
||||
# Connection pool configured
|
||||
```
|
||||
|
||||
### 2.2 连接 MySQL(可选)
|
||||
|
||||
创建 `db_connect_mysql.go`:
|
||||
|
||||
```bash
|
||||
nano db_connect_mysql.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// MySQL DSN 格式: user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local
|
||||
dsn := "root:password@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic("failed to connect database")
|
||||
}
|
||||
|
||||
fmt.Println("MySQL connected successfully!")
|
||||
}
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - GORM 连接 ≈ JPA 的 `EntityManagerFactory`
|
||||
> - `gorm.Open()` ≈ Hibernate 的 `SessionFactory`
|
||||
|
||||
---
|
||||
|
||||
## 三、模型定义
|
||||
|
||||
### 3.1 基础模型
|
||||
|
||||
创建 `model_basic.go`:
|
||||
|
||||
```bash
|
||||
nano model_basic.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User 模型
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100;not null"`
|
||||
Email string `gorm:"size:100;unique;not null"`
|
||||
Age int `gorm:"default:0"`
|
||||
Active bool `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
|
||||
// 自动迁移(创建表)
|
||||
db.AutoMigrate(&User{})
|
||||
|
||||
fmt.Println("Table created successfully!")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run model_basic.go
|
||||
# Table created successfully!
|
||||
```
|
||||
|
||||
### 3.2 常用 Tag 说明
|
||||
|
||||
| Tag | 说明 | Java/JPA 对比 |
|
||||
|-----|------|--------------|
|
||||
| `primaryKey` | 主键 | `@Id` |
|
||||
| `autoIncrement` | 自增 | `@GeneratedValue` |
|
||||
| `size:100` | 字段长度 | `@Column(length=100)` |
|
||||
| `unique` | 唯一约束 | `@Column(unique=true)` |
|
||||
| `not null` | 非空约束 | `@Column(nullable=false)` |
|
||||
| `default:value` | 默认值 | `@Column(columnDefinition="...")` |
|
||||
| `index` | 索引 | `@Index` |
|
||||
| `-` | 忽略字段 | `@Transient` |
|
||||
|
||||
> **Java 对比**:
|
||||
> - GORM Tag ≈ JPA 注解(`@Entity`, `@Column`)
|
||||
> - `AutoMigrate` ≈ Hibernate 的 `hbm2ddl.auto=update`
|
||||
|
||||
---
|
||||
|
||||
## 四、CRUD 操作
|
||||
|
||||
### 4.1 Create - 创建数据
|
||||
|
||||
创建 `crud_create.go`:
|
||||
|
||||
```bash
|
||||
nano crud_create.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100"`
|
||||
Email string `gorm:"size:100;unique"`
|
||||
Age int
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
db.AutoMigrate(&User{})
|
||||
|
||||
// 1. 单条插入
|
||||
user := User{
|
||||
Name: "Alice",
|
||||
Email: "alice@example.com",
|
||||
Age: 30,
|
||||
}
|
||||
result := db.Create(&user)
|
||||
|
||||
if result.Error != nil {
|
||||
panic(result.Error)
|
||||
}
|
||||
|
||||
fmt.Printf("New user ID: %d\n", user.ID)
|
||||
fmt.Printf("Rows affected: %d\n", result.RowsAffected)
|
||||
|
||||
// 2. 批量插入
|
||||
users := []User{
|
||||
{Name: "Bob", Email: "bob@example.com", Age: 25},
|
||||
{Name: "Charlie", Email: "charlie@example.com", Age: 35},
|
||||
{Name: "David", Email: "david@example.com", Age: 28},
|
||||
}
|
||||
|
||||
db.Create(users)
|
||||
fmt.Println("Batch insert completed!")
|
||||
|
||||
// 3. 指定字段插入
|
||||
user2 := User{Name: "Eve", Email: "eve@example.com", Age: 22}
|
||||
db.Select("name", "email").Create(&user2) // 仅插入 name 和 email
|
||||
|
||||
fmt.Println("All users created successfully!")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run crud_create.go
|
||||
# New user ID: 1
|
||||
# Rows affected: 1
|
||||
# Batch insert completed!
|
||||
# All users created successfully!
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `db.Create()` ≈ `entityManager.persist()`
|
||||
> - 批量插入 ≈ `saveAll()`
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Read - 查询数据
|
||||
|
||||
创建 `crud_read.go`:
|
||||
|
||||
```bash
|
||||
nano crud_read.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100"`
|
||||
Email string `gorm:"size:100;unique"`
|
||||
Age int
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
|
||||
// 先插入测试数据
|
||||
db.AutoMigrate(&User{})
|
||||
db.Create(&User{Name: "Alice", Email: "alice@example.com", Age: 30})
|
||||
db.Create(&User{Name: "Bob", Email: "bob@example.com", Age: 25})
|
||||
db.Create(&User{Name: "Charlie", Email: "charlie@example.com", Age: 35})
|
||||
|
||||
// 1. 根据 ID 查询
|
||||
var user User
|
||||
db.First(&user, 1) // 查询 ID=1 的用户
|
||||
fmt.Printf("User ID=1: %s (%s)\n", user.Name, user.Email)
|
||||
|
||||
// 2. 按条件查询单条
|
||||
var user2 User
|
||||
db.Where("email = ?", "bob@example.com").First(&user2)
|
||||
fmt.Printf("User by email: %s (age=%d)\n", user2.Name, user2.Age)
|
||||
|
||||
// 3. 查询所有
|
||||
var users []User
|
||||
db.Find(&users)
|
||||
fmt.Printf("Total users: %d\n", len(users))
|
||||
|
||||
// 4. 按条件查询多条
|
||||
var adults []User
|
||||
db.Where("age >= ?", 30).Find(&adults)
|
||||
fmt.Printf("Users age >= 30: %d\n", len(adults))
|
||||
|
||||
// 5. 使用 IN 查询
|
||||
var selectedUsers []User
|
||||
db.Where("id IN ?", []int{1, 2}).Find(&selectedUsers)
|
||||
fmt.Printf("Selected users: %d\n", len(selectedUsers))
|
||||
|
||||
// 6. 排序查询
|
||||
var sortedUsers []User
|
||||
db.Order("age DESC").Find(&sortedUsers)
|
||||
fmt.Println("Users sorted by age (DESC):")
|
||||
for _, u := range sortedUsers {
|
||||
fmt.Printf(" - %s (age=%d)\n", u.Name, u.Age)
|
||||
}
|
||||
|
||||
// 7. 分页查询
|
||||
var pageUsers []User
|
||||
db.Offset(0).Limit(2).Find(&pageUsers)
|
||||
fmt.Printf("Page 1 (limit 2): %d users\n", len(pageUsers))
|
||||
|
||||
// 8. 组合查询
|
||||
var combinedUsers []User
|
||||
db.Where("age > ?", 25).
|
||||
Order("age DESC").
|
||||
Limit(2).
|
||||
Find(&combinedUsers)
|
||||
fmt.Printf("Combined query: %d users\n", len(combinedUsers))
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run crud_read.go
|
||||
# User ID=1: Alice (alice@example.com)
|
||||
# User by email: Bob (age=25)
|
||||
# Total users: 3
|
||||
# Users age >= 30: 2
|
||||
# Selected users: 2
|
||||
# Users sorted by age (DESC):
|
||||
# - Charlie (age=35)
|
||||
# - Alice (age=30)
|
||||
# - Bob (age=25)
|
||||
# Page 1 (limit 2): 2 users
|
||||
# Combined query: 2 users
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `db.Find()` ≈ `findAll()`
|
||||
> - `db.Where()` ≈ `@Query("WHERE ...")`
|
||||
> - `db.Order()` ≈ `Sort.by()`
|
||||
> - `db.Limit()` ≈ `Pageable.ofSize()`
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Update - 更新数据
|
||||
|
||||
创建 `crud_update.go`:
|
||||
|
||||
```bash
|
||||
nano crud_update.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100"`
|
||||
Email string `gorm:"size:100;unique"`
|
||||
Age int
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
db.AutoMigrate(&User{})
|
||||
|
||||
// 插入测试数据
|
||||
user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
|
||||
db.Create(&user)
|
||||
|
||||
// 1. 更新单个字段
|
||||
db.Model(&User{}).Where("id = ?", user.ID).Update("name", "Alice Updated")
|
||||
fmt.Println("Updated single field")
|
||||
|
||||
// 2. 更新多个字段(使用 map)
|
||||
db.Model(&User{}).Where("id = ?", user.ID).Updates(map[string]interface{}{
|
||||
"name": "Alice Smith",
|
||||
"age": 31,
|
||||
})
|
||||
fmt.Println("Updated multiple fields (map)")
|
||||
|
||||
// 3. 更新多个字段(使用 struct,仅更新非零值)
|
||||
db.Model(&User{}).Where("id = ?", user.ID).Updates(User{
|
||||
Name: "Alice Johnson",
|
||||
Age: 32,
|
||||
})
|
||||
fmt.Println("Updated multiple fields (struct)")
|
||||
|
||||
// 4. 强制更新零值字段
|
||||
db.Model(&User{}).Where("id = ?", user.ID).Update("age", 0)
|
||||
fmt.Println("Updated age to zero")
|
||||
|
||||
// 5. 批量更新
|
||||
db.Model(&User{}).Where("age > ?", 25).Update("age", gorm.Expr("age + ?", 1))
|
||||
fmt.Println("Batch update completed")
|
||||
|
||||
// 查询最终结果
|
||||
var finalUser User
|
||||
db.First(&finalUser, user.ID)
|
||||
fmt.Printf("Final user: %s (age=%d)\n", finalUser.Name, finalUser.Age)
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run crud_update.go
|
||||
# Updated single field
|
||||
# Updated multiple fields (map)
|
||||
# Updated multiple fields (struct)
|
||||
# Updated age to zero
|
||||
# Batch update completed
|
||||
# Final user: Alice Johnson (age=0)
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `db.Updates()` ≈ `entityManager.merge()`
|
||||
> - 批量更新 ≈ `@Modifying @Query("UPDATE ...")`
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Delete - 删除数据
|
||||
|
||||
创建 `crud_delete.go`:
|
||||
|
||||
```bash
|
||||
nano crud_delete.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100"`
|
||||
Email string `gorm:"size:100;unique"`
|
||||
Age int
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"` // 软删除字段
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
db.AutoMigrate(&User{})
|
||||
|
||||
// 插入测试数据
|
||||
users := []User{
|
||||
{Name: "Alice", Email: "alice@example.com", Age: 30},
|
||||
{Name: "Bob", Email: "bob@example.com", Age: 25},
|
||||
{Name: "Charlie", Email: "charlie@example.com", Age: 35},
|
||||
}
|
||||
db.Create(users)
|
||||
|
||||
// 1. 软删除(标记 deleted_at)
|
||||
db.Delete(&User{}, 1) // 删除 ID=1 的用户
|
||||
fmt.Println("Soft deleted user ID=1")
|
||||
|
||||
// 2. 查询时默认不包括已删除的
|
||||
var activeUsers []User
|
||||
db.Find(&activeUsers)
|
||||
fmt.Printf("Active users: %d\n", len(activeUsers))
|
||||
|
||||
// 3. 查询所有记录(包括已删除的)
|
||||
var allUsers []User
|
||||
db.Unscoped().Find(&allUsers)
|
||||
fmt.Printf("All users (including deleted): %d\n", len(allUsers))
|
||||
|
||||
// 4. 永久删除(硬删除)
|
||||
db.Unscoped().Delete(&User{}, 2)
|
||||
fmt.Println("Permanently deleted user ID=2")
|
||||
|
||||
// 5. 批量删除
|
||||
db.Where("age < ?", 30).Delete(&User{})
|
||||
fmt.Println("Batch delete completed")
|
||||
|
||||
// 最终统计
|
||||
var finalCount int64
|
||||
db.Model(&User{}).Count(&finalCount)
|
||||
fmt.Printf("Final active users: %d\n", finalCount)
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run crud_delete.go
|
||||
# Soft deleted user ID=1
|
||||
# Active users: 2
|
||||
# All users (including deleted): 3
|
||||
# Permanently deleted user ID=2
|
||||
# Batch delete completed
|
||||
# Final active users: 1
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - 软删除 ≈ 使用 `@SQLDelete` + `@Where`
|
||||
> - `DeletedAt` ≈ JPA 的逻辑删除注解
|
||||
> - 硬删除 ≈ `entityManager.remove()`
|
||||
|
||||
---
|
||||
|
||||
## 五、高级查询
|
||||
|
||||
### 5.1 复杂查询
|
||||
|
||||
创建 `query_advanced.go`:
|
||||
|
||||
```bash
|
||||
nano query_advanced.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100"`
|
||||
Email string `gorm:"size:100;unique"`
|
||||
Age int
|
||||
Active bool `gorm:"default:true"`
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
db.AutoMigrate(&User{})
|
||||
|
||||
// 插入测试数据
|
||||
users := []User{
|
||||
{Name: "Alice", Email: "alice@example.com", Age: 30, Active: true},
|
||||
{Name: "Bob", Email: "bob@example.com", Age: 25, Active: false},
|
||||
{Name: "Charlie", Email: "charlie@example.com", Age: 35, Active: true},
|
||||
{Name: "David", Email: "david@example.com", Age: 28, Active: true},
|
||||
}
|
||||
db.Create(users)
|
||||
|
||||
// 1. 多条件查询
|
||||
var result1 []User
|
||||
db.Where("age > ? AND active = ?", 25, true).Find(&result1)
|
||||
fmt.Printf("Age > 25 AND active: %d users\n", len(result1))
|
||||
|
||||
// 2. OR 查询
|
||||
var result2 []User
|
||||
db.Where("age < ? OR active = ?", 30, false).Find(&result2)
|
||||
fmt.Printf("Age < 30 OR inactive: %d users\n", len(result2))
|
||||
|
||||
// 3. 模糊查询
|
||||
var result3 []User
|
||||
db.Where("name LIKE ?", "%li%").Find(&result3)
|
||||
fmt.Printf("Name contains 'li': %d users\n", len(result3))
|
||||
|
||||
// 4. 仅查询指定字段
|
||||
var result4 []User
|
||||
db.Select("name", "email").Find(&result4)
|
||||
fmt.Printf("Selected fields: %d users\n", len(result4))
|
||||
for _, u := range result4 {
|
||||
fmt.Printf(" - %s (%s)\n", u.Name, u.Email)
|
||||
}
|
||||
|
||||
// 5. 统计查询
|
||||
var count int64
|
||||
db.Model(&User{}).Where("active = ?", true).Count(&count)
|
||||
fmt.Printf("Active users count: %d\n", count)
|
||||
|
||||
// 6. 聚合查询
|
||||
var avgAge float64
|
||||
db.Model(&User{}).Select("AVG(age)").Row().Scan(&avgAge)
|
||||
fmt.Printf("Average age: %.2f\n", avgAge)
|
||||
|
||||
// 7. 分组查询
|
||||
type Result struct {
|
||||
Active bool
|
||||
Count int64
|
||||
}
|
||||
var groupResult []Result
|
||||
db.Model(&User{}).Select("active, count(*) as count").Group("active").Scan(&groupResult)
|
||||
fmt.Println("Group by active:")
|
||||
for _, r := range groupResult {
|
||||
fmt.Printf(" Active=%v: %d users\n", r.Active, r.Count)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run query_advanced.go
|
||||
# Age > 25 AND active: 2 users
|
||||
# Age < 30 OR inactive: 2 users
|
||||
# Name contains 'li': 2 users
|
||||
# Selected fields: 4 users
|
||||
# - Alice (alice@example.com)
|
||||
# - Bob (bob@example.com)
|
||||
# - Charlie (charlie@example.com)
|
||||
# - David (david@example.com)
|
||||
# Active users count: 3
|
||||
# Average age: 29.50
|
||||
# Group by active:
|
||||
# Active=false: 1 users
|
||||
# Active=true: 3 users
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `db.Where()` ≈ JPA Criteria API 或 `@Query`
|
||||
> - `db.Count()` ≈ `countBy...`
|
||||
> - `db.Group()` ≈ JPQL 的 `GROUP BY`
|
||||
|
||||
---
|
||||
|
||||
## 六、原生 SQL
|
||||
|
||||
### 6.1 执行原生查询
|
||||
|
||||
创建 `raw_sql.go`:
|
||||
|
||||
```bash
|
||||
nano raw_sql.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100"`
|
||||
Email string `gorm:"size:100;unique"`
|
||||
Age int
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
db.AutoMigrate(&User{})
|
||||
|
||||
// 插入测试数据
|
||||
db.Create(&User{Name: "Alice", Email: "alice@example.com", Age: 30})
|
||||
db.Create(&User{Name: "Bob", Email: "bob@example.com", Age: 25})
|
||||
|
||||
// 1. 原生查询
|
||||
var users []User
|
||||
db.Raw("SELECT * FROM users WHERE age > ?", 20).Scan(&users)
|
||||
fmt.Printf("Raw query result: %d users\n", len(users))
|
||||
|
||||
// 2. 原生更新
|
||||
db.Exec("UPDATE users SET age = age + 1 WHERE name = ?", "Alice")
|
||||
fmt.Println("Raw update executed")
|
||||
|
||||
// 3. 原生统计
|
||||
var count int64
|
||||
db.Raw("SELECT COUNT(*) FROM users").Scan(&count)
|
||||
fmt.Printf("Total users: %d\n", count)
|
||||
|
||||
// 4. 查询单行
|
||||
var result struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
db.Raw("SELECT name, age FROM users WHERE id = ?", 1).Scan(&result)
|
||||
fmt.Printf("User: %s (age=%d)\n", result.Name, result.Age)
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run raw_sql.go
|
||||
# Raw query result: 2 users
|
||||
# Raw update executed
|
||||
# Total users: 2
|
||||
# User: Alice (age=31)
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `db.Raw()` ≈ `entityManager.createNativeQuery()`
|
||||
> - `db.Exec()` ≈ `@Query(nativeQuery = true)`
|
||||
|
||||
---
|
||||
|
||||
## 七、对比总结
|
||||
|
||||
| 功能 | GORM | JPA/Hibernate | 说明 |
|
||||
|------|------|--------------|------|
|
||||
| 连接数据库 | `gorm.Open()` | `EntityManagerFactory` | GORM 更简洁 |
|
||||
| 模型定义 | Struct + Tag | `@Entity` + 注解 | GORM 更轻量 |
|
||||
| 创建 | `db.Create()` | `persist()` | 语法不同 |
|
||||
| 查询 | `db.Find()` | `findAll()` | GORM 更灵活 |
|
||||
| 更新 | `db.Updates()` | `merge()` | GORM 支持链式 |
|
||||
| 删除 | `db.Delete()` | `remove()` | GORM 自带软删除 |
|
||||
| 原生SQL | `db.Raw()` | `createNativeQuery()` | 类似 |
|
||||
|
||||
---
|
||||
|
||||
## 八、常见问题
|
||||
|
||||
**Q: GORM 如何防止 SQL 注入?**
|
||||
|
||||
A: GORM 使用预编译语句(prepared statements),只要使用 `?` 占位符就是安全的。
|
||||
|
||||
**Q: 软删除和硬删除的区别?**
|
||||
|
||||
A:
|
||||
- 软删除:仅标记 `deleted_at` 字段,数据仍在数据库中
|
||||
- 硬删除:使用 `Unscoped().Delete()` 永久删除数据
|
||||
|
||||
**Q: 如何处理查询不存在的记录?**
|
||||
|
||||
A:
|
||||
```go
|
||||
if result := db.First(&user, id); result.Error != nil {
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
// 记录不存在
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、下一步
|
||||
|
||||
✅ 已掌握 GORM 基础操作
|
||||
→ 下一章:**GORM 事务与关联**(一对多、多对多)
|
||||
→ 再下一章:**配置管理**(Viper)
|
||||
|
||||
祝你编码愉快!🚀
|
||||
775
src/programming/backend/go/Web开发数据库/18GORM事务关联.md
Normal file
775
src/programming/backend/go/Web开发数据库/18GORM事务关联.md
Normal file
@@ -0,0 +1,775 @@
|
||||
---
|
||||
title: GORM事务关联
|
||||
icon: mdi:relation-many-to-many
|
||||
date: 2025-12-23
|
||||
category:
|
||||
- Go
|
||||
- 后端
|
||||
- 数据库
|
||||
- ORM
|
||||
- GORM
|
||||
tag:
|
||||
- 事务
|
||||
- 关联关系
|
||||
- 一对多
|
||||
- 多对多
|
||||
- 外键
|
||||
---
|
||||
|
||||
GORM 提供了强大的事务和关联关系支持。本文通过完整可运行的示例,并结合 JPA 对比,帮助你掌握事务处理和关联关系操作。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
---
|
||||
|
||||
# GORM 事务与关联:事务处理、一对多、多对多
|
||||
|
||||
> **Java 对比**:GORM 的事务类似 `@Transactional`,关联关系类似 `@OneToMany`、`@ManyToMany`。
|
||||
|
||||
---
|
||||
|
||||
## 一、事务处理
|
||||
|
||||
### 1.1 基础事务
|
||||
|
||||
创建 `transaction_basic.go`:
|
||||
|
||||
```bash
|
||||
nano transaction_basic.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100"`
|
||||
Email string `gorm:"size:100;unique"`
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
UserID uint
|
||||
Balance int
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
db.AutoMigrate(&User{}, &Account{})
|
||||
|
||||
// 1. 手动事务
|
||||
tx := db.Begin()
|
||||
|
||||
// 创建用户
|
||||
user := User{Name: "Alice", Email: "alice@example.com"}
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
tx.Rollback()
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 创建账户
|
||||
account := Account{UserID: user.ID, Balance: 1000}
|
||||
if err := tx.Create(&account).Error; err != nil {
|
||||
tx.Rollback()
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
tx.Commit()
|
||||
fmt.Println("Transaction committed successfully!")
|
||||
|
||||
// 2. 自动事务(推荐)
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
// 在事务中执行操作
|
||||
user2 := User{Name: "Bob", Email: "bob@example.com"}
|
||||
if err := tx.Create(&user2).Error; err != nil {
|
||||
return err // 自动回滚
|
||||
}
|
||||
|
||||
account2 := Account{UserID: user2.ID, Balance: 2000}
|
||||
if err := tx.Create(&account2).Error; err != nil {
|
||||
return err // 自动回滚
|
||||
}
|
||||
|
||||
// 返回 nil 自动提交
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Transaction failed:", err)
|
||||
} else {
|
||||
fmt.Println("Auto transaction committed successfully!")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run transaction_basic.go
|
||||
# Transaction committed successfully!
|
||||
# Auto transaction committed successfully!
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `db.Transaction()` ≈ `@Transactional`
|
||||
> - 自动回滚 ≈ Spring 的 `@Transactional(rollbackFor = Exception.class)`
|
||||
|
||||
---
|
||||
|
||||
### 1.2 事务回滚示例
|
||||
|
||||
创建 `transaction_rollback.go`:
|
||||
|
||||
```bash
|
||||
nano transaction_rollback.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100"`
|
||||
Email string `gorm:"size:100;unique"`
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
UserID uint
|
||||
Balance int
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
db.AutoMigrate(&User{}, &Account{})
|
||||
|
||||
// 故意触发错误,测试回滚
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
user := User{Name: "Charlie", Email: "charlie@example.com"}
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("User created, ID:", user.ID)
|
||||
|
||||
// 故意返回错误,触发回滚
|
||||
return errors.New("something went wrong")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Transaction rolled back:", err)
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
var count int64
|
||||
db.Model(&User{}).Where("name = ?", "Charlie").Count(&count)
|
||||
fmt.Printf("User 'Charlie' count: %d (should be 0)\n", count)
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run transaction_rollback.go
|
||||
# User created, ID: 1
|
||||
# Transaction rolled back: something went wrong
|
||||
# User 'Charlie' count: 0 (should be 0)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.3 嵌套事务(SavePoint)
|
||||
|
||||
创建 `transaction_savepoint.go`:
|
||||
|
||||
```bash
|
||||
nano transaction_savepoint.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100"`
|
||||
Email string `gorm:"size:100;unique"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
db.AutoMigrate(&User{})
|
||||
|
||||
db.Transaction(func(tx *gorm.DB) error {
|
||||
tx.Create(&User{Name: "Alice", Email: "alice@example.com"})
|
||||
|
||||
// 创建 SavePoint
|
||||
tx.SavePoint("sp1")
|
||||
tx.Create(&User{Name: "Bob", Email: "bob@example.com"})
|
||||
|
||||
// 回滚到 SavePoint
|
||||
tx.RollbackTo("sp1")
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// 检查结果
|
||||
var count int64
|
||||
db.Model(&User{}).Count(&count)
|
||||
fmt.Printf("Total users: %d (should be 1, only Alice)\n", count)
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run transaction_savepoint.go
|
||||
# Total users: 1 (should be 1, only Alice)
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - SavePoint ≈ JDBC 的 `connection.setSavepoint()`
|
||||
> - 嵌套事务 ≈ Spring 的 `PROPAGATION_NESTED`
|
||||
|
||||
---
|
||||
|
||||
## 二、一对一关联
|
||||
|
||||
### 2.1 Has One(拥有一个)
|
||||
|
||||
创建 `relation_has_one.go`:
|
||||
|
||||
```bash
|
||||
nano relation_has_one.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 用户拥有一个账户
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100"`
|
||||
Account Account // 一对一关联
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
UserID uint // 外键
|
||||
Balance int
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
db.AutoMigrate(&User{}, &Account{})
|
||||
|
||||
// 1. 创建用户和账户
|
||||
user := User{
|
||||
Name: "Alice",
|
||||
Account: Account{Balance: 1000},
|
||||
}
|
||||
db.Create(&user)
|
||||
fmt.Println("User and account created")
|
||||
|
||||
// 2. 预加载查询
|
||||
var result User
|
||||
db.Preload("Account").First(&result, user.ID)
|
||||
fmt.Printf("User: %s, Balance: %d\n", result.Name, result.Account.Balance)
|
||||
|
||||
// 3. 更新关联数据
|
||||
db.Model(&result.Account).Update("balance", 2000)
|
||||
fmt.Println("Account balance updated")
|
||||
|
||||
// 4. 删除关联(不会自动删除 Account)
|
||||
db.Delete(&result)
|
||||
|
||||
var accountCount int64
|
||||
db.Model(&Account{}).Count(&accountCount)
|
||||
fmt.Printf("Account still exists: %d\n", accountCount)
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run relation_has_one.go
|
||||
# User and account created
|
||||
# User: Alice, Balance: 1000
|
||||
# Account balance updated
|
||||
# Account still exists: 1
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `Has One` ≈ `@OneToOne`
|
||||
> - `Preload` ≈ JPA 的 `fetch = FetchType.EAGER`
|
||||
|
||||
---
|
||||
|
||||
## 三、一对多关联
|
||||
|
||||
### 3.1 Has Many(拥有多个)
|
||||
|
||||
创建 `relation_has_many.go`:
|
||||
|
||||
```bash
|
||||
nano relation_has_many.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 用户拥有多个订单
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100"`
|
||||
Orders []Order // 一对多关联
|
||||
}
|
||||
|
||||
type Order struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
UserID uint // 外键
|
||||
Product string `gorm:"size:100"`
|
||||
Amount int
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
db.AutoMigrate(&User{}, &Order{})
|
||||
|
||||
// 1. 创建用户和订单
|
||||
user := User{
|
||||
Name: "Alice",
|
||||
Orders: []Order{
|
||||
{Product: "Laptop", Amount: 1},
|
||||
{Product: "Mouse", Amount: 2},
|
||||
{Product: "Keyboard", Amount: 1},
|
||||
},
|
||||
}
|
||||
db.Create(&user)
|
||||
fmt.Println("User and orders created")
|
||||
|
||||
// 2. 预加载查询
|
||||
var result User
|
||||
db.Preload("Orders").First(&result, user.ID)
|
||||
fmt.Printf("User: %s, Orders: %d\n", result.Name, len(result.Orders))
|
||||
for _, order := range result.Orders {
|
||||
fmt.Printf(" - %s (x%d)\n", order.Product, order.Amount)
|
||||
}
|
||||
|
||||
// 3. 添加新订单
|
||||
newOrder := Order{UserID: user.ID, Product: "Monitor", Amount: 1}
|
||||
db.Create(&newOrder)
|
||||
fmt.Println("New order added")
|
||||
|
||||
// 4. 查询用户的所有订单
|
||||
var orders []Order
|
||||
db.Where("user_id = ?", user.ID).Find(&orders)
|
||||
fmt.Printf("Total orders: %d\n", len(orders))
|
||||
|
||||
// 5. 删除某个订单
|
||||
db.Delete(&Order{}, orders[0].ID)
|
||||
fmt.Println("First order deleted")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run relation_has_many.go
|
||||
# User and orders created
|
||||
# User: Alice, Orders: 3
|
||||
# - Laptop (x1)
|
||||
# - Mouse (x2)
|
||||
# - Keyboard (x1)
|
||||
# New order added
|
||||
# Total orders: 4
|
||||
# First order deleted
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `Has Many` ≈ `@OneToMany`
|
||||
> - `Orders []Order` ≈ `List<Order> orders`
|
||||
|
||||
---
|
||||
|
||||
## 四、多对多关联
|
||||
|
||||
### 4.1 Many To Many(多对多)
|
||||
|
||||
创建 `relation_many_to_many.go`:
|
||||
|
||||
```bash
|
||||
nano relation_many_to_many.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 学生和课程:多对多关系
|
||||
type Student struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100"`
|
||||
Courses []Course `gorm:"many2many:student_courses;"` // 多对多
|
||||
}
|
||||
|
||||
type Course struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100"`
|
||||
Students []Student `gorm:"many2many:student_courses;"` // 多对多
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
db.AutoMigrate(&Student{}, &Course{})
|
||||
|
||||
// 1. 创建课程
|
||||
math := Course{Name: "Math"}
|
||||
physics := Course{Name: "Physics"}
|
||||
chemistry := Course{Name: "Chemistry"}
|
||||
db.Create(&math)
|
||||
db.Create(&physics)
|
||||
db.Create(&chemistry)
|
||||
|
||||
// 2. 创建学生并关联课程
|
||||
alice := Student{
|
||||
Name: "Alice",
|
||||
Courses: []Course{math, physics},
|
||||
}
|
||||
bob := Student{
|
||||
Name: "Bob",
|
||||
Courses: []Course{physics, chemistry},
|
||||
}
|
||||
db.Create(&alice)
|
||||
db.Create(&bob)
|
||||
fmt.Println("Students and courses created")
|
||||
|
||||
// 3. 查询学生的课程
|
||||
var student1 Student
|
||||
db.Preload("Courses").First(&student1, alice.ID)
|
||||
fmt.Printf("%s's courses:\n", student1.Name)
|
||||
for _, course := range student1.Courses {
|
||||
fmt.Printf(" - %s\n", course.Name)
|
||||
}
|
||||
|
||||
// 4. 查询课程的学生
|
||||
var course1 Course
|
||||
db.Preload("Students").First(&course1, physics.ID)
|
||||
fmt.Printf("%s course students:\n", course1.Name)
|
||||
for _, student := range course1.Students {
|
||||
fmt.Printf(" - %s\n", student.Name)
|
||||
}
|
||||
|
||||
// 5. 添加关联
|
||||
db.Model(&alice).Association("Courses").Append(&chemistry)
|
||||
fmt.Println("Alice enrolled in Chemistry")
|
||||
|
||||
// 6. 删除关联
|
||||
db.Model(&bob).Association("Courses").Delete(&chemistry)
|
||||
fmt.Println("Bob dropped Chemistry")
|
||||
|
||||
// 7. 替换所有关联
|
||||
db.Model(&alice).Association("Courses").Replace(&math)
|
||||
fmt.Println("Alice now only takes Math")
|
||||
|
||||
// 8. 清空关联
|
||||
db.Model(&bob).Association("Courses").Clear()
|
||||
fmt.Println("Bob dropped all courses")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run relation_many_to_many.go
|
||||
# Students and courses created
|
||||
# Alice's courses:
|
||||
# - Math
|
||||
# - Physics
|
||||
# Physics course students:
|
||||
# - Alice
|
||||
# - Bob
|
||||
# Alice enrolled in Chemistry
|
||||
# Bob dropped Chemistry
|
||||
# Alice now only takes Math
|
||||
# Bob dropped all courses
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `many2many` ≈ `@ManyToMany`
|
||||
> - `student_courses` ≈ `@JoinTable(name = "student_courses")`
|
||||
|
||||
---
|
||||
|
||||
## 五、关联操作详解
|
||||
|
||||
### 5.1 Association 方法
|
||||
|
||||
创建 `association_methods.go`:
|
||||
|
||||
```bash
|
||||
nano association_methods.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100"`
|
||||
Orders []Order
|
||||
}
|
||||
|
||||
type Order struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
UserID uint
|
||||
Product string `gorm:"size:100"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
db.AutoMigrate(&User{}, &Order{})
|
||||
|
||||
user := User{Name: "Alice"}
|
||||
db.Create(&user)
|
||||
|
||||
order1 := Order{Product: "Laptop"}
|
||||
order2 := Order{Product: "Mouse"}
|
||||
order3 := Order{Product: "Keyboard"}
|
||||
|
||||
// 1. Append - 添加关联
|
||||
db.Model(&user).Association("Orders").Append(&order1, &order2)
|
||||
fmt.Println("Orders appended")
|
||||
|
||||
// 2. Count - 统计关联数量
|
||||
count := db.Model(&user).Association("Orders").Count()
|
||||
fmt.Printf("Order count: %d\n", count)
|
||||
|
||||
// 3. Find - 查找关联
|
||||
var orders []Order
|
||||
db.Model(&user).Association("Orders").Find(&orders)
|
||||
fmt.Println("Orders found:")
|
||||
for _, o := range orders {
|
||||
fmt.Printf(" - %s\n", o.Product)
|
||||
}
|
||||
|
||||
// 4. Replace - 替换所有关联
|
||||
db.Model(&user).Association("Orders").Replace(&order3)
|
||||
fmt.Println("Orders replaced with Keyboard")
|
||||
|
||||
// 5. Delete - 删除关联(不删除记录本身)
|
||||
db.Model(&user).Association("Orders").Delete(&order3)
|
||||
fmt.Println("Association deleted")
|
||||
|
||||
// 6. Clear - 清空所有关联
|
||||
db.Model(&user).Association("Orders").Append(&order1)
|
||||
db.Model(&user).Association("Orders").Clear()
|
||||
fmt.Println("All associations cleared")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run association_methods.go
|
||||
# Orders appended
|
||||
# Order count: 2
|
||||
# Orders found:
|
||||
# - Laptop
|
||||
# - Mouse
|
||||
# Orders replaced with Keyboard
|
||||
# Association deleted
|
||||
# All associations cleared
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、预加载策略
|
||||
|
||||
### 6.1 不同的预加载方式
|
||||
|
||||
创建 `preload_strategies.go`:
|
||||
|
||||
```bash
|
||||
nano preload_strategies.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"size:100"`
|
||||
Profile Profile
|
||||
Orders []Order
|
||||
}
|
||||
|
||||
type Profile struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
UserID uint
|
||||
Bio string `gorm:"size:200"`
|
||||
}
|
||||
|
||||
type Order struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
UserID uint
|
||||
Product string `gorm:"size:100"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
db.AutoMigrate(&User{}, &Profile{}, &Order{})
|
||||
|
||||
// 创建测试数据
|
||||
user := User{
|
||||
Name: "Alice",
|
||||
Profile: Profile{Bio: "Software Engineer"},
|
||||
Orders: []Order{
|
||||
{Product: "Laptop"},
|
||||
{Product: "Mouse"},
|
||||
},
|
||||
}
|
||||
db.Create(&user)
|
||||
|
||||
// 1. 预加载单个关联
|
||||
var user1 User
|
||||
db.Preload("Profile").First(&user1, user.ID)
|
||||
fmt.Printf("User: %s, Bio: %s\n", user1.Name, user1.Profile.Bio)
|
||||
|
||||
// 2. 预加载多个关联
|
||||
var user2 User
|
||||
db.Preload("Profile").Preload("Orders").First(&user2, user.ID)
|
||||
fmt.Printf("User: %s, Orders: %d\n", user2.Name, len(user2.Orders))
|
||||
|
||||
// 3. 预加载所有关联
|
||||
var user3 User
|
||||
db.Preload("Profile").Preload("Orders").First(&user3, user.ID)
|
||||
fmt.Printf("Fully loaded user: %s\n", user3.Name)
|
||||
|
||||
// 4. 条件预加载
|
||||
var user4 User
|
||||
db.Preload("Orders", "product = ?", "Laptop").First(&user4, user.ID)
|
||||
fmt.Printf("Filtered orders: %d\n", len(user4.Orders))
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run preload_strategies.go
|
||||
# User: Alice, Bio: Software Engineer
|
||||
# User: Alice, Orders: 2
|
||||
# Fully loaded user: Alice
|
||||
# Filtered orders: 1
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `Preload` ≈ `@EntityGraph` 或 `fetch = FetchType.EAGER`
|
||||
> - 条件预加载 ≈ `@Where` 注解
|
||||
|
||||
---
|
||||
|
||||
## 七、对比总结
|
||||
|
||||
| 功能 | GORM | JPA/Hibernate | 说明 |
|
||||
|------|------|--------------|------|
|
||||
| 事务 | `db.Transaction()` | `@Transactional` | GORM 更灵活 |
|
||||
| 一对一 | `Has One` | `@OneToOne` | 类似 |
|
||||
| 一对多 | `Has Many` | `@OneToMany` | 类似 |
|
||||
| 多对多 | `many2many` | `@ManyToMany` | GORM 自动建表 |
|
||||
| 预加载 | `Preload` | `@EntityGraph` | GORM 更简洁 |
|
||||
| 关联操作 | `Association()` | `persist(cascade)` | GORM 更灵活 |
|
||||
|
||||
---
|
||||
|
||||
## 八、常见问题
|
||||
|
||||
**Q: 如何级联删除关联数据?**
|
||||
|
||||
A: GORM 不自动级联删除,需要手动处理:
|
||||
```go
|
||||
db.Select("Orders").Delete(&user) // 删除用户及其订单
|
||||
```
|
||||
|
||||
**Q: 如何避免 N+1 查询问题?**
|
||||
|
||||
A: 使用 `Preload` 预加载关联数据:
|
||||
```go
|
||||
db.Preload("Orders").Find(&users)
|
||||
```
|
||||
|
||||
**Q: 多对多关系如何自定义中间表?**
|
||||
|
||||
A: 使用 `joinTable` 指定:
|
||||
```go
|
||||
type Student struct {
|
||||
Courses []Course `gorm:"many2many:enrollments;"`
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、下一步
|
||||
|
||||
✅ 已掌握 GORM 事务和关联关系
|
||||
→ 下一章:**Viper 配置管理**
|
||||
→ 再下一章:**Zap 日志系统**
|
||||
|
||||
祝你编码愉快!🚀
|
||||
750
src/programming/backend/go/Web开发数据库/19Viper配置管理.md
Normal file
750
src/programming/backend/go/Web开发数据库/19Viper配置管理.md
Normal file
@@ -0,0 +1,750 @@
|
||||
---
|
||||
title: Viper配置管理
|
||||
icon: mdi:cog
|
||||
date: 2025-12-23
|
||||
category:
|
||||
- Go
|
||||
- 后端
|
||||
- 配置管理
|
||||
- 工程化
|
||||
- Viper
|
||||
tag:
|
||||
- Viper
|
||||
- 配置文件
|
||||
- 环境变量
|
||||
- YAML
|
||||
- JSON
|
||||
---
|
||||
|
||||
Viper 是 Go 最流行的配置管理库,支持多种配置格式和环境变量。本文通过完整可运行的示例,并结合 Spring Boot 配置对比,帮助你快速掌握 Viper 使用。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
---
|
||||
|
||||
# Viper 配置管理:YAML、环境变量、多环境配置
|
||||
|
||||
> **Java 对比**:Viper 类似 Spring Boot 的 `application.yml` + `@ConfigurationProperties`。
|
||||
|
||||
---
|
||||
|
||||
## 一、项目初始化
|
||||
|
||||
### 1.1 创建项目
|
||||
|
||||
```bash
|
||||
cd ~/GolandProjects
|
||||
mkdir go-viper-demo && cd go-viper-demo
|
||||
go mod init go-viper-demo
|
||||
|
||||
# 安装 Viper(指定兼容版本)
|
||||
go get github.com/spf13/viper@v1.18.2
|
||||
```
|
||||
|
||||
> **版本说明**:
|
||||
> - 所有示例代码兼容 Go 1.18-1.22 版本
|
||||
> - Viper v1.18.2 已在 Go 1.22.2 上测试通过
|
||||
> - 如需使用最新版本:`go get -u github.com/spf13/viper`
|
||||
|
||||
---
|
||||
|
||||
## 二、基础配置
|
||||
|
||||
### 2.1 读取 YAML 配置
|
||||
|
||||
创建配置文件 `config.yaml`:
|
||||
|
||||
```bash
|
||||
nano config.yaml
|
||||
# 将以下配置复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```yaml
|
||||
app:
|
||||
name: MyApp
|
||||
version: 1.0.0
|
||||
port: 8080
|
||||
|
||||
database:
|
||||
host: localhost
|
||||
port: 3306
|
||||
username: root
|
||||
password: password
|
||||
dbname: testdb
|
||||
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: ""
|
||||
db: 0
|
||||
```
|
||||
|
||||
创建 `viper_basic.go`:
|
||||
|
||||
```bash
|
||||
nano viper_basic.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 设置配置文件名和路径
|
||||
viper.SetConfigName("config") // 配置文件名(不含扩展名)
|
||||
viper.SetConfigType("yaml") // 配置文件类型
|
||||
viper.AddConfigPath(".") // 配置文件路径
|
||||
|
||||
// 2. 读取配置文件
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
panic(fmt.Errorf("Fatal error config file: %s", err))
|
||||
}
|
||||
|
||||
fmt.Println("Config file loaded successfully!")
|
||||
|
||||
// 3. 读取单个配置项
|
||||
appName := viper.GetString("app.name")
|
||||
appPort := viper.GetInt("app.port")
|
||||
fmt.Printf("App Name: %s\n", appName)
|
||||
fmt.Printf("App Port: %d\n", appPort)
|
||||
|
||||
// 4. 读取嵌套配置
|
||||
dbHost := viper.GetString("database.host")
|
||||
dbPort := viper.GetInt("database.port")
|
||||
fmt.Printf("Database: %s:%d\n", dbHost, dbPort)
|
||||
|
||||
// 5. 读取所有配置
|
||||
allSettings := viper.AllSettings()
|
||||
fmt.Printf("All settings: %v\n", allSettings)
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run viper_basic.go
|
||||
# Config file loaded successfully!
|
||||
# App Name: MyApp
|
||||
# App Port: 8080
|
||||
# Database: localhost:3306
|
||||
# All settings: map[app:map[name:MyApp port:8080 version:1.0.0] database:map[dbname:testdb host:localhost password:password port:3306 username:root] redis:map[db:0 host:localhost password: port:6379]]
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `viper.GetString()` ≈ `@Value("${app.name}")`
|
||||
> - YAML 配置 ≈ Spring Boot 的 `application.yml`
|
||||
|
||||
---
|
||||
|
||||
## 三、结构体绑定
|
||||
|
||||
### 3.1 将配置映射到结构体
|
||||
|
||||
创建 `viper_struct.go`:
|
||||
|
||||
```bash
|
||||
nano viper_struct.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// 配置结构体
|
||||
type Config struct {
|
||||
App AppConfig `mapstructure:"app"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
Redis RedisConfig `mapstructure:"redis"`
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Version string `mapstructure:"version"`
|
||||
Port int `mapstructure:"port"`
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
DBName string `mapstructure:"dbname"`
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Password string `mapstructure:"password"`
|
||||
DB int `mapstructure:"db"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 读取配置文件
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(".")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 将配置绑定到结构体
|
||||
var config Config
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 使用配置
|
||||
fmt.Printf("App: %s v%s\n", config.App.Name, config.App.Version)
|
||||
fmt.Printf("Port: %d\n", config.App.Port)
|
||||
fmt.Printf("Database: %s@%s:%d/%s\n",
|
||||
config.Database.Username,
|
||||
config.Database.Host,
|
||||
config.Database.Port,
|
||||
config.Database.DBName,
|
||||
)
|
||||
fmt.Printf("Redis: %s:%d (DB %d)\n",
|
||||
config.Redis.Host,
|
||||
config.Redis.Port,
|
||||
config.Redis.DB,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run viper_struct.go
|
||||
# App: MyApp v1.0.0
|
||||
# Port: 8080
|
||||
# Database: root@localhost:3306/testdb
|
||||
# Redis: localhost:6379 (DB 0)
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `viper.Unmarshal()` ≈ `@ConfigurationProperties(prefix = "app")`
|
||||
> - `mapstructure` tag ≈ `@Value` 注解
|
||||
|
||||
---
|
||||
|
||||
## 四、环境变量
|
||||
|
||||
### 4.1 绑定环境变量
|
||||
|
||||
创建 `viper_env.go`:
|
||||
|
||||
```bash
|
||||
nano viper_env.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 设置环境变量前缀
|
||||
viper.SetEnvPrefix("MYAPP") // 自动添加前缀
|
||||
|
||||
// 2. 绑定环境变量
|
||||
viper.BindEnv("port") // 绑定 MYAPP_PORT
|
||||
viper.BindEnv("debug") // 绑定 MYAPP_DEBUG
|
||||
viper.BindEnv("db.host") // 绑定 MYAPP_DB_HOST
|
||||
|
||||
// 3. 设置默认值
|
||||
viper.SetDefault("port", 8080)
|
||||
viper.SetDefault("debug", false)
|
||||
|
||||
// 4. 设置环境变量(模拟)
|
||||
os.Setenv("MYAPP_PORT", "9000")
|
||||
os.Setenv("MYAPP_DEBUG", "true")
|
||||
os.Setenv("MYAPP_DB_HOST", "192.168.1.100")
|
||||
|
||||
// 5. 读取配置(优先级:环境变量 > 配置文件 > 默认值)
|
||||
port := viper.GetInt("port")
|
||||
debug := viper.GetBool("debug")
|
||||
dbHost := viper.GetString("db.host")
|
||||
|
||||
fmt.Printf("Port: %d (from env)\n", port)
|
||||
fmt.Printf("Debug: %v (from env)\n", debug)
|
||||
fmt.Printf("DB Host: %s (from env)\n", dbHost)
|
||||
|
||||
// 6. 自动绑定所有环境变量
|
||||
viper.AutomaticEnv()
|
||||
|
||||
os.Setenv("MYAPP_CUSTOM_KEY", "custom_value")
|
||||
customKey := viper.GetString("custom.key") // 自动转换 CUSTOM_KEY -> custom.key
|
||||
fmt.Printf("Custom Key: %s\n", customKey)
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run viper_env.go
|
||||
# Port: 9000 (from env)
|
||||
# Debug: true (from env)
|
||||
# DB Host: 192.168.1.100 (from env)
|
||||
# Custom Key: custom_value
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `viper.BindEnv()` ≈ `${ENV_VAR}`
|
||||
> - `viper.AutomaticEnv()` ≈ Spring Boot 的自动环境变量绑定
|
||||
|
||||
---
|
||||
|
||||
## 五、多环境配置
|
||||
|
||||
### 5.1 根据环境加载不同配置
|
||||
|
||||
创建配置文件:
|
||||
|
||||
**config.dev.yaml**(开发环境):
|
||||
|
||||
```bash
|
||||
nano config.dev.yaml
|
||||
# 将以下配置复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```yaml
|
||||
app:
|
||||
name: MyApp-Dev
|
||||
port: 8080
|
||||
debug: true
|
||||
|
||||
database:
|
||||
host: localhost
|
||||
port: 3306
|
||||
username: root
|
||||
password: password
|
||||
dbname: dev_db
|
||||
```
|
||||
|
||||
**config.prod.yaml**(生产环境):
|
||||
|
||||
```bash
|
||||
nano config.prod.yaml
|
||||
# 将以下配置复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```yaml
|
||||
app:
|
||||
name: MyApp-Prod
|
||||
port: 80
|
||||
debug: false
|
||||
|
||||
database:
|
||||
host: prod.database.com
|
||||
port: 3306
|
||||
username: prod_user
|
||||
password: prod_password
|
||||
dbname: prod_db
|
||||
```
|
||||
|
||||
创建 `viper_multi_env.go`:
|
||||
|
||||
```bash
|
||||
nano viper_multi_env.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
App struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Port int `mapstructure:"port"`
|
||||
Debug bool `mapstructure:"debug"`
|
||||
} `mapstructure:"app"`
|
||||
Database struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
DBName string `mapstructure:"dbname"`
|
||||
} `mapstructure:"database"`
|
||||
}
|
||||
|
||||
func LoadConfig(env string) (*Config, error) {
|
||||
// 根据环境加载配置文件
|
||||
configName := fmt.Sprintf("config.%s", env)
|
||||
|
||||
viper.SetConfigName(configName)
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(".")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 从环境变量读取环境名称
|
||||
env := os.Getenv("APP_ENV")
|
||||
if env == "" {
|
||||
env = "dev" // 默认开发环境
|
||||
}
|
||||
|
||||
fmt.Printf("Loading config for environment: %s\n", env)
|
||||
|
||||
config, err := LoadConfig(env)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("App: %s\n", config.App.Name)
|
||||
fmt.Printf("Port: %d\n", config.App.Port)
|
||||
fmt.Printf("Debug: %v\n", config.App.Debug)
|
||||
fmt.Printf("Database: %s@%s:%d/%s\n",
|
||||
config.Database.Username,
|
||||
config.Database.Host,
|
||||
config.Database.Port,
|
||||
config.Database.DBName,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
# 开发环境
|
||||
APP_ENV=dev go run viper_multi_env.go
|
||||
# Loading config for environment: dev
|
||||
# App: MyApp-Dev
|
||||
# Port: 8080
|
||||
# Debug: true
|
||||
# Database: root@localhost:3306/dev_db
|
||||
|
||||
# 生产环境
|
||||
APP_ENV=prod go run viper_multi_env.go
|
||||
# Loading config for environment: prod
|
||||
# App: MyApp-Prod
|
||||
# Port: 80
|
||||
# Debug: false
|
||||
# Database: prod_user@prod.database.com:3306/prod_db
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - 多环境配置 ≈ Spring Boot 的 `application-{profile}.yml`
|
||||
> - `APP_ENV` ≈ `spring.profiles.active`
|
||||
|
||||
---
|
||||
|
||||
## 六、配置热重载
|
||||
|
||||
### 6.1 监听配置文件变化
|
||||
|
||||
创建 `viper_watch.go`:
|
||||
|
||||
```bash
|
||||
nano viper_watch.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 读取配置文件
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(".")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Println("Initial config loaded")
|
||||
fmt.Printf("App Name: %s\n", viper.GetString("app.name"))
|
||||
fmt.Printf("App Port: %d\n", viper.GetInt("app.port"))
|
||||
|
||||
// 监听配置文件变化
|
||||
viper.WatchConfig()
|
||||
viper.OnConfigChange(func(e interface{}) {
|
||||
fmt.Println("\n--- Config file changed! ---")
|
||||
fmt.Printf("App Name: %s\n", viper.GetString("app.name"))
|
||||
fmt.Printf("App Port: %d\n", viper.GetInt("app.port"))
|
||||
})
|
||||
|
||||
fmt.Println("\nWatching for config changes... (modify config.yaml to see changes)")
|
||||
fmt.Println("Press Ctrl+C to exit")
|
||||
|
||||
// 保持程序运行
|
||||
select {}
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run viper_watch.go
|
||||
# Initial config loaded
|
||||
# App Name: MyApp
|
||||
# App Port: 8080
|
||||
#
|
||||
# Watching for config changes... (modify config.yaml to see changes)
|
||||
# Press Ctrl+C to exit
|
||||
|
||||
# 修改 config.yaml 后自动输出:
|
||||
# --- Config file changed! ---
|
||||
# App Name: MyApp-Updated
|
||||
# App Port: 9000
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `viper.WatchConfig()` ≈ Spring Cloud Config 的 `@RefreshScope`
|
||||
> - 热重载 ≈ Spring Boot DevTools
|
||||
|
||||
---
|
||||
|
||||
## 七、支持的配置格式
|
||||
|
||||
### 7.1 JSON 配置
|
||||
|
||||
创建 `config.json`:
|
||||
|
||||
```bash
|
||||
nano config.json
|
||||
# 将以下配置复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"app": {
|
||||
"name": "MyApp",
|
||||
"port": 8080
|
||||
},
|
||||
"database": {
|
||||
"host": "localhost",
|
||||
"port": 3306
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
创建 `viper_json.go`:
|
||||
|
||||
```bash
|
||||
nano viper_json.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("json")
|
||||
viper.AddConfigPath(".")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("App Name: %s\n", viper.GetString("app.name"))
|
||||
fmt.Printf("App Port: %d\n", viper.GetInt("app.port"))
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 TOML 配置
|
||||
|
||||
创建 `config.toml`:
|
||||
|
||||
```bash
|
||||
nano config.toml
|
||||
# 将以下配置复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```toml
|
||||
[app]
|
||||
name = "MyApp"
|
||||
port = 8080
|
||||
|
||||
[database]
|
||||
host = "localhost"
|
||||
port = 3306
|
||||
```
|
||||
|
||||
创建 `viper_toml.go`:
|
||||
|
||||
```bash
|
||||
nano viper_toml.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("toml")
|
||||
viper.AddConfigPath(".")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("App Name: %s\n", viper.GetString("app.name"))
|
||||
fmt.Printf("App Port: %d\n", viper.GetInt("app.port"))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、配置优先级
|
||||
|
||||
Viper 的配置优先级(从高到低):
|
||||
|
||||
1. **显式设置**:`viper.Set()`
|
||||
2. **命令行参数**:`viper.BindPFlag()`
|
||||
3. **环境变量**:`viper.BindEnv()`
|
||||
4. **配置文件**:`viper.ReadInConfig()`
|
||||
5. **Key/Value 存储**:etcd, Consul
|
||||
6. **默认值**:`viper.SetDefault()`
|
||||
|
||||
创建 `viper_priority.go`:
|
||||
|
||||
```bash
|
||||
nano viper_priority.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 设置默认值(优先级最低)
|
||||
viper.SetDefault("port", 8080)
|
||||
fmt.Printf("Default: %d\n", viper.GetInt("port"))
|
||||
|
||||
// 2. 读取配置文件(优先级中等)
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(".")
|
||||
viper.ReadInConfig()
|
||||
fmt.Printf("From config file: %d\n", viper.GetInt("app.port"))
|
||||
|
||||
// 3. 环境变量(优先级较高)
|
||||
viper.BindEnv("port")
|
||||
os.Setenv("PORT", "9000")
|
||||
fmt.Printf("From env: %d\n", viper.GetInt("port"))
|
||||
|
||||
// 4. 显式设置(优先级最高)
|
||||
viper.Set("port", 10000)
|
||||
fmt.Printf("Explicit set: %d\n", viper.GetInt("port"))
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run viper_priority.go
|
||||
# Default: 8080
|
||||
# From config file: 8080
|
||||
# From env: 9000
|
||||
# Explicit set: 10000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、对比总结
|
||||
|
||||
| 功能 | Viper | Spring Boot | 说明 |
|
||||
|------|-------|------------|------|
|
||||
| 配置文件 | YAML/JSON/TOML | YAML/Properties | Viper 支持更多格式 |
|
||||
| 结构体绑定 | `Unmarshal` | `@ConfigurationProperties` | 类似 |
|
||||
| 环境变量 | `BindEnv` | `${ENV_VAR}` | Viper 更灵活 |
|
||||
| 多环境 | 多配置文件 | Profiles | 实现方式不同 |
|
||||
| 热重载 | `WatchConfig` | `@RefreshScope` | Viper 内置支持 |
|
||||
| 优先级 | 6 层优先级 | 多层覆盖 | 类似 |
|
||||
|
||||
---
|
||||
|
||||
## 十、常见问题
|
||||
|
||||
**Q: 如何读取数组/切片配置?**
|
||||
|
||||
A:
|
||||
```go
|
||||
// config.yaml: items: [a, b, c]
|
||||
items := viper.GetStringSlice("items")
|
||||
```
|
||||
|
||||
**Q: 如何设置配置文件的多个搜索路径?**
|
||||
|
||||
A:
|
||||
```go
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("./config")
|
||||
viper.AddConfigPath("/etc/myapp")
|
||||
```
|
||||
|
||||
**Q: 如何判断配置项是否存在?**
|
||||
|
||||
A:
|
||||
```go
|
||||
if viper.IsSet("app.port") {
|
||||
port := viper.GetInt("app.port")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十一、下一步
|
||||
|
||||
✅ 已掌握 Viper 配置管理
|
||||
→ 下一章:**Zap 日志系统**
|
||||
→ 再下一章:**综合实战项目**
|
||||
|
||||
祝你编码愉快!🚀
|
||||
673
src/programming/backend/go/Web开发数据库/20Zap日志.md
Normal file
673
src/programming/backend/go/Web开发数据库/20Zap日志.md
Normal file
@@ -0,0 +1,673 @@
|
||||
---
|
||||
title: Zap日志
|
||||
icon: mdi:file-document-edit
|
||||
date: 2025-12-23
|
||||
category:
|
||||
- Go
|
||||
- 后端
|
||||
- 日志
|
||||
- 工程化
|
||||
- Zap
|
||||
tag:
|
||||
- Zap
|
||||
- 日志
|
||||
- 结构化日志
|
||||
- 日志轮转
|
||||
- 性能优化
|
||||
---
|
||||
|
||||
Zap 是 Uber 开源的高性能结构化日志库。本文通过完整可运行的示例,并结合 Logback/SLF4J 对比,帮助你快速掌握 Zap 使用。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
---
|
||||
|
||||
# Zap 日志系统:结构化日志、日志轮转、性能优化
|
||||
|
||||
> **Java 对比**:Zap 类似 Logback + SLF4J,但性能更高,配置更灵活。
|
||||
|
||||
---
|
||||
|
||||
## 一、项目初始化
|
||||
|
||||
### 1.1 创建项目
|
||||
|
||||
```bash
|
||||
cd ~/GolandProjects
|
||||
mkdir go-zap-demo && cd go-zap-demo
|
||||
go mod init go-zap-demo
|
||||
|
||||
# 安装 Zap(指定兼容版本)
|
||||
go get go.uber.org/zap@v1.26.0
|
||||
|
||||
# 安装日志转转库(可选)
|
||||
go get gopkg.in/natefinch/lumberjack.v2
|
||||
```
|
||||
|
||||
> **版本说明**:
|
||||
> - 所有示例代码兼容 Go 1.18-1.22 版本
|
||||
> - Zap v1.26.0 已在 Go 1.22.2 上测试通过
|
||||
> - 如需使用最新版本:`go get -u go.uber.org/zap`
|
||||
|
||||
---
|
||||
|
||||
## 二、基础使用
|
||||
|
||||
### 2.1 快速开始
|
||||
|
||||
创建 `zap_basic.go`:
|
||||
|
||||
```bash
|
||||
nano zap_basic.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 创建默认 Logger(开发模式)
|
||||
logger, _ := zap.NewDevelopment()
|
||||
defer logger.Sync() // 刷新缓冲区
|
||||
|
||||
// 2. 基础日志
|
||||
logger.Info("Hello Zap!")
|
||||
logger.Warn("This is a warning")
|
||||
logger.Error("This is an error")
|
||||
|
||||
// 3. 结构化日志(带字段)
|
||||
logger.Info("User logged in",
|
||||
zap.String("username", "alice"),
|
||||
zap.Int("user_id", 123),
|
||||
zap.Bool("is_admin", true),
|
||||
)
|
||||
|
||||
// 4. 不同日志级别
|
||||
logger.Debug("Debug message")
|
||||
logger.Info("Info message")
|
||||
logger.Warn("Warn message")
|
||||
logger.Error("Error message")
|
||||
// logger.Fatal("Fatal message") // 会退出程序
|
||||
// logger.Panic("Panic message") // 会触发 panic
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run zap_basic.go
|
||||
# 2025-12-23T10:00:00.123+0800 INFO zap_basic.go:11 Hello Zap!
|
||||
# 2025-12-23T10:00:00.124+0800 WARN zap_basic.go:12 This is a warning
|
||||
# 2025-12-23T10:00:00.125+0800 ERROR zap_basic.go:13 This is an error
|
||||
# 2025-12-23T10:00:00.126+0800 INFO zap_basic.go:16 User logged in {"username": "alice", "user_id": 123, "is_admin": true}
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `zap.NewDevelopment()` ≈ Logback 的开发配置
|
||||
> - `zap.String()` ≈ SLF4J 的 `{}` 占位符
|
||||
|
||||
---
|
||||
|
||||
### 2.2 生产模式 Logger
|
||||
|
||||
创建 `zap_production.go`:
|
||||
|
||||
```bash
|
||||
nano zap_production.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建生产模式 Logger(JSON 格式,性能更高)
|
||||
logger, _ := zap.NewProduction()
|
||||
defer logger.Sync()
|
||||
|
||||
logger.Info("Application started",
|
||||
zap.String("version", "1.0.0"),
|
||||
zap.Int("port", 8080),
|
||||
)
|
||||
|
||||
logger.Error("Failed to process request",
|
||||
zap.String("method", "GET"),
|
||||
zap.String("path", "/api/users"),
|
||||
zap.Int("status_code", 500),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run zap_production.go
|
||||
# {"level":"info","ts":1703308800.123,"caller":"zap_production.go:10","msg":"Application started","version":"1.0.0","port":8080}
|
||||
# {"level":"error","ts":1703308800.124,"caller":"zap_production.go:15","msg":"Failed to process request","method":"GET","path":"/api/users","status_code":500}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、自定义配置
|
||||
|
||||
### 3.1 自定义 Logger 配置
|
||||
|
||||
创建 `zap_custom.go`:
|
||||
|
||||
```bash
|
||||
nano zap_custom.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 自定义配置
|
||||
config := zap.Config{
|
||||
Level: zap.NewAtomicLevelAt(zap.InfoLevel), // 日志级别
|
||||
Development: false, // 生产模式
|
||||
Encoding: "json", // 输出格式:json/console
|
||||
OutputPaths: []string{"stdout", "app.log"}, // 输出到控制台和文件
|
||||
ErrorOutputPaths: []string{"stderr"},
|
||||
EncoderConfig: zapcore.EncoderConfig{
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.LowercaseLevelEncoder, // 小写级别名
|
||||
EncodeTime: zapcore.ISO8601TimeEncoder, // ISO8601 时间格式
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder, // 短路径
|
||||
},
|
||||
}
|
||||
|
||||
// 2. 创建 Logger
|
||||
logger, _ := config.Build()
|
||||
defer logger.Sync()
|
||||
|
||||
// 3. 使用 Logger
|
||||
logger.Info("Custom logger initialized",
|
||||
zap.String("env", "production"),
|
||||
zap.Int("workers", 10),
|
||||
)
|
||||
|
||||
logger.Warn("Low disk space",
|
||||
zap.Int64("available_mb", 500),
|
||||
zap.Int64("threshold_mb", 1000),
|
||||
)
|
||||
|
||||
logger.Error("Database connection failed",
|
||||
zap.String("host", "localhost"),
|
||||
zap.Int("port", 3306),
|
||||
zap.Error(nil),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run zap_custom.go
|
||||
# {"level":"info","time":"2025-12-23T10:00:00.123+0800","caller":"zap_custom.go:38","msg":"Custom logger initialized","env":"production","workers":10}
|
||||
# {"level":"warn","time":"2025-12-23T10:00:00.124+0800","caller":"zap_custom.go:43","msg":"Low disk space","available_mb":500,"threshold_mb":1000}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、日志轮转
|
||||
|
||||
### 4.1 使用 Lumberjack 实现日志轮转
|
||||
|
||||
创建 `zap_rotate.go`:
|
||||
|
||||
```bash
|
||||
nano zap_rotate.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 配置日志轮转
|
||||
logRotate := &lumberjack.Logger{
|
||||
Filename: "./logs/app.log", // 日志文件路径
|
||||
MaxSize: 10, // 每个日志文件最大 10MB
|
||||
MaxBackups: 5, // 保留最近 5 个备份
|
||||
MaxAge: 30, // 保留 30 天
|
||||
Compress: true, // 压缩旧日志
|
||||
}
|
||||
|
||||
// 2. 创建 WriteSyncer
|
||||
writeSyncer := zapcore.AddSync(logRotate)
|
||||
|
||||
// 3. 配置 Encoder
|
||||
encoderConfig := zap.NewProductionEncoderConfig()
|
||||
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||
|
||||
// 4. 创建 Core
|
||||
core := zapcore.NewCore(
|
||||
zapcore.NewJSONEncoder(encoderConfig),
|
||||
writeSyncer,
|
||||
zap.InfoLevel,
|
||||
)
|
||||
|
||||
// 5. 创建 Logger
|
||||
logger := zap.New(core, zap.AddCaller())
|
||||
defer logger.Sync()
|
||||
|
||||
// 6. 使用 Logger
|
||||
for i := 0; i < 100; i++ {
|
||||
logger.Info("Processing request",
|
||||
zap.Int("request_id", i),
|
||||
zap.String("method", "GET"),
|
||||
zap.String("path", "/api/users"),
|
||||
)
|
||||
}
|
||||
|
||||
logger.Info("Log rotation configured successfully")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run zap_rotate.go
|
||||
# 日志写入到 ./logs/app.log
|
||||
# 当文件大小超过 10MB 时自动轮转
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - Lumberjack ≈ Logback 的 `RollingFileAppender`
|
||||
> - `MaxSize` ≈ `maxFileSize`
|
||||
> - `MaxBackups` ≈ `maxHistory`
|
||||
|
||||
---
|
||||
|
||||
## 五、多输出目标
|
||||
|
||||
### 5.1 同时输出到控制台和文件
|
||||
|
||||
创建 `zap_multi_output.go`:
|
||||
|
||||
```bash
|
||||
nano zap_multi_output.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 配置 Encoder
|
||||
encoderConfig := zap.NewProductionEncoderConfig()
|
||||
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder // 带颜色的级别
|
||||
|
||||
// 2. 控制台输出(console 格式)
|
||||
consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig)
|
||||
consoleOutput := zapcore.AddSync(os.Stdout)
|
||||
|
||||
// 3. 文件输出(json 格式)
|
||||
fileEncoder := zapcore.NewJSONEncoder(encoderConfig)
|
||||
file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
fileOutput := zapcore.AddSync(file)
|
||||
|
||||
// 4. 创建多输出 Core
|
||||
core := zapcore.NewTee(
|
||||
zapcore.NewCore(consoleEncoder, consoleOutput, zap.InfoLevel),
|
||||
zapcore.NewCore(fileEncoder, fileOutput, zap.InfoLevel),
|
||||
)
|
||||
|
||||
// 5. 创建 Logger
|
||||
logger := zap.New(core, zap.AddCaller())
|
||||
defer logger.Sync()
|
||||
|
||||
// 6. 使用 Logger
|
||||
logger.Info("Application started")
|
||||
logger.Warn("This is a warning")
|
||||
logger.Error("This is an error")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run zap_multi_output.go
|
||||
# 控制台输出(带颜色):
|
||||
# 2025-12-23T10:00:00.123+0800 INFO zap_multi_output.go:38 Application started
|
||||
#
|
||||
# app.log 文件内容(JSON):
|
||||
# {"level":"INFO","time":"2025-12-23T10:00:00.123+0800","caller":"zap_multi_output.go:38","msg":"Application started"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、Sugar Logger(便捷日志)
|
||||
|
||||
### 6.1 使用 Sugar Logger
|
||||
|
||||
创建 `zap_sugar.go`:
|
||||
|
||||
```bash
|
||||
nano zap_sugar.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger, _ := zap.NewProduction()
|
||||
defer logger.Sync()
|
||||
|
||||
// 1. 获取 Sugar Logger
|
||||
sugar := logger.Sugar()
|
||||
|
||||
// 2. 使用格式化字符串(类似 fmt.Printf)
|
||||
sugar.Infof("User %s logged in with ID %d", "alice", 123)
|
||||
sugar.Warnf("Low disk space: %d MB available", 500)
|
||||
sugar.Errorf("Failed to connect to %s:%d", "localhost", 3306)
|
||||
|
||||
// 3. 使用键值对
|
||||
sugar.Infow("User logged in",
|
||||
"username", "alice",
|
||||
"user_id", 123,
|
||||
"is_admin", true,
|
||||
)
|
||||
|
||||
// 4. 简洁日志
|
||||
sugar.Info("Simple info message")
|
||||
sugar.Warn("Simple warn message")
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run zap_sugar.go
|
||||
# {"level":"info","ts":1703308800.123,"caller":"zap_sugar.go:15","msg":"User alice logged in with ID 123"}
|
||||
# {"level":"warn","ts":1703308800.124,"caller":"zap_sugar.go:16","msg":"Low disk space: 500 MB available"}
|
||||
# {"level":"info","ts":1703308800.125,"caller":"zap_sugar.go:19","msg":"User logged in","username":"alice","user_id":123,"is_admin":true}
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - Sugar Logger ≈ SLF4J 的 `logger.info("{}", value)`
|
||||
> - `Infof` ≈ `logger.info(String.format(...))`
|
||||
|
||||
---
|
||||
|
||||
## 七、日志级别动态调整
|
||||
|
||||
### 7.1 运行时调整日志级别
|
||||
|
||||
创建 `zap_dynamic_level.go`:
|
||||
|
||||
```bash
|
||||
nano zap_dynamic_level.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 创建可调整的日志级别
|
||||
atom := zap.NewAtomicLevelAt(zap.InfoLevel)
|
||||
|
||||
// 2. 配置 Logger
|
||||
config := zap.Config{
|
||||
Level: atom,
|
||||
Development: false,
|
||||
Encoding: "json",
|
||||
OutputPaths: []string{"stdout"},
|
||||
ErrorOutputPaths: []string{"stderr"},
|
||||
EncoderConfig: zap.NewProductionEncoderConfig(),
|
||||
}
|
||||
|
||||
logger, _ := config.Build()
|
||||
defer logger.Sync()
|
||||
|
||||
// 3. 初始日志级别为 Info
|
||||
logger.Debug("Debug message - 1") // 不会输出
|
||||
logger.Info("Info message - 1") // 会输出
|
||||
|
||||
// 4. 动态调整为 Debug 级别
|
||||
time.Sleep(1 * time.Second)
|
||||
atom.SetLevel(zap.DebugLevel)
|
||||
logger.Info("Log level changed to Debug")
|
||||
|
||||
logger.Debug("Debug message - 2") // 现在会输出
|
||||
logger.Info("Info message - 2") // 会输出
|
||||
|
||||
// 5. 动态调整为 Warn 级别
|
||||
time.Sleep(1 * time.Second)
|
||||
atom.SetLevel(zap.WarnLevel)
|
||||
logger.Info("Log level changed to Warn")
|
||||
|
||||
logger.Info("Info message - 3") // 不会输出
|
||||
logger.Warn("Warn message - 3") // 会输出
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run zap_dynamic_level.go
|
||||
# {"level":"info","ts":1703308800.123,"msg":"Info message - 1"}
|
||||
# {"level":"info","ts":1703308801.124,"msg":"Log level changed to Debug"}
|
||||
# {"level":"debug","ts":1703308801.125,"msg":"Debug message - 2"}
|
||||
# {"level":"info","ts":1703308801.126,"msg":"Info message - 2"}
|
||||
# {"level":"warn","ts":1703308802.127,"msg":"Warn message - 3"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、全局 Logger
|
||||
|
||||
### 8.1 设置全局 Logger
|
||||
|
||||
创建 `zap_global.go`:
|
||||
|
||||
```bash
|
||||
nano zap_global.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 创建 Logger
|
||||
logger, _ := zap.NewProduction()
|
||||
defer logger.Sync()
|
||||
|
||||
// 2. 设置为全局 Logger
|
||||
zap.ReplaceGlobals(logger)
|
||||
|
||||
// 3. 在任何地方使用全局 Logger
|
||||
doSomething()
|
||||
doAnotherThing()
|
||||
}
|
||||
|
||||
func doSomething() {
|
||||
// 直接使用全局 Logger
|
||||
zap.L().Info("Doing something",
|
||||
zap.String("function", "doSomething"),
|
||||
)
|
||||
}
|
||||
|
||||
func doAnotherThing() {
|
||||
// 使用全局 Sugar Logger
|
||||
zap.S().Infow("Doing another thing",
|
||||
"function", "doAnotherThing",
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 运行 & 测试
|
||||
|
||||
```bash
|
||||
go run zap_global.go
|
||||
# {"level":"info","ts":1703308800.123,"caller":"zap_global.go:20","msg":"Doing something","function":"doSomething"}
|
||||
# {"level":"info","ts":1703308800.124,"caller":"zap_global.go:27","msg":"Doing another thing","function":"doAnotherThing"}
|
||||
```
|
||||
|
||||
> **Java 对比**:
|
||||
> - `zap.ReplaceGlobals()` ≈ SLF4J 的 `LoggerFactory.getLogger()`
|
||||
> - `zap.L()` ≈ 静态 Logger 实例
|
||||
|
||||
---
|
||||
|
||||
## 九、性能优化
|
||||
|
||||
### 9.1 性能对比
|
||||
|
||||
Zap 相比其他 Go 日志库的性能优势:
|
||||
|
||||
| 操作 | Zap | Logrus | 标准库 log |
|
||||
|------|-----|--------|-----------|
|
||||
| 结构化日志(单线程) | 6000 ns/op | 40000 ns/op | N/A |
|
||||
| 结构化日志(多线程) | 700 ns/op | 25000 ns/op | N/A |
|
||||
| 纯文本日志 | 200 ns/op | 10000 ns/op | 5000 ns/op |
|
||||
|
||||
### 9.2 性能优化建议
|
||||
|
||||
创建 `zap_performance.go`:
|
||||
|
||||
```bash
|
||||
nano zap_performance.go
|
||||
# 将以下代码复制粘贴到文件中,然后按 Ctrl+O 保存,Ctrl+X 退出
|
||||
```
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 使用 Sampling 采样(减少高频日志)
|
||||
config := zap.NewProductionConfig()
|
||||
config.Sampling = &zap.SamplingConfig{
|
||||
Initial: 100, // 前 100 条日志全部记录
|
||||
Thereafter: 100, // 之后每 100 条记录 1 条
|
||||
}
|
||||
|
||||
logger, _ := config.Build()
|
||||
defer logger.Sync()
|
||||
|
||||
// 2. 预分配字段(避免重复创建)
|
||||
baseFields := []zapcore.Field{
|
||||
zap.String("service", "user-service"),
|
||||
zap.String("version", "1.0.0"),
|
||||
}
|
||||
|
||||
// 3. 使用 With 创建子 Logger
|
||||
userLogger := logger.With(baseFields...)
|
||||
|
||||
// 4. 高效记录日志
|
||||
for i := 0; i < 1000; i++ {
|
||||
userLogger.Info("Processing request",
|
||||
zap.Int("request_id", i),
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、对比总结
|
||||
|
||||
| 功能 | Zap | Logback/SLF4J | 说明 |
|
||||
|------|-----|--------------|------|
|
||||
| 性能 | 极高 | 中等 | Zap 快 10-50 倍 |
|
||||
| 结构化日志 | 原生支持 | 需要 MDC | Zap 更简洁 |
|
||||
| 日志级别 | 6 个级别 | 5 个级别 | 类似 |
|
||||
| 日志轮转 | Lumberjack | RollingFileAppender | 配置方式不同 |
|
||||
| 格式化 | JSON/Console | Pattern | Zap 更灵活 |
|
||||
| 配置 | 代码配置 | XML/YAML | 不同风格 |
|
||||
|
||||
---
|
||||
|
||||
## 十一、常见问题
|
||||
|
||||
**Q: Zap 和 Logrus 哪个更好?**
|
||||
|
||||
A: Zap 性能更高(快 4-10 倍),适合高并发场景;Logrus 使用更简单,适合小型项目。
|
||||
|
||||
**Q: 如何在 Gin 中使用 Zap?**
|
||||
|
||||
A:
|
||||
```go
|
||||
router.Use(ginzap.Ginzap(logger, time.RFC3339, true))
|
||||
router.Use(ginzap.RecoveryWithZap(logger, true))
|
||||
```
|
||||
|
||||
**Q: 如何记录 HTTP 请求日志?**
|
||||
|
||||
A:
|
||||
```go
|
||||
logger.Info("HTTP Request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.Int("status", statusCode),
|
||||
zap.Duration("duration", duration),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十二、下一步
|
||||
|
||||
✅ 已掌握 Zap 日志系统
|
||||
→ 下一章:**综合实战项目**(Gin + GORM + Viper + Zap)
|
||||
→ 完整的 Web 应用开发
|
||||
|
||||
祝你编码愉快!🚀
|
||||
826
src/programming/backend/go/Web开发数据库/21综合实战项目.md
Normal file
826
src/programming/backend/go/Web开发数据库/21综合实战项目.md
Normal file
@@ -0,0 +1,826 @@
|
||||
---
|
||||
title: 综合实战项目
|
||||
icon: mdi:shield-account
|
||||
date: 2025-12-23
|
||||
category:
|
||||
- Go
|
||||
- 后端
|
||||
- 工程化
|
||||
- 实战项目
|
||||
tag:
|
||||
- API
|
||||
- 用户认证
|
||||
- 密码加密
|
||||
- JWT
|
||||
- 综合实战
|
||||
---
|
||||
|
||||
整合 Gin、GORM、Viper、Zap 等框架,开发一个完整的用户注册/登录 API。这个项目包含用户认证、密码加密、JWT 令牌和数据库操作,是学习 Go Web 开发的完美案例。
|
||||
|
||||
<!-- more -->
|
||||
|
||||
---
|
||||
|
||||
# Go 综合实战:用户注册/登录 API 完整指南
|
||||
|
||||
这是一个完整的 Web 应用示例,整合了前面学到的所有知识:Gin、GORM、Viper、Zap、中间件等。
|
||||
|
||||
---
|
||||
|
||||
## 一、项目结构
|
||||
|
||||
```
|
||||
go-auth-api/
|
||||
├── config/
|
||||
│ ├── app.yaml
|
||||
│ ├── app.dev.yaml
|
||||
│ └── app.prod.yaml
|
||||
├── logs/
|
||||
│ └── app.log
|
||||
├── main.go
|
||||
├── config.go
|
||||
├── logger.go
|
||||
├── db.go
|
||||
├── models.go
|
||||
├── handlers.go
|
||||
├── middleware.go
|
||||
├── jwt.go
|
||||
├── utils.go
|
||||
└── go.mod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、项目初始化
|
||||
|
||||
### 2.1 创建项目
|
||||
|
||||
```bash
|
||||
cd ~/GolandProjects
|
||||
mkdir go-auth-api && cd go-auth-api
|
||||
go mod init go-auth-api
|
||||
|
||||
# 安装依赖
|
||||
go get -u github.com/gin-gonic/gin
|
||||
go get -u gorm.io/gorm
|
||||
go get -u gorm.io/driver/sqlite
|
||||
go get -u github.com/spf13/viper
|
||||
go get -u go.uber.org/zap
|
||||
go get -u github.com/golang-jwt/jwt/v4
|
||||
go get -u golang.org/x/crypto
|
||||
go get -u gopkg.in/natefinch/lumberjack.v2
|
||||
```
|
||||
|
||||
> **版本说明**:代码兼容 Go 1.18+ 版本(建议 1.22+ 体验最佳性能)
|
||||
|
||||
### 2.2 创建目录
|
||||
|
||||
```bash
|
||||
mkdir -p config logs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、配置文件
|
||||
|
||||
### 3.1 config/app.yaml
|
||||
|
||||
```yaml
|
||||
app:
|
||||
name: AuthAPI
|
||||
version: 1.0.0
|
||||
port: 8080
|
||||
env: dev
|
||||
|
||||
database:
|
||||
driver: sqlite
|
||||
path: auth.db
|
||||
|
||||
jwt:
|
||||
secret: your-secret-key-change-in-production
|
||||
expire: 86400 # 24 小时
|
||||
|
||||
password:
|
||||
bcrypt_cost: 10
|
||||
|
||||
logging:
|
||||
level: info
|
||||
format: json
|
||||
```
|
||||
|
||||
### 3.2 config/app.prod.yaml
|
||||
|
||||
```yaml
|
||||
app:
|
||||
port: 80
|
||||
env: prod
|
||||
|
||||
logging:
|
||||
level: warn
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、模型定义(models.go)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
Email string `gorm:"size:100;unique;not null" json:"email"`
|
||||
Password string `gorm:"size:255;not null" json:"-"` // 不在 JSON 中显示
|
||||
Phone string `gorm:"size:20" json:"phone,omitempty"`
|
||||
Age int `json:"age,omitempty"`
|
||||
Active bool `gorm:"default:true" json:"active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-"`
|
||||
}
|
||||
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
// 请求体
|
||||
type RegisterRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
Phone string `json:"phone" binding:"omitempty,len=11"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
User User `json:"user"`
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库初始化(db.go)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
func InitDB(cfg *Config) error {
|
||||
var dsn string
|
||||
|
||||
if cfg.Database.Driver == "sqlite" {
|
||||
dsn = cfg.Database.Path
|
||||
}
|
||||
|
||||
var err error
|
||||
DB, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect database: %w", err)
|
||||
}
|
||||
|
||||
// 自动迁移
|
||||
if err = DB.AutoMigrate(&User{}); err != nil {
|
||||
return fmt.Errorf("failed to migrate database: %w", err)
|
||||
}
|
||||
|
||||
Logger.Info("Database initialized successfully")
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、JWT 和密码工具(jwt.go + utils.go)
|
||||
|
||||
### 6.1 jwt.go
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateToken(userID uint, email string, secret string, expire int64) (string, error) {
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expire) * time.Second)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString([]byte(secret))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
func VerifyToken(tokenString string, secret string) (*Claims, error) {
|
||||
claims := &Claims{}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 utils.go
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// 密码加密
|
||||
func HashPassword(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
func VerifyPassword(hashedPassword, password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// 检查邮箱是否已注册
|
||||
func EmailExists(email string) bool {
|
||||
var count int64
|
||||
DB.Model(&User{}).Where("email = ?", email).Count(&count)
|
||||
return count > 0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、中间件(middleware.go)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 请求日志中间件
|
||||
func LoggingMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
Logger.Info("Request",
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.String("ip", c.ClientIP()),
|
||||
)
|
||||
|
||||
c.Next()
|
||||
|
||||
Logger.Info("Response",
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.Int("status", c.Writer.Status()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// JWT 认证中间件
|
||||
func AuthMiddleware(cfg *Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
|
||||
if authHeader == "" {
|
||||
c.JSON(401, gin.H{"error": "Missing authorization header"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
c.JSON(401, gin.H{"error": "Invalid authorization header"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := VerifyToken(parts[1], cfg.JWT.Secret)
|
||||
if err != nil {
|
||||
Logger.Error("Token verification failed", zap.Error(err))
|
||||
c.JSON(401, gin.H{"error": "Invalid token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存储在上下文中
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("email", claims.Email)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// CORS 中间件
|
||||
func CORSMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、业务逻辑处理器(handlers.go)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 注册用户
|
||||
func Register(cfg *Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req RegisterRequest
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
Logger.Error("Invalid registration request", zap.Error(err))
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if EmailExists(req.Email) {
|
||||
c.JSON(400, gin.H{"error": "Email already registered"})
|
||||
return
|
||||
}
|
||||
|
||||
// 密码加密
|
||||
hashedPassword, err := HashPassword(req.Password)
|
||||
if err != nil {
|
||||
Logger.Error("Failed to hash password", zap.Error(err))
|
||||
c.JSON(500, gin.H{"error": "Internal server error"})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
user := User{
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Password: hashedPassword,
|
||||
Phone: req.Phone,
|
||||
}
|
||||
|
||||
if err := DB.Create(&user).Error; err != nil {
|
||||
Logger.Error("Failed to create user", zap.Error(err))
|
||||
c.JSON(500, gin.H{"error": "Failed to register user"})
|
||||
return
|
||||
}
|
||||
|
||||
Logger.Info("User registered successfully", zap.String("email", user.Email))
|
||||
|
||||
c.JSON(201, gin.H{
|
||||
"message": "User registered successfully",
|
||||
"user_id": user.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 用户登录
|
||||
func Login(cfg *Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
var user User
|
||||
if err := DB.Where("email = ?", req.Email).First(&user).Error; err != nil {
|
||||
Logger.Warn("User not found", zap.String("email", req.Email))
|
||||
c.JSON(401, gin.H{"error": "Invalid email or password"})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if !VerifyPassword(user.Password, req.Password) {
|
||||
Logger.Warn("Invalid password", zap.String("email", req.Email))
|
||||
c.JSON(401, gin.H{"error": "Invalid email or password"})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成 JWT token
|
||||
token, err := GenerateToken(user.ID, user.Email, cfg.JWT.Secret, int64(cfg.JWT.Expire))
|
||||
if err != nil {
|
||||
Logger.Error("Failed to generate token", zap.Error(err))
|
||||
c.JSON(500, gin.H{"error": "Failed to generate token"})
|
||||
return
|
||||
}
|
||||
|
||||
Logger.Info("User logged in successfully", zap.String("email", user.Email))
|
||||
|
||||
c.JSON(200, LoginResponse{
|
||||
Token: token,
|
||||
User: User{
|
||||
ID: user.ID,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
Age: user.Age,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
func GetProfile(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var user User
|
||||
if err := DB.First(&user, userID).Error; err != nil {
|
||||
c.JSON(404, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, user)
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
func UpdateProfile(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Phone string `json:"phone"`
|
||||
Age int `json:"age"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := DB.Model(&User{}).Where("id = ?", userID).Updates(req).Error; err != nil {
|
||||
Logger.Error("Failed to update user", zap.Error(err))
|
||||
c.JSON(500, gin.H{"error": "Failed to update profile"})
|
||||
return
|
||||
}
|
||||
|
||||
Logger.Info("User profile updated", zap.Uint("user_id", userID))
|
||||
c.JSON(200, gin.H{"message": "Profile updated successfully"})
|
||||
}
|
||||
|
||||
// 健康检查
|
||||
func HealthCheck(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
"app": "AuthAPI",
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、配置加载(config.go)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
App struct {
|
||||
Name string `mapstructure:"name"`
|
||||
Version string `mapstructure:"version"`
|
||||
Port int `mapstructure:"port"`
|
||||
Env string `mapstructure:"env"`
|
||||
} `mapstructure:"app"`
|
||||
|
||||
Database struct {
|
||||
Driver string `mapstructure:"driver"`
|
||||
Path string `mapstructure:"path"`
|
||||
} `mapstructure:"database"`
|
||||
|
||||
JWT struct {
|
||||
Secret int `mapstructure:"secret"`
|
||||
Expire int `mapstructure:"expire"`
|
||||
} `mapstructure:"jwt"`
|
||||
|
||||
Logging struct {
|
||||
Level string `mapstructure:"level"`
|
||||
Format string `mapstructure:"format"`
|
||||
} `mapstructure:"logging"`
|
||||
}
|
||||
|
||||
var GlobalConfig *Config
|
||||
|
||||
func LoadConfig() (*Config, error) {
|
||||
env := os.Getenv("GO_ENV")
|
||||
if env == "" {
|
||||
env = "dev"
|
||||
}
|
||||
|
||||
viper.SetConfigName("app")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath("./config")
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read config: %w", err)
|
||||
}
|
||||
|
||||
viper.SetConfigName("app." + env)
|
||||
viper.MergeInConfig()
|
||||
|
||||
var cfg Config
|
||||
if err := viper.Unmarshal(&cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
GlobalConfig = &cfg
|
||||
return &cfg, nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、日志初始化(logger.go)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
var Logger *zap.Logger
|
||||
|
||||
func InitLogger(env string) error {
|
||||
var level zapcore.Level
|
||||
|
||||
switch env {
|
||||
case "prod":
|
||||
level = zapcore.WarnLevel
|
||||
case "test":
|
||||
level = zapcore.DebugLevel
|
||||
default:
|
||||
level = zapcore.InfoLevel
|
||||
}
|
||||
|
||||
logFile := &lumberjack.Logger{
|
||||
Filename: "logs/app.log",
|
||||
MaxSize: 100,
|
||||
MaxBackups: 10,
|
||||
MaxAge: 7,
|
||||
Compress: true,
|
||||
}
|
||||
|
||||
encoderConfig := zapcore.EncoderConfig{
|
||||
TimeKey: "ts",
|
||||
LevelKey: "level",
|
||||
MessageKey: "msg",
|
||||
CallerKey: "caller",
|
||||
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
}
|
||||
|
||||
core := zapcore.NewCore(
|
||||
zapcore.NewJSONEncoder(encoderConfig),
|
||||
zapcore.NewMultiWriteSyncer(
|
||||
zapcore.AddSync(os.Stdout),
|
||||
zapcore.AddSync(logFile),
|
||||
),
|
||||
level,
|
||||
)
|
||||
|
||||
Logger = zap.New(core, zap.AddCaller())
|
||||
zap.ReplaceGlobals(Logger)
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十一、主程序(main.go)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 加载配置
|
||||
cfg, err := LoadConfig()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 初始化日志
|
||||
if err = InitLogger(cfg.App.Env); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer Logger.Sync()
|
||||
|
||||
// 初始化数据库
|
||||
if err = InitDB(cfg); err != nil {
|
||||
Logger.Fatal("Failed to initialize database", zap.Error(err))
|
||||
}
|
||||
|
||||
// 创建 Gin 应用
|
||||
r := gin.Default()
|
||||
|
||||
// 应用中间件
|
||||
r.Use(LoggingMiddleware())
|
||||
r.Use(CORSMiddleware())
|
||||
|
||||
// 公开路由
|
||||
public := r.Group("/api")
|
||||
{
|
||||
public.GET("/health", HealthCheck)
|
||||
public.POST("/register", Register(cfg))
|
||||
public.POST("/login", Login(cfg))
|
||||
}
|
||||
|
||||
// 受保护的路由
|
||||
protected := r.Group("/api")
|
||||
protected.Use(AuthMiddleware(cfg))
|
||||
{
|
||||
protected.GET("/profile", GetProfile)
|
||||
protected.PUT("/profile", UpdateProfile(cfg))
|
||||
}
|
||||
|
||||
// 启动服务器
|
||||
addr := fmt.Sprintf(":%d", cfg.App.Port)
|
||||
Logger.Info("Server starting",
|
||||
zap.String("app", cfg.App.Name),
|
||||
zap.Int("port", cfg.App.Port),
|
||||
zap.String("env", cfg.App.Env),
|
||||
)
|
||||
|
||||
if err = r.Run(addr); err != nil {
|
||||
Logger.Fatal("Server error", zap.Error(err))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十二、API 使用示例
|
||||
|
||||
### 12.1 注册用户
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com",
|
||||
"password": "password123",
|
||||
"phone": "13800138000"
|
||||
}'
|
||||
|
||||
# 响应
|
||||
{
|
||||
"message": "User registered successfully",
|
||||
"user_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 12.2 用户登录
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "alice@example.com",
|
||||
"password": "password123"
|
||||
}'
|
||||
|
||||
# 响应
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com",
|
||||
"phone": "13800138000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 12.3 获取用户信息(需要认证)
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8080/api/profile \
|
||||
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
|
||||
|
||||
# 响应
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Alice",
|
||||
"email": "alice@example.com",
|
||||
"phone": "13800138000",
|
||||
"age": 0,
|
||||
"active": true,
|
||||
"created_at": "2025-12-23T14:30:45Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十三、项目验收清单
|
||||
|
||||
- ✅ 用户注册(密码加密、邮箱验证)
|
||||
- ✅ 用户登录(JWT 令牌生成)
|
||||
- ✅ 用户认证(JWT 验证中间件)
|
||||
- ✅ 用户信息管理(获取、更新)
|
||||
- ✅ 配置管理(多环境)
|
||||
- ✅ 日志记录(结构化、日志轮转)
|
||||
- ✅ 错误处理(友好的错误消息)
|
||||
- ✅ 代码组织(清晰的文件结构)
|
||||
|
||||
---
|
||||
|
||||
## 十四、进阶扩展方向
|
||||
|
||||
1. **添加邮箱验证** - 发送验证码验证邮箱
|
||||
2. **实现刷新令牌** - 增加安全性
|
||||
3. **添加速率限制** - 防止暴力破解
|
||||
4. **用户权限管理** - 基于角色的访问控制
|
||||
5. **社交登录** - 集成 OAuth2(GitHub、Google)
|
||||
6. **单元测试** - 为关键业务逻辑编写测试
|
||||
7. **Docker 打包** - 容器化部署
|
||||
8. **CI/CD 流程** - 自动化测试和部署
|
||||
|
||||
---
|
||||
|
||||
祝你编码愉快!🚀 这个项目整合了 Go Web 开发的所有核心知识,是学习和面试的好材料。
|
||||
|
||||
327
src/programming/backend/go/Web开发数据库/README.md
Normal file
327
src/programming/backend/go/Web开发数据库/README.md
Normal file
@@ -0,0 +1,327 @@
|
||||
# Go Web 工程化完整学习路径
|
||||
|
||||
> **适合对象**:已掌握 Go 基础语法和并发模型,想学习 Go Web 开发和工程实践的开发者
|
||||
|
||||
## 📚 学习大纲
|
||||
|
||||
### **Week 3:Web 开发 + 数据库**
|
||||
|
||||
这一周主要学习 Go Web 框架、数据库操作、配置管理、日志系统和综合项目实战。
|
||||
|
||||
| Day | 文件 | 任务 | 重点 |
|
||||
|-----|------|------|------|
|
||||
| 1-2 | `15Gin基础入门.md` | Gin 入门:路由、handler、绑定参数 | 掌握 RESTful API 基础 |
|
||||
| 3 | `16Gin中间件.md` | 中间件:日志、异常捕获、CORS | 理解请求/响应拦截机制 |
|
||||
| 4 | `17GORM数据库.md` | GORM:CRUD、模型定义、自动迁移 | 掌握 ORM 数据库操作 |
|
||||
| 5 | `18GORM事务关联.md` | 事务、关联关系、一对多查询 | 处理复杂业务逻辑 |
|
||||
| 6 | `19Viper配置管理.md` | viper:配置加载、分环境配置 | 实现灵活的配置管理 |
|
||||
| 6+ | `20Zap日志.md` | zap:高性能日志 | 掌握生产级日志系统 |
|
||||
| 7 | `21综合实战项目.md` | 小项目:用户注册/登录 API | 整合所有知识进行实战 |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 核心知识点
|
||||
|
||||
### **Day 1-2:Gin 基础入门**
|
||||
- ✅ 最小化 Gin 应用
|
||||
- ✅ 路由注册(GET、POST、PUT、DELETE)
|
||||
- ✅ 路由分组和嵌套
|
||||
- ✅ Handler 处理器的三种定义方式
|
||||
- ✅ Context 上下文对象
|
||||
- ✅ 参数绑定(URL、Query、JSON、Form)
|
||||
- ✅ 完整的用户管理 API 示例
|
||||
|
||||
**Java 对比**:Gin ≈ Spring Boot 的 `@RestController` 和 `@RequestMapping`
|
||||
|
||||
### **Day 3:Gin 中间件**
|
||||
- ✅ 中间件原理和执行链
|
||||
- ✅ 编写自定义中间件
|
||||
- ✅ 日志中间件
|
||||
- ✅ 异常捕获和恢复
|
||||
- ✅ 认证中间件(基础认证、Token、JWT)
|
||||
- ✅ CORS 中间件
|
||||
- ✅ 全局、分组、路由级中间件的应用
|
||||
|
||||
**Java 对比**:Gin 中间件 ≈ Spring 的 `Filter` 和 `Interceptor`
|
||||
|
||||
### **Day 4:GORM 基础**
|
||||
- ✅ 数据库连接(SQLite、MySQL、PostgreSQL)
|
||||
- ✅ 模型定义和数据库标签
|
||||
- ✅ 自动迁移(建表、字段管理)
|
||||
- ✅ CRUD 操作:创建、读取、更新、删除
|
||||
- ✅ 高级查询(条件、排序、分页、指定字段)
|
||||
- ✅ 软删除和硬删除
|
||||
|
||||
**Java 对比**:GORM ≈ JPA/Hibernate,但更轻量灵活
|
||||
|
||||
### **Day 5:GORM 进阶**
|
||||
- ✅ 数据库事务(ACID)
|
||||
- ✅ 函数式事务处理
|
||||
- ✅ 一对多关系(One-to-Many)
|
||||
- ✅ 多对多关系(Many-to-Many)
|
||||
- ✅ 预加载(Preload)解决 N+1 问题
|
||||
- ✅ 关联操作(Append、Replace、Delete)
|
||||
- ✅ 银行转账、博客系统等实战例子
|
||||
|
||||
**Java 对比**:关联关系类似 JPA 的 `@OneToMany`、`@ManyToMany`
|
||||
|
||||
### **Day 6:Viper 配置管理**
|
||||
- ✅ 配置文件加载(YAML、JSON、TOML)
|
||||
- ✅ 结构体绑定
|
||||
- ✅ 环境变量集成
|
||||
- ✅ 多环境配置(dev、prod、test)
|
||||
- ✅ 默认值设置
|
||||
- ✅ 配置热重载
|
||||
- ✅ 完整的应用配置系统
|
||||
|
||||
**Java 对比**:Viper ≈ Spring Boot 的 `application.yml` 和环境变量
|
||||
|
||||
### **Day 6+:Zap 日志系统**
|
||||
- ✅ 开发模式和生产模式
|
||||
- ✅ 自定义日志配置
|
||||
- ✅ 结构化日志和字段
|
||||
- ✅ 日志轮转(Lumberjack)
|
||||
- ✅ 全局日志记录器
|
||||
- ✅ 与 Gin 框架集成
|
||||
- ✅ 日志性能优化
|
||||
|
||||
**Java 对比**:Zap ≈ SLF4J + Logback,性能更优
|
||||
|
||||
### **Day 7:综合实战项目**
|
||||
- ✅ 用户注册 API
|
||||
- ✅ 用户登录 API(JWT 认证)
|
||||
- ✅ 用户信息管理 API
|
||||
- ✅ 密码加密和验证
|
||||
- ✅ 完整的项目结构和配置
|
||||
- ✅ 错误处理和日志记录
|
||||
- ✅ 多环境部署
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 知识结构图
|
||||
|
||||
```
|
||||
Week 3:Web 开发 + 数据库
|
||||
├─ Gin Web 框架 (Day 1-2)
|
||||
│ ├─ 路由和 Handler
|
||||
│ ├─ 参数绑定
|
||||
│ ├─ 中间件系统
|
||||
│ ├─ 认证和授权
|
||||
│ └─ 错误处理
|
||||
│
|
||||
├─ GORM 数据库 ORM (Day 3-4)
|
||||
│ ├─ 模型定义
|
||||
│ ├─ CRUD 操作
|
||||
│ ├─ 高级查询
|
||||
│ ├─ 事务处理
|
||||
│ └─ 关联关系
|
||||
│
|
||||
├─ 配置管理 (Day 5)
|
||||
│ ├─ Viper 框架
|
||||
│ ├─ 多环境配置
|
||||
│ ├─ 环境变量
|
||||
│ └─ 动态配置
|
||||
│
|
||||
├─ 日志系统 (Day 6)
|
||||
│ ├─ Zap 框架
|
||||
│ ├─ 结构化日志
|
||||
│ ├─ 日志轮转
|
||||
│ └─ 性能优化
|
||||
│
|
||||
└─ 综合实战 (Day 7)
|
||||
└─ 用户认证系统
|
||||
├─ 注册
|
||||
├─ 登录
|
||||
├─ JWT
|
||||
└─ 权限管理
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 重要对比总结
|
||||
|
||||
### **Gin vs Spring Boot**
|
||||
|
||||
| 功能 | Gin | Spring Boot | Go 优势 |
|
||||
|------|-----|------------|--------|
|
||||
| 启动时间 | < 100ms | > 1s | ✅ 快 10 倍 |
|
||||
| 内存占用 | ~ 10MB | ~ 200MB | ✅ 小 20 倍 |
|
||||
| 并发性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ✅ 更高效 |
|
||||
| 学习曲线 | 平缓 | 陡峭 | ✅ 更简单 |
|
||||
| 功能完整度 | 足够 | 非常完整 | Spring 更全 |
|
||||
|
||||
### **GORM vs JPA/Hibernate**
|
||||
|
||||
| 功能 | GORM | JPA | 特点 |
|
||||
|------|-----|-----|------|
|
||||
| 自动迁移 | ✅ 细致 | ❌ 需配置 | GORM 更灵活 |
|
||||
| 查询语法 | 简洁 | 复杂 | GORM 更直观 |
|
||||
| 性能 | ⭐⭐⭐⭐ | ⭐⭐⭐ | GORM 更快 |
|
||||
| 关联加载 | Preload | Lazy Load | 都支持 |
|
||||
|
||||
### **Viper vs Spring Config**
|
||||
|
||||
| 功能 | Viper | Spring Config | 特点 |
|
||||
|------|-------|---------------|------|
|
||||
| 多环境支持 | ✅ 原生 | ✅ 原生 | 都很好 |
|
||||
| 热重载 | ✅ 支持 | ❌ 需插件 | Viper 更简单 |
|
||||
| 环境变量 | ✅ 灵活 | ✅ 灵活 | 都支持 |
|
||||
|
||||
---
|
||||
|
||||
## 💡 学习建议
|
||||
|
||||
### **学习方式**
|
||||
|
||||
1. **边读边写**:每篇文档都有完整的代码示例,务必亲手实现
|
||||
2. **逐步深入**:不要跳过,按顺序学习,后面的内容依赖前面的知识
|
||||
3. **多做练习**:每章结束后,自己尝试修改和扩展代码
|
||||
4. **对比理解**:充分利用 Java 对比,加深理解(如果你是 Java 开发者)
|
||||
|
||||
### **时间规划**
|
||||
|
||||
- **Day 1-2**(Gin):2-3 天
|
||||
- 掌握路由、参数绑定
|
||||
- 理解中间件执行链
|
||||
|
||||
- **Day 3-4**(GORM):3-4 天
|
||||
- 掌握 CRUD 操作
|
||||
- 理解关联关系和事务
|
||||
|
||||
- **Day 5-6**(配置和日志):2 天
|
||||
- 快速掌握 Viper 和 Zap
|
||||
- 知道如何在项目中使用
|
||||
|
||||
- **Day 7**(实战项目):2-3 天
|
||||
- 完整实现用户认证系统
|
||||
- 整合所有知识点
|
||||
|
||||
### **实战项目建议**
|
||||
|
||||
完成 Week 3 学习后,建议做以下练习项目(难度递增):
|
||||
|
||||
1. **简单 TODO API**(1-2 小时)
|
||||
- CRUD 操作、Gin 路由、Viper 配置
|
||||
|
||||
2. **博客系统**(3-5 小时)
|
||||
- 用户、文章、评论的一对多和多对多
|
||||
- 事务处理、Zap 日志
|
||||
|
||||
3. **电商购物车 API**(5-8 小时)
|
||||
- 用户认证、权限管理、订单事务
|
||||
- 完整的生产级代码
|
||||
|
||||
4. **实时聊天系统**(8+ 小时)
|
||||
- WebSocket、Goroutine 并发
|
||||
- 消息队列、事务处理
|
||||
|
||||
---
|
||||
|
||||
## 🚀 进阶方向
|
||||
|
||||
学完 Week 3 后,可继续学习 Week 4:微服务与高级特性
|
||||
|
||||
1. **微服务架构**
|
||||
- gRPC、protobuf
|
||||
- 服务注册和发现
|
||||
|
||||
2. **中间件和消息队列**
|
||||
- RabbitMQ、Kafka
|
||||
- Redis 缓存
|
||||
|
||||
3. **容器和部署**
|
||||
- Docker、Docker Compose
|
||||
- Kubernetes(K8s)
|
||||
|
||||
4. **测试和监控**
|
||||
- 单元测试、集成测试
|
||||
- Prometheus、Grafana 监控
|
||||
|
||||
---
|
||||
|
||||
## 📝 快速参考
|
||||
|
||||
### **常用命令**
|
||||
|
||||
```bash
|
||||
# 创建项目
|
||||
go mod init project-name
|
||||
|
||||
# 安装依赖
|
||||
go get -u github.com/gin-gonic/gin
|
||||
go get -u gorm.io/gorm
|
||||
go get -u github.com/spf13/viper
|
||||
go get -u go.uber.org/zap
|
||||
|
||||
# 运行和测试
|
||||
go run .
|
||||
go run main.go
|
||||
go test ./...
|
||||
go build -o app
|
||||
|
||||
# 格式化和检查
|
||||
go fmt ./...
|
||||
go vet ./...
|
||||
```
|
||||
|
||||
### **Gin 路由速查**
|
||||
|
||||
```go
|
||||
r.GET() // 查询
|
||||
r.POST() // 创建
|
||||
r.PUT() // 更新
|
||||
r.DELETE() // 删除
|
||||
|
||||
r.Group() // 分组
|
||||
r.Use() // 中间件
|
||||
r.Run() // 启动服务
|
||||
```
|
||||
|
||||
### **GORM 操作速查**
|
||||
|
||||
```go
|
||||
db.Create() // 创建
|
||||
db.First() // 查询单条
|
||||
db.Find() // 查询多条
|
||||
db.Update() // 更新
|
||||
db.Delete() // 删除
|
||||
db.Transaction() // 事务
|
||||
db.Preload() // 预加载
|
||||
db.AutoMigrate() // 自动迁移
|
||||
```
|
||||
|
||||
### **中间件速查**
|
||||
|
||||
```go
|
||||
r.Use(logging) // 全局中间件
|
||||
r.Group().Use(auth) // 分组中间件
|
||||
r.GET("/", auth, handler) // 路由中间件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 验收标准
|
||||
|
||||
完成 Week 3 的学习后,你应该能够:
|
||||
|
||||
- ✅ 使用 Gin 快速开发 RESTful API
|
||||
- ✅ 使用 GORM 进行数据库 CRUD 操作
|
||||
- ✅ 理解和使用数据库事务
|
||||
- ✅ 使用中间件实现认证、授权、日志等功能
|
||||
- ✅ 使用 Viper 管理多环境配置
|
||||
- ✅ 使用 Zap 记录结构化日志
|
||||
- ✅ 从零开始开发一个完整的 Web API 应用
|
||||
- ✅ 理解 Go Web 开发的最佳实践
|
||||
|
||||
---
|
||||
|
||||
## 🤝 学习社区
|
||||
|
||||
- **Go 官方文档**:https://golang.org/
|
||||
- **Gin 文档**:https://github.com/gin-gonic/gin
|
||||
- **GORM 文档**:https://gorm.io/
|
||||
- **Go 问答**:https://stackoverflow.com/questions/tagged/go
|
||||
|
||||
---
|
||||
|
||||
**开始学习吧!祝你编码愉快!🚀**
|
||||
Reference in New Issue
Block a user