From 67c0cbf0d7208f3e02595d7ad295b14da60082ec Mon Sep 17 00:00:00 2001 From: liumangmang Date: Wed, 24 Dec 2025 17:23:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(sidebar):=20=E6=B7=BB=E5=8A=A0=20Web?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E6=95=B0=E6=8D=AE=E5=BA=93=20=E4=BE=A7?= =?UTF-8?q?=E8=BE=B9=E6=A0=8F=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在侧边栏配置中新增 Web开发数据库分类 - 增加相关文档链接,包括 Gin 框架、GORM、Viper、Zap 等内容 - 设置分类可折叠,提升导航体验 refactor(go): 精简 Go 示例代码导入包 - 移除未使用的 runtime 和 time 包 - 保留 fmt 和 sync 包,优化依赖 docs(linux): 删除 Nginx 基础入门文档 - 移除包含 Nginx 基本概念、安装、配置和常用命令的完整文档 - 清理废弃内容,简化项目文档结构 --- src/.vuepress/sidebar.ts | 16 + .../backend/go/Go基础语法/03slice和map.md | 2 - .../backend/go/Web开发数据库/15Gin基础入门.md | 793 +++++++++++++++++ .../backend/go/Web开发数据库/16Gin中间件.md | 766 ++++++++++++++++ .../backend/go/Web开发数据库/17GORM数据库.md | 810 +++++++++++++++++ .../go/Web开发数据库/18GORM事务关联.md | 775 ++++++++++++++++ .../go/Web开发数据库/19Viper配置管理.md | 750 ++++++++++++++++ .../backend/go/Web开发数据库/20Zap日志.md | 673 ++++++++++++++ .../go/Web开发数据库/21综合实战项目.md | 826 ++++++++++++++++++ .../backend/go/Web开发数据库/README.md | 327 +++++++ src/programming/docker/Navidrome.md | 602 +++++++++++++ .../linux/基础/03-应用安装与快捷方式.md | 664 ++++++++++++++ .../{nginx基础入门.md => 04-nginx基础入门.md} | 0 src/work/log/2025-12.md | 0 14 files changed, 7002 insertions(+), 2 deletions(-) create mode 100644 src/programming/backend/go/Web开发数据库/15Gin基础入门.md create mode 100644 src/programming/backend/go/Web开发数据库/16Gin中间件.md create mode 100644 src/programming/backend/go/Web开发数据库/17GORM数据库.md create mode 100644 src/programming/backend/go/Web开发数据库/18GORM事务关联.md create mode 100644 src/programming/backend/go/Web开发数据库/19Viper配置管理.md create mode 100644 src/programming/backend/go/Web开发数据库/20Zap日志.md create mode 100644 src/programming/backend/go/Web开发数据库/21综合实战项目.md create mode 100644 src/programming/backend/go/Web开发数据库/README.md create mode 100644 src/programming/docker/Navidrome.md create mode 100644 src/programming/linux/基础/03-应用安装与快捷方式.md rename src/programming/linux/基础/{nginx基础入门.md => 04-nginx基础入门.md} (100%) create mode 100644 src/work/log/2025-12.md diff --git a/src/.vuepress/sidebar.ts b/src/.vuepress/sidebar.ts index 3640c75..6b4aceb 100644 --- a/src/.vuepress/sidebar.ts +++ b/src/.vuepress/sidebar.ts @@ -110,6 +110,22 @@ export default sidebar({ "14并发爬虫实战.md", ], }, + { + text: "Web开发数据库", + icon: "mdi:database-outline", + collapsible: true, + prefix: "Web开发数据库/", + children: [ + "README.md", + "15Gin基础入门.md", + "16Gin中间件.md", + "17GORM数据库.md", + "18GORM事务关联.md", + "19Viper配置管理.md", + "20Zap日志.md", + "21综合实战项目.md", + ], + } ], }, { diff --git a/src/programming/backend/go/Go基础语法/03slice和map.md b/src/programming/backend/go/Go基础语法/03slice和map.md index 783f288..d14963b 100644 --- a/src/programming/backend/go/Go基础语法/03slice和map.md +++ b/src/programming/backend/go/Go基础语法/03slice和map.md @@ -55,9 +55,7 @@ package main import ( "fmt" - "runtime" "sync" - "time" ) func main() { diff --git a/src/programming/backend/go/Web开发数据库/15Gin基础入门.md b/src/programming/backend/go/Web开发数据库/15Gin基础入门.md new file mode 100644 index 0000000..95f6404 --- /dev/null +++ b/src/programming/backend/go/Web开发数据库/15Gin基础入门.md @@ -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 的核心概念:路由、处理器、参数绑定。 + + + +--- + +# 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 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) + +祝你编码愉快!🚀 + diff --git a/src/programming/backend/go/Web开发数据库/16Gin中间件.md b/src/programming/backend/go/Web开发数据库/16Gin中间件.md new file mode 100644 index 0000000..0fbc11e --- /dev/null +++ b/src/programming/backend/go/Web开发数据库/16Gin中间件.md @@ -0,0 +1,766 @@ +--- +title: Gin中间件 +icon: mdi:pipe +date: 2025-12-23 +category: + - Go + - 后端 + - Web框架 + - 工程化 + - Gin +tag: + - 中间件 + - 日志 + - 异常捕获 + - CORS + - 认证 +--- + +Gin 中间件是 Web 应用的核心机制,用于实现日志、认证、CORS、错误处理等跨切面需求。本文通过完整可运行的示例,并结合 Java Filter 对比,帮助你快速掌握中间件开发。 + + + +--- + +# 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) + +祝你编码愉快!🚀 diff --git a/src/programming/backend/go/Web开发数据库/17GORM数据库.md b/src/programming/backend/go/Web开发数据库/17GORM数据库.md new file mode 100644 index 0000000..5fdfa92 --- /dev/null +++ b/src/programming/backend/go/Web开发数据库/17GORM数据库.md @@ -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 使用。 + + + +--- + +# 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) + +祝你编码愉快!🚀 diff --git a/src/programming/backend/go/Web开发数据库/18GORM事务关联.md b/src/programming/backend/go/Web开发数据库/18GORM事务关联.md new file mode 100644 index 0000000..eb4b3b8 --- /dev/null +++ b/src/programming/backend/go/Web开发数据库/18GORM事务关联.md @@ -0,0 +1,775 @@ +--- +title: GORM事务关联 +icon: mdi:relation-many-to-many +date: 2025-12-23 +category: + - Go + - 后端 + - 数据库 + - ORM + - GORM +tag: + - 事务 + - 关联关系 + - 一对多 + - 多对多 + - 外键 +--- + +GORM 提供了强大的事务和关联关系支持。本文通过完整可运行的示例,并结合 JPA 对比,帮助你掌握事务处理和关联关系操作。 + + + +--- + +# 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 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 日志系统** + +祝你编码愉快!🚀 diff --git a/src/programming/backend/go/Web开发数据库/19Viper配置管理.md b/src/programming/backend/go/Web开发数据库/19Viper配置管理.md new file mode 100644 index 0000000..d731cbd --- /dev/null +++ b/src/programming/backend/go/Web开发数据库/19Viper配置管理.md @@ -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 使用。 + + + +--- + +# 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 日志系统** +→ 再下一章:**综合实战项目** + +祝你编码愉快!🚀 diff --git a/src/programming/backend/go/Web开发数据库/20Zap日志.md b/src/programming/backend/go/Web开发数据库/20Zap日志.md new file mode 100644 index 0000000..0175dcf --- /dev/null +++ b/src/programming/backend/go/Web开发数据库/20Zap日志.md @@ -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 使用。 + + + +--- + +# 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 应用开发 + +祝你编码愉快!🚀 diff --git a/src/programming/backend/go/Web开发数据库/21综合实战项目.md b/src/programming/backend/go/Web开发数据库/21综合实战项目.md new file mode 100644 index 0000000..c2299a7 --- /dev/null +++ b/src/programming/backend/go/Web开发数据库/21综合实战项目.md @@ -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 开发的完美案例。 + + + +--- + +# 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 开发的所有核心知识,是学习和面试的好材料。 + diff --git a/src/programming/backend/go/Web开发数据库/README.md b/src/programming/backend/go/Web开发数据库/README.md new file mode 100644 index 0000000..c897a4d --- /dev/null +++ b/src/programming/backend/go/Web开发数据库/README.md @@ -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 + +--- + +**开始学习吧!祝你编码愉快!🚀** diff --git a/src/programming/docker/Navidrome.md b/src/programming/docker/Navidrome.md new file mode 100644 index 0000000..d1d1b9f --- /dev/null +++ b/src/programming/docker/Navidrome.md @@ -0,0 +1,602 @@ +--- +title: Docker 安装 Navidrome +icon: mdi:music-box-multiple +date: 2025-12-23 +category: + - Docker +tag: + - Navidrome + - 音乐服务器 + - Docker Compose +--- + +# Docker 安装 Navidrome + +Navidrome 是一个开源的音乐流媒体服务器和流媒体播放器,兼容 Subsonic/Airsonic API,可以让你随时随地访问和播放自己的音乐收藏。 + +## 什么是 Navidrome? + +### 主要特性 + +- 🎵 支持多种音频格式(MP3、FLAC、OGG、M4A 等) +- 📱 响应式 Web 界面,支持手机和平板 +- 🎨 自动获取专辑封面和艺术家信息 +- 🔐 多用户支持,每个用户独立的播放列表 +- 📻 智能播放列表和电台功能 +- 🌐 兼容 Subsonic API,支持第三方客户端 +- ⚡ 轻量级,低资源占用 +- 🔄 实时音乐库扫描和更新 + +## 安装前准备 + +### 1. 确保 Docker 已安装 + +检查 Docker 和 Docker Compose 版本: + +```bash +docker --version +docker-compose --version +``` + +如果未安装,请参考 [Docker 安装指南](./安装.md)。 + +### 2. 准备目录结构 + +创建 Navidrome 所需的目录: + +```bash +# 创建工作目录 +mkdir -p ~/navidrome/{data,music} + +# 进入工作目录 +cd ~/navidrome +``` + +目录说明: +- `data`: 存储 Navidrome 的数据库和配置文件 +- `music`: 存放你的音乐文件 + +## 方式一:使用 docker run 安装 + +### 基础安装 + +```bash +docker run -d \ + --name navidrome \ + --restart=unless-stopped \ + -p 4533:4533 \ + -v ~/navidrome/data:/data \ + -v ~/navidrome/music:/music:ro \ + -e ND_LOGLEVEL=info \ + -e ND_SESSIONTIMEOUT=24h \ + deluan/navidrome:latest +``` + +### 参数说明 + +- `-d`: 后台运行容器 +- `--name navidrome`: 容器名称 +- `--restart=unless-stopped`: 自动重启策略 +- `-p 4533:4533`: 端口映射(主机端口:容器端口) +- `-v ~/navidrome/data:/data`: 数据目录挂载 +- `-v ~/navidrome/music:/music:ro`: 音乐目录挂载(只读) +- `-e ND_LOGLEVEL=info`: 日志级别 +- `-e ND_SESSIONTIMEOUT=24h`: 会话超时时间 + +### 进阶配置 + +如果需要更多配置选项: + +```bash +docker run -d \ + --name navidrome \ + --restart=unless-stopped \ + -p 4533:4533 \ + -v ~/navidrome/data:/data \ + -v ~/navidrome/music:/music:ro \ + -e ND_LOGLEVEL=info \ + -e ND_SESSIONTIMEOUT=24h \ + -e ND_BASEURL="" \ + -e ND_SCANSCHEDULE="1h" \ + -e ND_TRANSCODINGCACHESIZE="100MB" \ + -e ND_ENABLETRANSCODINGCONFIG=true \ + -e ND_ENABLESTARRATING=true \ + -e ND_ENABLEFAVOURITES=true \ + deluan/navidrome:latest +``` + +## 方式二:使用 Docker Compose 安装(推荐) + +### 1. 创建 docker-compose.yml + +在 `~/navidrome` 目录下创建 `docker-compose.yml` 文件: + +```bash +vim docker-compose.yml +``` + +### 2. 基础配置 + +```yaml +version: '3.8' + +services: + navidrome: + image: deluan/navidrome:latest + container_name: navidrome + restart: unless-stopped + ports: + - "4533:4533" + volumes: + - ./data:/data + - ./music:/music:ro + environment: + # 基础配置 + ND_LOGLEVEL: info + ND_SESSIONTIMEOUT: 24h + ND_BASEURL: "" +``` + +### 3. 完整配置(推荐) + +```yaml +version: '3.8' + +services: + navidrome: + image: deluan/navidrome:latest + container_name: navidrome + restart: unless-stopped + ports: + - "4533:4533" + volumes: + # 数据目录 + - ./data:/data + # 音乐目录(可以添加多个) + - ./music:/music:ro + # 如果有多个音乐目录,可以这样添加: + # - /path/to/music1:/music1:ro + # - /path/to/music2:/music2:ro + environment: + # 基础配置 + ND_LOGLEVEL: info # 日志级别: error, warn, info, debug, trace + ND_SESSIONTIMEOUT: 24h # 会话超时时间 + ND_BASEURL: "" # 基础 URL(如果通过反向代理访问需要设置) + + # 扫描配置 + ND_SCANSCHEDULE: 1h # 自动扫描间隔(@every 1h 或 cron 表达式) + ND_SCANINTERVAL: 90s # 扫描时检查文件变化的间隔 + + # 转码配置 + ND_TRANSCODINGCACHESIZE: 100MB # 转码缓存大小 + ND_ENABLETRANSCODINGCONFIG: "true" # 启用转码配置 + + # 功能开关 + ND_ENABLESTARRATING: "true" # 启用星级评分 + ND_ENABLEFAVOURITES: "true" # 启用收藏功能 + ND_ENABLEUSEREDITING: "true" # 允许用户编辑个人信息 + ND_ENABLESHARING: "true" # 启用分享功能 + ND_ENABLEDOWNLOADS: "true" # 启用下载功能 + + # 播放器配置 + ND_DEFAULTTHEME: "Dark" # 默认主题: Light, Dark, Auto + ND_UILOGINBACKGROUNDURL: "" # 登录页背景图 URL + ND_UIWELCOMEMESSAGE: "" # 欢迎消息 + + # 性能配置 + ND_IMAGECACHESIZE: 100MB # 封面缓存大小 + ND_AUTOIMPORTPLAYLISTS: "true" # 自动导入播放列表 + + # 多媒体配置 + ND_ENABLECOVERANIMATION: "true" # 启用封面动画 + ND_ENABLEREPLAYGAIN: "true" # 启用音量平衡 + + # 搜索配置 + ND_SEARCHFULLSTRING: "false" # 全字符串搜索 + ND_RECENTLYADDEDBYMODTIME: "false" # 按修改时间显示最近添加 + + # Spotify 集成(可选) + # ND_SPOTIFY_ID: "your_spotify_client_id" + # ND_SPOTIFY_SECRET: "your_spotify_secret" + + # Last.fm 集成(可选) + # ND_LASTFM_ENABLED: "true" + # ND_LASTFM_APIKEY: "your_lastfm_api_key" + # ND_LASTFM_SECRET: "your_lastfm_secret" + + # 资源限制(可选) + # deploy: + # resources: + # limits: + # cpus: '2.0' + # memory: 1G + # reservations: + # cpus: '0.5' + # memory: 512M +``` + +### 4. 启动服务 + +```bash +# 启动 Navidrome +docker-compose up -d + +# 查看日志 +docker-compose logs -f + +# 停止服务 +docker-compose down + +# 重启服务 +docker-compose restart +``` + +## 配置说明 + +### 环境变量详解 + +| 变量名 | 默认值 | 说明 | +|--------|--------|------| +| `ND_LOGLEVEL` | info | 日志级别:error, warn, info, debug, trace | +| `ND_SESSIONTIMEOUT` | 24h | 会话超时时间 | +| `ND_BASEURL` | "" | 基础 URL(反向代理时使用) | +| `ND_SCANSCHEDULE` | 1h | 自动扫描音乐库的时间间隔 | +| `ND_TRANSCODINGCACHESIZE` | 100MB | 转码缓存大小 | +| `ND_IMAGECACHESIZE` | 100MB | 图片缓存大小 | +| `ND_ENABLETRANSCODINGCONFIG` | true | 是否启用转码配置 | +| `ND_ENABLESTARRATING` | true | 是否启用星级评分 | +| `ND_ENABLEFAVOURITES` | true | 是否启用收藏功能 | +| `ND_ENABLESHARING` | true | 是否启用分享功能 | +| `ND_ENABLEDOWNLOADS` | true | 是否启用下载功能 | +| `ND_DEFAULTTHEME` | Dark | 默认主题:Light, Dark, Auto | + +更多配置选项请参考 [官方文档](https://www.navidrome.org/docs/usage/configuration-options/)。 + +### 端口说明 + +默认端口:`4533` + +如果端口冲突,可以修改为其他端口: +```yaml +ports: + - "8080:4533" # 主机使用 8080 端口 +``` + +## 初始化和使用 + +### 1. 访问 Web 界面 + +容器启动后,在浏览器中访问: + +``` +http://localhost:4533 +``` + +或者使用服务器 IP: + +``` +http://your-server-ip:4533 +``` + +### 2. 创建管理员账户 + +首次访问会提示创建管理员账户: +1. 输入用户名 +2. 设置密码 +3. 确认密码 +4. 点击创建账户 + +### 3. 添加音乐 + +将音乐文件复制到 `~/navidrome/music` 目录: + +```bash +# 复制音乐文件 +cp -r /path/to/your/music/* ~/navidrome/music/ + +# 或者创建软链接 +ln -s /path/to/your/music ~/navidrome/music/library +``` + +Navidrome 会自动扫描并导入音乐。 + +### 4. 手动触发扫描 + +如果想立即扫描新添加的音乐: + +1. 登录 Web 界面 +2. 点击右上角用户头像 +3. 选择 "Settings"(设置) +4. 在 "Library" 部分点击 "Scan Library Now"(立即扫描库) + +## 数据管理 + +### 备份数据 + +```bash +# 备份数据目录 +tar -czf navidrome-backup-$(date +%Y%m%d).tar.gz ~/navidrome/data + +# 或者只备份数据库 +cp ~/navidrome/data/navidrome.db ~/navidrome-db-backup-$(date +%Y%m%d).db +``` + +### 恢复数据 + +```bash +# 停止容器 +docker-compose down + +# 恢复数据 +tar -xzf navidrome-backup-20231223.tar.gz -C ~/ + +# 重启容器 +docker-compose up -d +``` + +### 清理缓存 + +```bash +# 进入容器 +docker exec -it navidrome sh + +# 清理缓存(在容器内执行) +rm -rf /data/cache/* + +# 退出容器 +exit + +# 重启服务 +docker-compose restart +``` + +## 使用第三方客户端 + +Navidrome 兼容 Subsonic API,可以使用多种第三方客户端: + +### Android 客户端 +- **DSub**: 功能丰富,界面友好 +- **Ultrasonic**: 开源免费 +- **Subsonic**: 官方客户端 + +### iOS 客户端 +- **play:Sub**: 界面精美 +- **substreamer**: 功能全面 +- **Amperfy**: 开源免费 + +### 桌面客户端 +- **Sublime Music**: Linux 客户端 +- **Sonixd**: 跨平台客户端 +- **Supersonic**: 现代化界面 + +### 客户端配置 + +在客户端中配置服务器信息: +- **服务器地址**: `http://your-server-ip:4533` +- **用户名**: 你的 Navidrome 用户名 +- **密码**: 你的 Navidrome 密码 + +## 高级配置 + +### 1. 使用反向代理(Nginx) + +如果使用 Nginx 反向代理: + +```nginx +server { + listen 80; + server_name music.example.com; + + location / { + proxy_pass http://localhost:4533; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket 支持 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } +} +``` + +同时在 docker-compose.yml 中设置: +```yaml +environment: + ND_BASEURL: "http://music.example.com" +``` + +### 2. 启用 HTTPS + +使用 Let's Encrypt 和 Nginx: + +```bash +# 安装 certbot +sudo apt install certbot python3-certbot-nginx + +# 获取证书 +sudo certbot --nginx -d music.example.com +``` + +### 3. 多音乐目录配置 + +```yaml +volumes: + - ./data:/data + - /mnt/music1:/music:ro + - /mnt/music2:/music2:ro + - /mnt/audiobooks:/audiobooks:ro +``` + +### 4. 集成 Spotify(获取元数据) + +1. 前往 [Spotify Developer Dashboard](https://developer.spotify.com/dashboard) +2. 创建应用获取 Client ID 和 Secret +3. 在 docker-compose.yml 中添加: + +```yaml +environment: + ND_SPOTIFY_ID: "your_client_id" + ND_SPOTIFY_SECRET: "your_client_secret" +``` + +### 5. 集成 Last.fm(音乐信息) + +1. 前往 [Last.fm API](https://www.last.fm/api/account/create) +2. 创建应用获取 API Key 和 Secret +3. 在 docker-compose.yml 中添加: + +```yaml +environment: + ND_LASTFM_ENABLED: "true" + ND_LASTFM_APIKEY: "your_api_key" + ND_LASTFM_SECRET: "your_secret" +``` + +## 常见问题 + +### 1. 无法访问 Web 界面 + +检查容器状态: +```bash +docker-compose ps +docker-compose logs +``` + +检查端口是否被占用: +```bash +sudo netstat -tulpn | grep 4533 +``` + +### 2. 音乐扫描不到 + +确认音乐目录挂载正确: +```bash +# 进入容器检查 +docker exec -it navidrome ls -la /music +``` + +检查文件权限: +```bash +# 修改音乐目录权限 +chmod -R 755 ~/navidrome/music +``` + +### 3. 转码不工作 + +确保启用了转码配置: +```yaml +environment: + ND_ENABLETRANSCODINGCONFIG: "true" +``` + +检查是否安装了 FFmpeg(Navidrome 镜像已包含)。 + +### 4. 性能优化 + +增加缓存大小: +```yaml +environment: + ND_TRANSCODINGCACHESIZE: 500MB + ND_IMAGECACHESIZE: 200MB +``` + +限制扫描频率: +```yaml +environment: + ND_SCANSCHEDULE: "0 2 * * *" # 每天凌晨2点扫描 +``` + +### 5. 数据库锁定错误 + +停止容器并修复: +```bash +docker-compose down +sqlite3 ~/navidrome/data/navidrome.db "VACUUM;" +docker-compose up -d +``` + +## 容器管理 + +### 查看容器状态 +```bash +docker-compose ps +``` + +### 查看实时日志 +```bash +docker-compose logs -f navidrome +``` + +### 重启服务 +```bash +docker-compose restart +``` + +### 更新到最新版本 +```bash +# 拉取最新镜像 +docker-compose pull + +# 重新创建容器 +docker-compose up -d + +# 清理旧镜像 +docker image prune +``` + +### 停止并删除容器 +```bash +docker-compose down +``` + +### 完全卸载(包括数据) +```bash +# 停止并删除容器 +docker-compose down -v + +# 删除数据目录 +rm -rf ~/navidrome +``` + +## 性能监控 + +### 查看资源使用 +```bash +docker stats navidrome +``` + +### 查看容器详细信息 +```bash +docker inspect navidrome +``` + +## 参考资源 + +- [Navidrome 官方网站](https://www.navidrome.org/) +- [Navidrome GitHub](https://github.com/navidrome/navidrome) +- [Navidrome 文档](https://www.navidrome.org/docs/) +- [Docker Hub - Navidrome](https://hub.docker.com/r/deluan/navidrome) +- [Subsonic API](http://www.subsonic.org/pages/api.jsp) + +## 推荐音乐格式 + +为了获得最佳体验,推荐使用以下格式: +- **高质量**: FLAC(无损) +- **平衡**: MP3 320kbps / AAC 256kbps +- **移动端**: MP3 192kbps / AAC 128kbps + +## 下一步 + +1. ✅ 安装并启动 Navidrome +2. 📁 整理音乐文件,添加正确的标签信息 +3. 🎨 优化专辑封面和艺术家信息 +4. 📱 安装并配置移动客户端 +5. 🔐 配置 HTTPS 和反向代理(生产环境) +6. 🎵 享受你的私人音乐流媒体服务! + +--- + +**提示**: Navidrome 会根据音乐文件的 ID3 标签自动组织你的音乐库,确保你的音乐文件有正确的标签信息以获得最佳体验。 diff --git a/src/programming/linux/基础/03-应用安装与快捷方式.md b/src/programming/linux/基础/03-应用安装与快捷方式.md new file mode 100644 index 0000000..2c8ea40 --- /dev/null +++ b/src/programming/linux/基础/03-应用安装与快捷方式.md @@ -0,0 +1,664 @@ +--- +title: Linux 应用安装与快捷方式 +icon: mdi:application-box +date: 2025-12-22 +category: + - Linux 基础 + - 系统管理 +tag: + - Linux + - 应用安装 + - 快捷方式 + - opt目录 + - 压缩包应用 + +# 主题相关增强 +star: false +article: true +timeline: true + +# SEO & 列表摘要 +description: >- + 详解在 Linux 系统中解压和安装便携应用的完整流程, + 包括 .tar.gz 和 .AppImage 等常见格式的处理方式, + 以及为当前用户创建快捷方式的实用技巧。 +--- + +详解在 Linux 系统中解压和安装便携应用的完整流程,包括 .tar.gz 和 .AppImage 等常见格式的处理方式,以及为当前用户创建快捷方式的实用技巧。 + + + +# Linux 应用安装与快捷方式配置完整指南 + +> 这是一份关于在 Linux 系统中安装和配置常见应用的实用指南,涵盖压缩包解压、应用移动、权限配置及快捷方式创建等完整流程。 + +--- + +## 一、应用安装目录规划 + +### 1.1 Linux 系统目录说明 + +在 Linux 中,应用安装的目录位置很重要: + +| 目录 | 用途 | 权限 | +|------|------|------| +| `/usr/local/bin` | 用户级命令行工具 | 需要 sudo | +| `/opt` | 第三方或自定义应用 | 需要 sudo(共享) | +| `~/.local/bin` | 当前用户专用命令 | 无需 sudo | +| `~/Applications` | 当前用户应用快捷方式 | 无需 sudo | + +### 1.2 不同安装方案的选择 + +**方案对比**: + +| 方案 | 安装位置 | 权限要求 | 适用场景 | +|------|---------|--------|---------| +| 系统级安装 | `/opt` 或 `/usr/local` | 需要 sudo | 多用户共享 | +| 用户级安装 | `~/.local` | 无需 sudo | 个人使用 | +| 便携式应用 | 任意位置 | 灵活 | 开发工具 | + +--- + +## 二、.tar.gz 压缩包应用安装(以 GoLand-2025.2.6 为例) + +GoLand 是 JetBrains 出品的 Go 语言 IDE,通常以 .tar.gz 格式分发。 + +### 2.1 解压应用包 + +```bash +# 1. 进入下载目录(假设文件在 ~/Downloads) +cd ~/Downloads + +# 2. 查看文件大小和详细信息 +ls -lh goland-2025.2.6.tar.gz + +# 3. 解压到临时目录 +tar -xzf goland-2025.2.6.tar.gz +# 或更详细的解压过程(显示进度) +tar -xzvf goland-2025.2.6.tar.gz + +# 解压后会生成 GoLand-2025.2 或类似名称的目录 +ls -d GoLand*/ +``` + +**tar 命令选项说明**: +- `-x`:解压(提取) +- `-z`:处理 gzip 压缩 +- `-f`:指定文件名 +- `-v`:显示解压过程(可选) + +### 2.2 移动应用到 /opt + +```bash +# 1. 检查当前用户是否有 /opt 权限 +ls -ld /opt + +# 2. 如果 /opt 不存在,需要创建 +sudo mkdir -p /opt + +# 3. 移动解压后的应用目录到 /opt +# 假设解压生成的目录名为 GoLand-2025.2 +sudo mv GoLand-2025.2 /opt/GoLand + +# 4. 验证移动结果 +ls -la /opt/GoLand +``` + +### 2.3 检查和设置执行权限 + +```bash +# 1. 进入应用目录 +cd /opt/GoLand + +# 2. 查找可执行文件(通常在 bin 目录下) +ls -la bin/ + +# 3. 如果执行权限不足,设置可执行权限 +sudo chmod +x bin/goland.sh # 主启动脚本 +sudo chmod +x bin/goland # 可能的可执行文件 + +# 4. 验证权限 +ls -la bin/goland.sh +``` + +### 2.4 创建桌面快捷方式(图形界面) + +#### 方法一:在 ~/Desktop 创建快捷方式 + +```bash +# 1. 创建 Desktop 目录(如果不存在) +mkdir -p ~/Desktop + +# 2. 创建 .desktop 文件 +cat > ~/Desktop/GoLand.desktop << 'EOF' +[Desktop Entry] +Version=1.0 +Type=Application +Name=GoLand +Exec=/opt/GoLand/bin/goland.sh +Icon=/opt/GoLand/bin/goland.png +Terminal=false +Categories=Development;IDE; +Comment=JetBrains GoLand IDE +EOF + +# 3. 设置可执行权限 +chmod +x ~/Desktop/GoLand.desktop + +# 4. 验证创建 +ls -la ~/Desktop/GoLand.desktop +``` + +#### 方法二:在 ~/.local/share/applications 创建快捷方式 + +这种方法让应用显示在应用菜单中: + +```bash +# 1. 创建目录(通常已存在) +mkdir -p ~/.local/share/applications + +# 2. 创建 .desktop 文件 +cat > ~/.local/share/applications/GoLand.desktop << 'EOF' +[Desktop Entry] +Version=1.0 +Type=Application +Name=GoLand +Exec=/opt/GoLand/bin/goland.sh +Icon=/opt/GoLand/bin/goland.png +Terminal=false +Categories=Development;IDE; +Comment=JetBrains GoLand IDE +StartupWMClass=jetbrains-goland +EOF + +# 3. 刷新应用菜单 +update-desktop-database ~/.local/share/applications + +# 4. 验证创建 +ls -la ~/.local/share/applications/GoLand.desktop +``` + +### 2.5 创建命令行快捷方式 + +```bash +# 1. 方法一:创建符号链接到 ~/.local/bin +mkdir -p ~/.local/bin +ln -s /opt/GoLand/bin/goland.sh ~/.local/bin/goland + +# 2. 确保 ~/.local/bin 在 PATH 中 +# 在 ~/.bashrc 或 ~/.bash_profile 中检查是否包含: +echo $PATH | grep -q "$HOME/.local/bin" && echo "Already in PATH" || echo "Need to add to PATH" + +# 3. 如果不在 PATH 中,添加到 ~/.bashrc +cat >> ~/.bashrc << 'EOF' +# Add local bin to PATH +if [ -d "$HOME/.local/bin" ] ; then + PATH="$HOME/.local/bin:$PATH" +fi +EOF + +# 4. 使 PATH 修改生效 +source ~/.bashrc + +# 5. 测试命令 +which goland +goland --version # 如果支持的话 +``` + +--- + +## 三、.AppImage 应用安装(以 Navicat17-premium-lite 为例) + +AppImage 是一种便携式应用格式,类似 Windows 的绿色软件,无需安装即可运行。 + +### 3.1 检查和设置权限 + +```bash +# 1. 列出 AppImage 文件信息 +ls -lh navicat17-premium-lite-cs-x86_64.AppImage + +# 2. 设置可执行权限 +chmod +x navicat17-premium-lite-cs-x86_64.AppImage + +# 3. 验证权限 +ls -la navicat17-premium-lite-cs-x86_64.AppImage +# 应该看到 x 权限标记 +``` + +### 3.2 移动 AppImage 到 /opt + +```bash +# 1. 在 /opt 中创建应用目录 +sudo mkdir -p /opt/Navicat17 + +# 2. 移动 AppImage 文件 +sudo mv navicat17-premium-lite-cs-x86_64.AppImage /opt/Navicat17/ + +# 3. 确保可执行权限(移动后可能丢失) +sudo chmod +x /opt/Navicat17/navicat17-premium-lite-cs-x86_64.AppImage + +# 4. 创建简化的启动脚本(可选但推荐) +sudo cat > /opt/Navicat17/navicat << 'EOF' +#!/bin/bash +exec /opt/Navicat17/navicat17-premium-lite-cs-x86_64.AppImage "$@" +EOF + +# 5. 设置脚本权限 +sudo chmod +x /opt/Navicat17/navicat + +# 6. 验证 +ls -la /opt/Navicat17/ +``` + +### 3.3 创建桌面快捷方式 + +#### 方法一:在 ~/Desktop + +```bash +# 1. 创建 .desktop 快捷方式文件 +cat > ~/Desktop/Navicat17.desktop << 'EOF' +[Desktop Entry] +Version=1.0 +Type=Application +Name=Navicat17 Premium +Exec=/opt/Navicat17/navicat17-premium-lite-cs-x86_64.AppImage +Icon=/opt/Navicat17/navicat.png +Terminal=false +Categories=Development;Database; +Comment=Navicat Premium Database Management Tool +StartupWMClass=Navicat +EOF + +# 2. 设置可执行权限 +chmod +x ~/Desktop/Navicat17.desktop + +# 3. 验证 +ls -la ~/Desktop/Navicat17.desktop +``` + +#### 方法二:在应用菜单 + +```bash +# 1. 创建 .desktop 文件 +cat > ~/.local/share/applications/Navicat17.desktop << 'EOF' +[Desktop Entry] +Version=1.0 +Type=Application +Name=Navicat17 Premium +Exec=/opt/Navicat17/navicat17-premium-lite-cs-x86_64.AppImage +Icon=/opt/Navicat17/navicat.png +Terminal=false +Categories=Development;Database; +Comment=Navicat Premium Database Management Tool +StartupWMClass=Navicat +EOF + +# 2. 刷新应用菜单 +update-desktop-database ~/.local/share/applications + +# 3. 验证 +ls -la ~/.local/share/applications/Navicat17.desktop +``` + +### 3.4 创建命令行快捷方式 + +```bash +# 1. 为 AppImage 创建软链接到 ~/.local/bin +mkdir -p ~/.local/bin +ln -s /opt/Navicat17/navicat17-premium-lite-cs-x86_64.AppImage ~/.local/bin/navicat + +# 2. 或创建包装脚本(更灵活) +cat > ~/.local/bin/navicat << 'EOF' +#!/bin/bash +/opt/Navicat17/navicat17-premium-lite-cs-x86_64.AppImage "$@" +EOF + +chmod +x ~/.local/bin/navicat + +# 3. 测试命令 +which navicat +navicat & # 后台启动应用 +``` + +--- + +## 四、通用安装流程总结 + +### 4.1 快速安装检查清单 + +```bash +# 1. 解压应用 +tar -xzf application.tar.gz +# 或 +chmod +x application.AppImage + +# 2. 移动到 /opt +sudo mv application /opt/ +sudo chmod -R 755 /opt/application + +# 3. 检查可执行权限 +ls -la /opt/application/bin/ +# 或 +ls -la /opt/application/*.AppImage + +# 4. 创建桌面快捷方式 +cat > ~/.local/share/applications/Application.desktop << 'EOF' +[Desktop Entry] +Version=1.0 +Type=Application +Name=ApplicationName +Exec=/opt/application/bin/launch +Icon=/opt/application/icon.png +Terminal=false +Categories=Development; +EOF + +# 5. 创建命令行快捷方式 +mkdir -p ~/.local/bin +ln -s /opt/application/bin/launch ~/.local/bin/application + +# 6. 更新 PATH(如需要) +source ~/.bashrc +``` + +### 4.2 权限问题排查 + +```bash +# 问题:无法执行应用 +# 检查: +1. 文件是否有 x 权限 + ls -la /opt/application/bin/ + +2. 是否有依赖库问题 + ldd /opt/application/bin/executable + # 如果显示 "not found",需要安装相关库 + +3. 是否是 AppImage + file application.AppImage + +# 解决: +# 设置执行权限 +chmod +x /opt/application/bin/executable + +# 安装缺失的依赖库 +sudo apt-get install libXXX libYYY # 根据 ldd 输出安装 +``` + +### 4.3 常见 .desktop 文件配置 + +```ini +[Desktop Entry] +Version=1.0 # 文件格式版本 +Type=Application # 类型:Application/Link/Directory +Name=MyApp # 应用显示名称 +Comment=Application description # 应用描述 +Exec=/opt/myapp/bin/launch # 执行命令 +Icon=/opt/myapp/icon.png # 图标路径 +Terminal=false # 是否在终端运行 +Categories=Development;IDE; # 应用分类 +StartupWMClass=myapp-class # 窗口类名(用于任务栏识别) +Keywords=development;coding; # 搜索关键词 +``` + +--- + +## 五、实战案例:完整安装流程 + +### 5.1 安装 GoLand 完整步骤 + +```bash +# 1. 进入下载目录 +cd ~/Downloads + +# 2. 解压 +tar -xzf goland-2025.2.6.tar.gz + +# 3. 移动到 /opt +sudo mv GoLand-2025.2 /opt/GoLand + +# 4. 设置权限 +sudo chmod -R 755 /opt/GoLand + +# 5. 创建应用菜单快捷方式 +cat > ~/.local/share/applications/GoLand.desktop << 'EOF' +[Desktop Entry] +Version=1.0 +Type=Application +Name=GoLand +Exec=/opt/GoLand/bin/goland.sh +Icon=goland +Terminal=false +Categories=Development;IDE; +Comment=JetBrains Go IDE +StartupWMClass=jetbrains-goland +EOF + +# 6. 创建命令行快捷方式 +mkdir -p ~/.local/bin +ln -s /opt/GoLand/bin/goland.sh ~/.local/bin/goland + +# 7. 更新应用数据库 +update-desktop-database ~/.local/share/applications + +# 8. 刷新 PATH(如需要) +source ~/.bashrc + +# 9. 测试 +which goland +goland # 启动应用 +``` + +### 5.2 安装 Navicat 完整步骤 + +```bash +# 1. 进入下载目录 +cd ~/Downloads + +# 2. 设置执行权限 +chmod +x navicat17-premium-lite-cs-x86_64.AppImage + +# 3. 创建应用目录并移动 +sudo mkdir -p /opt/Navicat17 +sudo mv navicat17-premium-lite-cs-x86_64.AppImage /opt/Navicat17/ +sudo chmod +x /opt/Navicat17/navicat17-premium-lite-cs-x86_64.AppImage + +# 4. 创建启动脚本 +sudo tee /opt/Navicat17/navicat > /dev/null << 'EOF' +#!/bin/bash +exec /opt/Navicat17/navicat17-premium-lite-cs-x86_64.AppImage "$@" +EOF +sudo chmod +x /opt/Navicat17/navicat + +# 5. 创建应用菜单快捷方式 +cat > ~/.local/share/applications/Navicat17.desktop << 'EOF' +[Desktop Entry] +Version=1.0 +Type=Application +Name=Navicat17 Premium +Exec=/opt/Navicat17/navicat +Icon=navicat +Terminal=false +Categories=Development;Database; +Comment=Database Management Tool +StartupWMClass=Navicat +EOF + +# 6. 创建命令行快捷方式 +mkdir -p ~/.local/bin +ln -s /opt/Navicat17/navicat ~/.local/bin/navicat + +# 7. 更新应用数据库 +update-desktop-database ~/.local/share/applications + +# 8. 测试 +which navicat +navicat & # 后台启动 +``` + +--- + +## 六、卸载和清理 + +### 6.1 卸载应用 + +```bash +# 1. 删除应用目录 +sudo rm -rf /opt/GoLand +sudo rm -rf /opt/Navicat17 + +# 2. 删除快捷方式 +rm ~/.local/share/applications/GoLand.desktop +rm ~/.local/share/applications/Navicat17.desktop +rm ~/Desktop/GoLand.desktop +rm ~/Desktop/Navicat17.desktop + +# 3. 删除命令行快捷方式 +rm ~/.local/bin/goland +rm ~/.local/bin/navicat + +# 4. 更新应用数据库 +update-desktop-database ~/.local/share/applications +``` + +### 6.2 清理残留文件 + +```bash +# 1. 查找应用配置目录(通常在 ~/.config) +ls -la ~/.config/ | grep -i goland +ls -la ~/.config/ | grep -i navicat + +# 2. 删除配置(可选,如需完全卸载) +rm -rf ~/.config/GoLand +rm -rf ~/.config/Navicat + +# 3. 查找缓存目录(通常在 ~/.cache) +rm -rf ~/.cache/GoLand +rm -rf ~/.cache/Navicat + +# 4. 删除下载的安装包 +rm ~/Downloads/goland-2025.2.6.tar.gz +rm ~/Downloads/navicat17-premium-lite-cs-x86_64.AppImage +``` + +--- + +## 七、常见问题解答 + +### Q1:解压后文件权限不对? + +**A**:设置正确的权限: +```bash +sudo chmod -R 755 /opt/application # 所有用户可读可执行 +sudo chmod -R u+rwx /opt/application # 仅所有者可读写执行 +``` + +### Q2:创建的快捷方式无法找到? + +**A**:更新应用菜单缓存: +```bash +update-desktop-database ~/.local/share/applications +# 或强制重建 +rm ~/.cache/application-registry.cache +``` + +### Q3:AppImage 无法运行,提示权限问题? + +**A**:确保执行权限和依赖库: +```bash +chmod +x application.AppImage +ldd application.AppImage # 检查依赖 +sudo apt-get install -y libfuse2 # 通常需要的依赖 +``` + +### Q4:命令行启动应用后立即返回? + +**A**:使用后台启动或添加 `&`: +```bash +goland & # 后台启动 +nohup goland & # 忽略挂起信号 +``` + +### Q5:PATH 问题导致命令找不到? + +**A**:检查和修复 PATH: +```bash +echo $PATH # 查看当前 PATH +which application # 查找应用 +# 在 ~/.bashrc 中检查或添加: +export PATH="$HOME/.local/bin:$PATH" +``` + +--- + +## 八、最佳实践建议 + +### 8.1 安装前检查 + +- ✅ 确保有足够的磁盘空间(`df -h`) +- ✅ 检查系统架构是否匹配(`uname -m`) +- ✅ 下载文件的完整性验证(如有 checksum) + +### 8.2 安装中遵循 + +- ✅ 优先使用 `/opt` 目录用于系统级应用 +- ✅ 为每个应用创建独立目录 +- ✅ 始终保持正确的权限设置 +- ✅ 同时创建图形和命令行快捷方式 + +### 8.3 安装后维护 + +- ✅ 定期检查应用更新 +- ✅ 保留原始安装包以便升级 +- ✅ 记录安装位置和配置路径 +- ✅ 定期备份应用配置文件 + +--- + +## 九、快速参考命令 + +```bash +# 解压 tar.gz +tar -xzf file.tar.gz + +# 移动到 /opt +sudo mv application /opt/ + +# 设置权限 +sudo chmod -R 755 /opt/application + +# 创建桌面快捷方式 +cat > ~/.local/share/applications/App.desktop << 'EOF' +[Desktop Entry] +Version=1.0 +Type=Application +Name=App +Exec=/opt/application/bin/launch +Terminal=false +EOF + +# 创建命令行快捷方式 +ln -s /opt/application/bin/launch ~/.local/bin/app + +# 刷新应用菜单 +update-desktop-database ~/.local/share/applications + +# 卸载应用 +sudo rm -rf /opt/application +rm ~/.local/share/applications/App.desktop +rm ~/.local/bin/app +``` + +--- + +## 总结 + +通过本指南,你已经学会了: + +1. **理解 Linux 应用安装目录结构** - 知道什么应用应该放在哪里 +2. **处理 .tar.gz 压缩包** - 解压、移动和配置的完整流程 +3. **安装 AppImage 应用** - 便携式应用的特殊处理方法 +4. **创建快捷方式** - 图形界面和命令行的双重快捷方式 +5. **问题诊断和解决** - 常见问题的排查方法 + +这些技能将让你能够灵活安装和管理 Linux 系统中的各类应用,大大提高工作效率! + diff --git a/src/programming/linux/基础/nginx基础入门.md b/src/programming/linux/基础/04-nginx基础入门.md similarity index 100% rename from src/programming/linux/基础/nginx基础入门.md rename to src/programming/linux/基础/04-nginx基础入门.md diff --git a/src/work/log/2025-12.md b/src/work/log/2025-12.md new file mode 100644 index 0000000..e69de29