【GO】Gin 框架从入门到精通完整教程
Gin 框架从入门到精通完整教程
目录
1. Gin 框架简介
1.1 什么是 Gin?
Gin 是一个用 Go 语言编写的 Web 框架,具有以下特点:
- 高性能:基于 httprouter,性能比其他框架快 40 倍
- 中间件支持:内置中间件机制,易于扩展
- 路由分组:支持路由分组,便于管理
- 错误管理:提供便捷的错误收集机制
- JSON 验证:内置 JSON 验证功能
- 渲染支持:支持 JSON、XML、HTML 等多种渲染方式
1.2 为什么选择 Gin?
性能对比(请求/秒): - Gin: 30,000+ - Echo: 28,000+ - Beego: 15,000+ - Martini: 3,000+ 1.3 适用场景
- RESTful API 开发
- 微服务架构
- Web 应用后端
- 实时通信服务
- 高并发场景
2. 环境搭建
2.1 安装 Go 语言
# 下载 Go(访问 https://golang.org/dl/)# Linux/Mac 安装wget https://golang.org/dl/go1.21.0.linux-amd64.tar.gz sudotar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz # 配置环境变量exportPATH=$PATH:/usr/local/go/bin exportGOPATH=$HOME/go exportGO111MODULE=on exportGOPROXY=https://goproxy.cn,direct # 验证安装 go version 2.2 安装 Gin 框架
# 创建项目目录mkdir gin-tutorial cd gin-tutorial # 初始化 Go 模块 go mod init gin-tutorial # 安装 Gin go get -u github.com/gin-gonic/gin 2.3 IDE 推荐
- GoLand(JetBrains,付费)
- VS Code(免费,推荐插件:Go、REST Client)
- Vim/Neovim(配合 vim-go)
3. 快速入门
3.1 第一个 Gin 应用
// main.gopackage main import("github.com/gin-gonic/gin""net/http")funcmain(){// 创建默认的 Gin 引擎(包含 Logger 和 Recovery 中间件) r := gin.Default()// 定义路由 r.GET("/",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"message":"Hello, Gin!",})})// 启动服务器(默认端口 8080) r.Run(":8080")}运行应用:
go run main.go 访问 http://localhost:8080/,你将看到 JSON 响应。
3.2 不使用默认中间件
package main import("github.com/gin-gonic/gin""net/http")funcmain(){// 创建不带中间件的引擎 r := gin.New()// 手动添加中间件 r.Use(gin.Logger()) r.Use(gin.Recovery()) r.GET("/ping",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"message":"pong",})}) r.Run(":8080")}3.3 自定义端口和地址
// 方式 1:使用 Run r.Run(":3000")// 方式 2:使用 RunTLS(HTTPS) r.RunTLS(":443","cert.pem","key.pem")// 方式 3:使用自定义 HTTP 服务器 server :=&http.Server{ Addr:":8080", Handler: r, ReadTimeout:10* time.Second, WriteTimeout:10* time.Second, MaxHeaderBytes:1<<20,} server.ListenAndServe()4. 路由系统
4.1 基本路由
package main import("github.com/gin-gonic/gin""net/http")funcmain(){ r := gin.Default()// GET 请求 r.GET("/get",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"method":"GET"})})// POST 请求 r.POST("/post",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"method":"POST"})})// PUT 请求 r.PUT("/put",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"method":"PUT"})})// DELETE 请求 r.DELETE("/delete",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"method":"DELETE"})})// PATCH 请求 r.PATCH("/patch",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"method":"PATCH"})})// HEAD 请求 r.HEAD("/head",func(c *gin.Context){ c.Status(http.StatusOK)})// OPTIONS 请求 r.OPTIONS("/options",func(c *gin.Context){ c.Status(http.StatusOK)}) r.Run(":8080")}4.2 路由参数
funcmain(){ r := gin.Default()// 路径参数 r.GET("/user/:name",func(c *gin.Context){ name := c.Param("name") c.JSON(http.StatusOK, gin.H{"name": name,})})// 多个路径参数 r.GET("/user/:name/:id",func(c *gin.Context){ name := c.Param("name") id := c.Param("id") c.JSON(http.StatusOK, gin.H{"name": name,"id": id,})})// 通配符参数(匹配所有路径) r.GET("/files/*filepath",func(c *gin.Context){ filepath := c.Param("filepath") c.JSON(http.StatusOK, gin.H{"filepath": filepath,})}) r.Run(":8080")}4.3 查询参数
funcmain(){ r := gin.Default()// 获取查询参数 r.GET("/search",func(c *gin.Context){// 获取单个参数 query := c.Query("q")// 获取参数(带默认值) page := c.DefaultQuery("page","1")// 获取参数(返回是否存在) sort, exists := c.GetQuery("sort") c.JSON(http.StatusOK, gin.H{"query": query,"page": page,"sort": sort,"exists": exists,})})// 获取数组参数 r.GET("/tags",func(c *gin.Context){ tags := c.QueryArray("tag") c.JSON(http.StatusOK, gin.H{"tags": tags,})}) r.Run(":8080")}4.4 路由分组
funcmain(){ r := gin.Default()// API v1 分组 v1 := r.Group("/api/v1"){ v1.GET("/users", getUsers) v1.GET("/users/:id", getUser) v1.POST("/users", createUser) v1.PUT("/users/:id", updateUser) v1.DELETE("/users/:id", deleteUser)}// API v2 分组 v2 := r.Group("/api/v2"){ v2.GET("/users", getUsersV2) v2.POST("/users", createUserV2)}// 管理员路由分组(带中间件) admin := r.Group("/admin") admin.Use(AuthMiddleware()){ admin.GET("/dashboard", getDashboard) admin.GET("/users", getAdminUsers)} r.Run(":8080")}funcgetUsers(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"version":"v1","users":[]string{}})}funcgetUser(c *gin.Context){ id := c.Param("id") c.JSON(http.StatusOK, gin.H{"id": id})}funccreateUser(c *gin.Context){ c.JSON(http.StatusCreated, gin.H{"message":"User created"})}funcupdateUser(c *gin.Context){ id := c.Param("id") c.JSON(http.StatusOK, gin.H{"message":"User updated","id": id})}funcdeleteUser(c *gin.Context){ id := c.Param("id") c.JSON(http.StatusOK, gin.H{"message":"User deleted","id": id})}funcgetUsersV2(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"version":"v2","users":[]string{}})}funccreateUserV2(c *gin.Context){ c.JSON(http.StatusCreated, gin.H{"message":"User created (v2)"})}funcgetDashboard(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"dashboard":"data"})}funcgetAdminUsers(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"admin_users":[]string{}})}funcAuthMiddleware() gin.HandlerFunc {returnfunc(c *gin.Context){// 认证逻辑 token := c.GetHeader("Authorization")if token ==""{ c.JSON(http.StatusUnauthorized, gin.H{"error":"Unauthorized"}) c.Abort()return} c.Next()}}4.5 路由注册的其他方式
funcmain(){ r := gin.Default()// Any 方法(匹配所有 HTTP 方法) r.Any("/any",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"method": c.Request.Method,})})// 静态文件服务 r.Static("/assets","./assets") r.StaticFS("/static", http.Dir("./static")) r.StaticFile("/favicon.ico","./favicon.ico")// NoRoute(404 处理) r.NoRoute(func(c *gin.Context){ c.JSON(http.StatusNotFound, gin.H{"error":"Page not found",})}) r.Run(":8080")}5. 请求处理
5.1 获取表单数据
funcmain(){ r := gin.Default()// 单个表单字段 r.POST("/form",func(c *gin.Context){ username := c.PostForm("username") password := c.DefaultPostForm("password","default") c.JSON(http.StatusOK, gin.H{"username": username,"password": password,})})// 表单数组 r.POST("/form-array",func(c *gin.Context){ hobbies := c.PostFormArray("hobby") c.JSON(http.StatusOK, gin.H{"hobbies": hobbies,})})// 表单 Map r.POST("/form-map",func(c *gin.Context){ ids := c.QueryMap("ids") names := c.PostFormMap("names") c.JSON(http.StatusOK, gin.H{"ids": ids,"names": names,})}) r.Run(":8080")}5.2 绑定 JSON 数据
type User struct{ Username string`json:"username" binding:"required"` Password string`json:"password" binding:"required,min=6"` Email string`json:"email" binding:"required,email"` Age int`json:"age" binding:"gte=0,lte=130"`}funcmain(){ r := gin.Default() r.POST("/user",func(c *gin.Context){var user User // 绑定 JSON 数据if err := c.ShouldBindJSON(&user); err !=nil{ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(),})return} c.JSON(http.StatusOK, gin.H{"message":"User created","user": user,})}) r.Run(":8080")}5.3 绑定 XML 数据
type Article struct{ XMLName xml.Name `xml:"article"` Title string`xml:"title" binding:"required"` Content string`xml:"content" binding:"required"` Author string`xml:"author"`}funcmain(){ r := gin.Default() r.POST("/article",func(c *gin.Context){var article Article if err := c.ShouldBindXML(&article); err !=nil{ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(),})return} c.JSON(http.StatusOK, article)}) r.Run(":8080")}5.4 绑定查询参数和表单
type SearchQuery struct{ Query string`form:"q" binding:"required"` Page int`form:"page" binding:"gte=1"` PageSize int`form:"page_size" binding:"gte=1,lte=100"` Sort string`form:"sort"`}funcmain(){ r := gin.Default() r.GET("/search",func(c *gin.Context){var query SearchQuery if err := c.ShouldBindQuery(&query); err !=nil{ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(),})return} c.JSON(http.StatusOK, gin.H{"query": query.Query,"page": query.Page,"page_size": query.PageSize,"sort": query.Sort,})}) r.Run(":8080")}5.5 绑定 URI 参数
type UserID struct{ ID int`uri:"id" binding:"required,gte=1"`}funcmain(){ r := gin.Default() r.GET("/user/:id",func(c *gin.Context){var userID UserID if err := c.ShouldBindUri(&userID); err !=nil{ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(),})return} c.JSON(http.StatusOK, gin.H{"user_id": userID.ID,})}) r.Run(":8080")}5.6 自定义验证器
import("github.com/gin-gonic/gin/binding""github.com/go-playground/validator/v10")// 自定义验证函数funccustomValidator(fl validator.FieldLevel)bool{ value := fl.Field().String()return value =="admin"|| value =="user"}type RegisterForm struct{ Username string`json:"username" binding:"required"` Role string`json:"role" binding:"required,customRole"`}funcmain(){ r := gin.Default()// 注册自定义验证器if v, ok := binding.Validator.Engine().(*validator.Validate); ok { v.RegisterValidation("customRole", customValidator)} r.POST("/register",func(c *gin.Context){var form RegisterForm if err := c.ShouldBindJSON(&form); err !=nil{ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error(),})return} c.JSON(http.StatusOK, gin.H{"message":"Registration successful","user": form,})}) r.Run(":8080")}6. 响应处理
6.1 JSON 响应
funcmain(){ r := gin.Default()// 使用 gin.H r.GET("/json1",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"message":"Hello","status":200,})})// 使用结构体 r.GET("/json2",func(c *gin.Context){type Response struct{ Message string`json:"message"` Status int`json:"status"`} c.JSON(http.StatusOK, Response{ Message:"Hello", Status:200,})})// 使用 Map r.GET("/json3",func(c *gin.Context){ data :=map[string]interface{}{"message":"Hello","status":200,} c.JSON(http.StatusOK, data)})// SecureJSON(防止 JSON 劫持) r.GET("/secure-json",func(c *gin.Context){ c.SecureJSON(http.StatusOK, gin.H{"data":"sensitive data",})})// JSONP r.GET("/jsonp",func(c *gin.Context){ c.JSONP(http.StatusOK, gin.H{"message":"Hello JSONP",})})// AsciiJSON(转义非 ASCII 字符) r.GET("/ascii-json",func(c *gin.Context){ c.AsciiJSON(http.StatusOK, gin.H{"message":"你好,世界",})})// PureJSON(不转义 HTML 字符) r.GET("/pure-json",func(c *gin.Context){ c.PureJSON(http.StatusOK, gin.H{"html":"<b>Hello</b>",})}) r.Run(":8080")}6.2 XML 响应
funcmain(){ r := gin.Default() r.GET("/xml",func(c *gin.Context){type User struct{ XMLName xml.Name `xml:"user"` Name string`xml:"name"` Age int`xml:"age"`} c.XML(http.StatusOK, User{ Name:"John", Age:30,})}) r.Run(":8080")}6.3 HTML 响应
funcmain(){ r := gin.Default()// 加载 HTML 模板 r.LoadHTMLGlob("templates/*") r.GET("/html",func(c *gin.Context){ c.HTML(http.StatusOK,"index.html", gin.H{"title":"Gin Tutorial","name":"John",})}) r.Run(":8080")}模板文件 templates/index.html:
<!DOCTYPEhtml><html><head><title>{{ .title }}</title></head><body><h1>Hello, {{ .name }}!</h1></body></html>6.4 文件响应
funcmain(){ r := gin.Default()// 返回文件 r.GET("/file",func(c *gin.Context){ c.File("./files/document.pdf")})// 文件下载 r.GET("/download",func(c *gin.Context){ c.FileAttachment("./files/document.pdf","my-document.pdf")})// 从文件系统返回 r.GET("/fs",func(c *gin.Context){ c.FileFromFS("document.pdf", http.Dir("./files"))}) r.Run(":8080")}6.5 重定向
funcmain(){ r := gin.Default()// HTTP 重定向 r.GET("/redirect",func(c *gin.Context){ c.Redirect(http.StatusMovedPermanently,"https://www.google.com")})// 路由重定向 r.GET("/old-path",func(c *gin.Context){ c.Request.URL.Path ="/new-path" r.HandleContext(c)}) r.GET("/new-path",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"message":"New path",})}) r.Run(":8080")}6.6 流式响应
funcmain(){ r := gin.Default() r.GET("/stream",func(c *gin.Context){ c.Stream(func(w io.Writer)bool{for i :=0; i <10; i++{ fmt.Fprintf(w,"data: %d\n\n", i) time.Sleep(time.Second)}returnfalse})}) r.Run(":8080")}7. 中间件
7.1 全局中间件
funcLogger() gin.HandlerFunc {returnfunc(c *gin.Context){ t := time.Now()// 设置变量 c.Set("example","12345")// 请求前 log.Printf("Before request") c.Next()// 请求后 latency := time.Since(t) log.Printf("Latency: %v", latency) status := c.Writer.Status() log.Printf("Status: %d", status)}}funcmain(){ r := gin.New()// 使用全局中间件 r.Use(Logger()) r.Use(gin.Recovery()) r.GET("/test",func(c *gin.Context){ example := c.MustGet("example").(string) c.JSON(http.StatusOK, gin.H{"example": example,})}) r.Run(":8080")}7.2 路由级中间件
funcAuthRequired() gin.HandlerFunc {returnfunc(c *gin.Context){ token := c.GetHeader("Authorization")if token ==""{ c.JSON(http.StatusUnauthorized, gin.H{"error":"Authorization required",}) c.Abort()return}// 验证 tokenif token !="valid-token"{ c.JSON(http.StatusUnauthorized, gin.H{"error":"Invalid token",}) c.Abort()return} c.Next()}}funcmain(){ r := gin.Default()// 公开路由 r.GET("/public",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"message":"Public endpoint",})})// 受保护的路由 r.GET("/protected",AuthRequired(),func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"message":"Protected endpoint",})}) r.Run(":8080")}7.3 分组中间件
funcmain(){ r := gin.Default()// 公开 API public := r.Group("/api/public"){ public.GET("/info", getInfo)}// 需要认证的 API authorized := r.Group("/api/private") authorized.Use(AuthRequired()){ authorized.GET("/profile", getProfile) authorized.POST("/update", updateProfile)}// 管理员 API admin := r.Group("/api/admin") admin.Use(AuthRequired(),AdminRequired()){ admin.GET("/users", getAllUsers) admin.DELETE("/user/:id", deleteUser)} r.Run(":8080")}funcgetInfo(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"info":"public"})}funcgetProfile(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"profile":"user profile"})}funcupdateProfile(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"message":"Profile updated"})}funcgetAllUsers(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"users":[]string{}})}funcAdminRequired() gin.HandlerFunc {returnfunc(c *gin.Context){ role := c.GetHeader("X-User-Role")if role !="admin"{ c.JSON(http.StatusForbidden, gin.H{"error":"Admin access required",}) c.Abort()return} c.Next()}}7.4 CORS 中间件
funcCORSMiddleware() gin.HandlerFunc {returnfunc(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, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") c.Writer.Header().Set("Access-Control-Allow-Methods","POST, OPTIONS, GET, PUT, DELETE")if c.Request.Method =="OPTIONS"{ c.AbortWithStatus(204)return} c.Next()}}funcmain(){ r := gin.Default() r.Use(CORSMiddleware()) r.GET("/api/data",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"data":"CORS enabled",})}) r.Run(":8080")}7.5 限流中间件
import("golang.org/x/time/rate""sync")funcRateLimitMiddleware(r rate.Limit, b int) gin.HandlerFunc { limiters :=&sync.Map{}returnfunc(c *gin.Context){ ip := c.ClientIP() limiterInterface,_:= limiters.LoadOrStore(ip, rate.NewLimiter(r, b)) limiter := limiterInterface.(*rate.Limiter)if!limiter.Allow(){ c.JSON(http.StatusTooManyRequests, gin.H{"error":"Too many requests",}) c.Abort()return} c.Next()}}funcmain(){ r := gin.Default()// 每秒最多 5 个请求,突发 10 个 r.Use(RateLimitMiddleware(5,10)) r.GET("/api/data",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"message":"Success",})}) r.Run(":8080")}8. 数据验证(续)
8.1 基本验证标签
type User struct{// 必填字段 Username string`json:"username" binding:"required"`// 最小长度 Password string`json:"password" binding:"required,min=6"`// 最大长度 Nickname string`json:"nickname" binding:"max=20"`// 长度范围 Code string`json:"code" binding:"len=6"`// 邮箱格式 Email string`json:"email" binding:"required,email"`// URL 格式 Website string`json:"website" binding:"url"`// 数字范围 Age int`json:"age" binding:"gte=0,lte=130"`// 枚举值 Gender string`json:"gender" binding:"oneof=male female other"`// IP 地址 IP string`json:"ip" binding:"ip"`// 日期时间 Birthday time.Time `json:"birthday" binding:"required"`}8.2 常用验证标签
type Product struct{// 字符串验证 Name string`binding:"required,min=3,max=100"` Description string`binding:"omitempty,max=500"` SKU string`binding:"required,alphanum"`// 数字验证 Price float64`binding:"required,gt=0"` Stock int`binding:"required,gte=0"` Discount float64`binding:"omitempty,gte=0,lte=100"`// 数组验证 Tags []string`binding:"required,min=1,max=10,dive,min=2,max=20"`// 嵌套结构验证 Category Category `binding:"required"`}type Category struct{ ID int`binding:"required,gt=0"` Name string`binding:"required,min=2,max=50"`}8.3 自定义错误消息
type LoginForm struct{ Username string`json:"username" binding:"required"` Password string`json:"password" binding:"required,min=6"`}funcmain(){ r := gin.Default() r.POST("/login",func(c *gin.Context){var form LoginForm if err := c.ShouldBindJSON(&form); err !=nil{// 自定义错误消息 errors :=make(map[string]string)for_, fieldErr :=range err.(validator.ValidationErrors){ field := fieldErr.Field() tag := fieldErr.Tag()switch field {case"Username":if tag =="required"{ errors[field]="用户名不能为空"}case"Password":if tag =="required"{ errors[field]="密码不能为空"}elseif tag =="min"{ errors[field]="密码长度至少为 6 位"}}} c.JSON(http.StatusBadRequest, gin.H{"errors": errors,})return} c.JSON(http.StatusOK, gin.H{"message":"登录成功",})}) r.Run(":8080")}9. 数据库集成
9.1 使用 GORM
安装 GORM:
go get -u gorm.io/gorm go get -u gorm.io/driver/mysql go get -u gorm.io/driver/postgres go get -u gorm.io/driver/sqlite 9.2 连接数据库
package main import("github.com/gin-gonic/gin""gorm.io/driver/mysql""gorm.io/gorm""log""net/http")var DB *gorm.DB type User struct{ ID uint`gorm:"primaryKey" json:"id"` Username string`gorm:"unique;not null" json:"username"` Email string`gorm:"unique;not null" json:"email"` Password string`gorm:"not null" json:"-"` Age int`json:"age"`}funcInitDB(){ dsn :="user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"var err error DB, err = gorm.Open(mysql.Open(dsn),&gorm.Config{})if err !=nil{ log.Fatal("Failed to connect to database:", err)}// 自动迁移 DB.AutoMigrate(&User{})}funcmain(){InitDB() r := gin.Default()// CRUD 操作 r.POST("/users", createUser) r.GET("/users", getUsers) r.GET("/users/:id", getUser) r.PUT("/users/:id", updateUser) r.DELETE("/users/:id", deleteUser) r.Run(":8080")}funccreateUser(c *gin.Context){var user User if err := c.ShouldBindJSON(&user); err !=nil{ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}if err := DB.Create(&user).Error; err !=nil{ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})return} c.JSON(http.StatusCreated, user)}funcgetUsers(c *gin.Context){var users []User // 分页 page := c.DefaultQuery("page","1") pageSize := c.DefaultQuery("page_size","10")var total int64 DB.Model(&User{}).Count(&total) DB.Offset((atoi(page)-1)*atoi(pageSize)).Limit(atoi(pageSize)).Find(&users) c.JSON(http.StatusOK, gin.H{"data": users,"total": total,"page": page,})}funcgetUser(c *gin.Context){ id := c.Param("id")var user User if err := DB.First(&user, id).Error; err !=nil{ c.JSON(http.StatusNotFound, gin.H{"error":"User not found"})return} c.JSON(http.StatusOK, user)}funcupdateUser(c *gin.Context){ id := c.Param("id")var user User if err := DB.First(&user, id).Error; err !=nil{ c.JSON(http.StatusNotFound, gin.H{"error":"User not found"})return}if err := c.ShouldBindJSON(&user); err !=nil{ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return} DB.Save(&user) c.JSON(http.StatusOK, user)}funcdeleteUser(c *gin.Context){ id := c.Param("id")if err := DB.Delete(&User{}, id).Error; err !=nil{ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})return} c.JSON(http.StatusOK, gin.H{"message":"User deleted"})}funcatoi(s string)int{ i,_:= strconv.Atoi(s)return i }9.3 关联查询
type User struct{ ID uint`gorm:"primaryKey"` Username string`gorm:"unique"` Posts []Post `gorm:"foreignKey:UserID"` Profile Profile `gorm:"foreignKey:UserID"`}type Post struct{ ID uint`gorm:"primaryKey"` Title string Content string UserID uint User User `gorm:"foreignKey:UserID"`}type Profile struct{ ID uint`gorm:"primaryKey"` Bio string Avatar string UserID uint}funcgetUserWithPosts(c *gin.Context){ id := c.Param("id")var user User // 预加载关联数据if err := DB.Preload("Posts").Preload("Profile").First(&user, id).Error; err !=nil{ c.JSON(http.StatusNotFound, gin.H{"error":"User not found"})return} c.JSON(http.StatusOK, user)}10. 文件操作
10.1 单文件上传
funcmain(){ r := gin.Default()// 设置文件上传大小限制(默认 32MB) r.MaxMultipartMemory =8<<20// 8 MB r.POST("/upload",func(c *gin.Context){// 获取上传的文件 file, err := c.FormFile("file")if err !=nil{ c.JSON(http.StatusBadRequest, gin.H{"error":"No file uploaded",})return}// 验证文件类型if!isAllowedFileType(file.Filename){ c.JSON(http.StatusBadRequest, gin.H{"error":"File type not allowed",})return}// 生成唯一文件名 filename :=generateUniqueFilename(file.Filename) filepath :="./uploads/"+ filename // 保存文件if err := c.SaveUploadedFile(file, filepath); err !=nil{ c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to save file",})return} c.JSON(http.StatusOK, gin.H{"message":"File uploaded successfully","filename": filename,"size": file.Size,})}) r.Run(":8080")}funcisAllowedFileType(filename string)bool{ allowedTypes :=[]string{".jpg",".jpeg",".png",".gif",".pdf"} ext := strings.ToLower(filepath.Ext(filename))for_, allowed :=range allowedTypes {if ext == allowed {returntrue}}returnfalse}funcgenerateUniqueFilename(originalName string)string{ ext := filepath.Ext(originalName) name := strings.TrimSuffix(originalName, ext) timestamp := time.Now().Unix()return fmt.Sprintf("%s_%d%s", name, timestamp, ext)}10.2 多文件上传
funcmain(){ r := gin.Default() r.POST("/upload-multiple",func(c *gin.Context){ form, err := c.MultipartForm()if err !=nil{ c.JSON(http.StatusBadRequest, gin.H{"error":"Failed to parse form",})return} files := form.File["files"]var uploadedFiles []stringfor_, file :=range files { filename :=generateUniqueFilename(file.Filename) filepath :="./uploads/"+ filename if err := c.SaveUploadedFile(file, filepath); err !=nil{ c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to save file: "+ file.Filename,})return} uploadedFiles =append(uploadedFiles, filename)} c.JSON(http.StatusOK, gin.H{"message":"Files uploaded successfully","files": uploadedFiles,"count":len(uploadedFiles),})}) r.Run(":8080")}10.3 文件下载
funcmain(){ r := gin.Default() r.GET("/download/:filename",func(c *gin.Context){ filename := c.Param("filename") filepath :="./uploads/"+ filename // 检查文件是否存在if_, err := os.Stat(filepath); os.IsNotExist(err){ c.JSON(http.StatusNotFound, gin.H{"error":"File not found",})return}// 设置下载文件名 c.FileAttachment(filepath, filename)}) r.Run(":8080")}11. 会话管理
11.1 使用 Cookie
funcmain(){ r := gin.Default() r.GET("/cookie/set",func(c *gin.Context){ c.SetCookie("session_id",// name"abc123",// value3600,// maxAge (秒)"/",// path"localhost",// domainfalse,// securetrue,// httpOnly) c.JSON(http.StatusOK, gin.H{"message":"Cookie set"})}) r.GET("/cookie/get",func(c *gin.Context){ sessionID, err := c.Cookie("session_id")if err !=nil{ c.JSON(http.StatusNotFound, gin.H{"error":"Cookie not found",})return} c.JSON(http.StatusOK, gin.H{"session_id": sessionID})}) r.GET("/cookie/delete",func(c *gin.Context){ c.SetCookie("session_id","",-1,"/","localhost",false,true) c.JSON(http.StatusOK, gin.H{"message":"Cookie deleted"})}) r.Run(":8080")}11.2 使用 Session(gin-contrib/sessions)
go get github.com/gin-contrib/sessions go get github.com/gin-contrib/sessions/cookie import("github.com/gin-contrib/sessions""github.com/gin-contrib/sessions/cookie")funcmain(){ r := gin.Default()// 创建 cookie store store := cookie.NewStore([]byte("secret-key")) r.Use(sessions.Sessions("mysession", store)) r.POST("/login",func(c *gin.Context){ session := sessions.Default(c)var loginForm struct{ Username string`json:"username" binding:"required"` Password string`json:"password" binding:"required"`}if err := c.ShouldBindJSON(&loginForm); err !=nil{ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}// 验证用户(示例)if loginForm.Username =="admin"&& loginForm.Password =="password"{ session.Set("user_id",1) session.Set("username", loginForm.Username) session.Save() c.JSON(http.StatusOK, gin.H{"message":"Login successful"})}else{ c.JSON(http.StatusUnauthorized, gin.H{"error":"Invalid credentials"})}}) r.GET("/profile",func(c *gin.Context){ session := sessions.Default(c) userID := session.Get("user_id")if userID ==nil{ c.JSON(http.StatusUnauthorized, gin.H{"error":"Not logged in"})return} username := session.Get("username") c.JSON(http.StatusOK, gin.H{"user_id": userID,"username": username,})}) r.POST("/logout",func(c *gin.Context){ session := sessions.Default(c) session.Clear() session.Save() c.JSON(http.StatusOK, gin.H{"message":"Logged out"})}) r.Run(":8080")}11.3 JWT 认证
go get github.com/golang-jwt/jwt/v5 import("github.com/golang-jwt/jwt/v5""time")var jwtSecret =[]byte("your-secret-key")type Claims struct{ UserID uint`json:"user_id"` Username string`json:"username"` jwt.RegisteredClaims }funcGenerateToken(userID uint, username string)(string,error){ claims := Claims{ UserID: userID, Username: username, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(24* time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), Issuer:"gin-app",},} token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)return token.SignedString(jwtSecret)}funcParseToken(tokenString string)(*Claims,error){ token, err := jwt.ParseWithClaims(tokenString,&Claims{},func(token *jwt.Token)(interface{},error){return jwtSecret,nil})if err !=nil{returnnil, err }if claims, ok := token.Claims.(*Claims); ok && token.Valid {return claims,nil}returnnil, jwt.ErrSignatureInvalid }funcJWTAuthMiddleware() gin.HandlerFunc {returnfunc(c *gin.Context){ tokenString := c.GetHeader("Authorization")if tokenString ==""{ c.JSON(http.StatusUnauthorized, gin.H{"error":"Authorization header required"}) c.Abort()return}// 移除 "Bearer " 前缀iflen(tokenString)>7&& tokenString[:7]=="Bearer "{ tokenString = tokenString[7:]} claims, err :=ParseToken(tokenString)if err !=nil{ c.JSON(http.StatusUnauthorized, gin.H{"error":"Invalid token"}) c.Abort()return} c.Set("user_id", claims.UserID) c.Set("username", claims.Username) c.Next()}}funcmain(){ r := gin.Default() r.POST("/login",func(c *gin.Context){var loginForm struct{ Username string`json:"username" binding:"required"` Password string`json:"password" binding:"required"`}if err := c.ShouldBindJSON(&loginForm); err !=nil{ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}// 验证用户(示例)if loginForm.Username =="admin"&& loginForm.Password =="password"{ token, err :=GenerateToken(1, loginForm.Username)if err !=nil{ c.JSON(http.StatusInternalServerError, gin.H{"error":"Failed to generate token"})return} c.JSON(http.StatusOK, gin.H{"token": token,})}else{ c.JSON(http.StatusUnauthorized, gin.H{"error":"Invalid credentials"})}})// 受保护的路由 authorized := r.Group("/api") authorized.Use(JWTAuthMiddleware()){ authorized.GET("/profile",func(c *gin.Context){ userID := c.GetUint("user_id") username := c.GetString("username") c.JSON(http.StatusOK, gin.H{"user_id": userID,"username": username,})})} r.Run(":8080")}12. 错误处理
12.1 统一错误处理
type APIError struct{ Code int`json:"code"` Message string`json:"message"`}funcErrorHandler() gin.HandlerFunc {returnfunc(c *gin.Context){ c.Next()// 检查是否有错误iflen(c.Errors)>0{ err := c.Errors.Last()// 根据错误类型返回不同的响应switch err.Type {case gin.ErrorTypeBind: c.JSON(http.StatusBadRequest, APIError{ Code:400, Message: err.Error(),})case gin.ErrorTypePublic: c.JSON(http.StatusInternalServerError, APIError{ Code:500, Message: err.Error(),})default: c.JSON(http.StatusInternalServerError, APIError{ Code:500, Message:"Internal server error",})}}}}funcmain(){ r := gin.Default() r.Use(ErrorHandler()) r.GET("/error",func(c *gin.Context){// 添加错误 c.Error(errors.New("Something went wrong"))}) r.Run(":8080")}12.2 自定义错误类型
type AppError struct{ Code int Message string Err error}func(e *AppError)Error()string{if e.Err !=nil{return fmt.Sprintf("%s: %v", e.Message, e.Err)}return e.Message }funcNewAppError(code int, message string, err error)*AppError {return&AppError{ Code: code, Message: message, Err: err,}}funcHandleError(c *gin.Context, err error){if appErr, ok := err.(*AppError); ok { c.JSON(appErr.Code, gin.H{"error": appErr.Message,})}else{ c.JSON(http.StatusInternalServerError, gin.H{"error":"Internal server error",})}}13. 日志系统
13.1 自定义日志格式
funcmain(){ r := gin.New()// 自定义日志格式 r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams)string{return fmt.Sprintf("[%s] %s %s %d %s %s\n", param.TimeStamp.Format("2006-01-02 15:04:05"), param.Method, param.Path, param.StatusCode, param.Latency, param.ErrorMessage,)})) r.Use(gin.Recovery()) r.GET("/ping",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"message":"pong"})}) r.Run(":8080")}13.2 日志写入文件
funcmain(){// 创建日志文件 f,_:= os.Create("gin.log") gin.DefaultWriter = io.MultiWriter(f, os.Stdout) r := gin.Default() r.GET("/ping",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"message":"pong"})}) r.Run(":8080")}13.3 使用 Zap 日志库
go get -u go.uber.org/zap import("go.uber.org/zap")var logger *zap.Logger funcInitLogger(){var err error logger, err = zap.NewProduction()if err !=nil{panic(err)}}funcLoggerMiddleware() gin.HandlerFunc {returnfunc(c *gin.Context){ start := time.Now() path := c.Request.URL.Path query := c.Request.URL.RawQuery c.Next() latency := time.Since(start) logger.Info("Request", zap.String("method", c.Request.Method), zap.String("path", path), zap.String("query", query), zap.Int("status", c.Writer.Status()), zap.Duration("latency", latency), zap.String("ip", c.ClientIP()),)}}funcmain(){InitLogger()defer logger.Sync() r := gin.New() r.Use(LoggerMiddleware()) r.Use(gin.Recovery()) r.GET("/ping",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"message":"pong"})}) r.Run(":8080")}14. 性能优化
14.1 使用连接池
funcInitDB(){ dsn :="user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" db, err := gorm.Open(mysql.Open(dsn),&gorm.Config{})if err !=nil{ log.Fatal(err)} sqlDB, err := db.DB()if err !=nil{ log.Fatal(err)}// 设置连接池参数 sqlDB.SetMaxIdleConns(10)// 最大空闲连接数 sqlDB.SetMaxOpenConns(100)// 最大打开连接数 sqlDB.SetConnMaxLifetime(time.Hour)// 连接最大生命周期}14.2 启用 Gzip 压缩
go get github.com/gin-contrib/gzip import"github.com/gin-contrib/gzip"funcmain(){ r := gin.Default()// 使用 Gzip 中间件 r.Use(gzip.Gzip(gzip.DefaultCompression)) r.GET("/data",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"message":"Large data response","data": strings.Repeat("x",10000),})}) r.Run(":8080")}14.3 缓存
import("github.com/patrickmn/go-cache""time")var cacheStore *cache.Cache funcInitCache(){// 创建缓存(默认过期时间 5 分钟,清理间隔 10 分钟) cacheStore = cache.New(5*time.Minute,10*time.Minute)}funcCacheMiddleware(duration time.Duration) gin.HandlerFunc {returnfunc(c *gin.Context){// 生成缓存键 key := c.Request.URL.Path +"?"+ c.Request.URL.RawQuery // 尝试从缓存获取if cached, found := cacheStore.Get(key); found { c.JSON(http.StatusOK, cached) c.Abort()return}// 创建响应写入器 writer :=&responseWriter{ ResponseWriter: c.Writer, body:&bytes.Buffer{},} c.Writer = writer c.Next()// 缓存响应if c.Writer.Status()== http.StatusOK { cacheStore.Set(key, writer.body.String(), duration)}}}type responseWriter struct{ gin.ResponseWriter body *bytes.Buffer }func(w *responseWriter)Write(b []byte)(int,error){ w.body.Write(b)return w.ResponseWriter.Write(b)}14.4 优化路由
funcmain(){// 使用 gin.New() 而不是 gin.Default() r := gin.New()// 只添加必要的中间件 r.Use(gin.Recovery())// 使用路由分组 api := r.Group("/api/v1"){ users := api.Group("/users"){ users.GET("", getUsers) users.POST("", createUser) users.GET("/:id", getUser) users.PUT("/:id", updateUser) users.DELETE("/:id", deleteUser)}} r.Run(":8080")}15. 测试
15.1 单元测试
// handlers_test.gopackage main import("net/http""net/http/httptest""testing""github.com/gin-gonic/gin""github.com/stretchr/testify/assert")funcSetupRouter()*gin.Engine { r := gin.Default() r.GET("/ping",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"message":"pong",})})return r }funcTestPingRoute(t *testing.T){ router :=SetupRouter() w := httptest.NewRecorder() req,_:= http.NewRequest("GET","/ping",nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(),"pong")}15.2 API 测试
funcTestCreateUser(t *testing.T){ router :=SetupRouter()// 准备测试数据 jsonData :=`{"username":"testuser","email":"[email protected]"}` w := httptest.NewRecorder() req,_:= http.NewRequest("POST","/users", strings.NewReader(jsonData)) req.Header.Set("Content-Type","application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code)var response map[string]interface{} json.Unmarshal(w.Body.Bytes(),&response) assert.Equal(t,"testuser", response["username"])}15.3 集成测试
funcTestUserFlow(t *testing.T){ router :=SetupRouter()// 1. 创建用户 createData :=`{"username":"testuser","password":"password123","email":"[email protected]"}` w := httptest.NewRecorder() req,_:= http.NewRequest("POST","/users", strings.NewReader(createData)) req.Header.Set("Content-Type","application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code)// 2. 登录 loginData :=`{"username":"testuser","password":"password123"}` w = httptest.NewRecorder() req,_= http.NewRequest("POST","/login", strings.NewReader(loginData)) req.Header.Set("Content-Type","application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code)var loginResponse map[string]string json.Unmarshal(w.Body.Bytes(),&loginResponse) token := loginResponse["token"]// 3. 访问受保护的资源 w = httptest.NewRecorder() req,_= http.NewRequest("GET","/api/profile",nil) req.Header.Set("Authorization","Bearer "+token) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code)}16. 部署
16.1 编译应用
# 编译当前平台 go build -o app main.go # 交叉编译 LinuxGOOS=linux GOARCH=amd64 go build -o app-linux main.go # 交叉编译 WindowsGOOS=windows GOARCH=amd64 go build -o app.exe main.go # 交叉编译 MacGOOS=darwin GOARCH=amd64 go build -o app-mac main.go # 优化编译(减小体积) go build -ldflags="-s -w" -o app main.go 16.2 使用 Systemd 部署
创建服务文件 /etc/systemd/system/gin-app.service:
[Unit] Description=Gin Application After=network.target [Service] Type=simple User=www-data WorkingDirectory=/opt/gin-app ExecStart=/opt/gin-app/app Restart=on-failure RestartSec=5s Environment="GIN_MODE=release" Environment="PORT=8080" [Install] WantedBy=multi-user.target 启动服务:
sudo systemctl daemon-reload sudo systemctl enable gin-app sudo systemctl start gin-app sudo systemctl status gin-app 16.3 使用 Docker 部署
创建 Dockerfile:
# 构建阶段 FROM golang:1.21-alpine AS builder WORKDIR /app # 复制依赖文件 COPY go.mod go.sum ./ RUN go mod download # 复制源代码 COPY . . # 编译应用 RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . # 运行阶段 FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ # 从构建阶段复制二进制文件 COPY --from=builder /app/main . COPY --from=builder /app/templates ./templates COPY --from=builder /app/static ./static EXPOSE 8080 CMD ["./main"] 创建 docker-compose.yml:
version:'3.8'services:app:build: . ports:-"8080:8080"environment:- GIN_MODE=release - DB_HOST=db - DB_PORT=3306 - DB_USER=root - DB_PASSWORD=password - DB_NAME=myapp depends_on:- db restart: unless-stopped db:image: mysql:8.0environment:- MYSQL_ROOT_PASSWORD=password - MYSQL_DATABASE=myapp volumes:- db_data:/var/lib/mysql restart: unless-stopped nginx:image: nginx:alpine ports:-"80:80"-"443:443"volumes:- ./nginx.conf:/etc/nginx/nginx.conf - ./ssl:/etc/nginx/ssl depends_on:- app restart: unless-stopped volumes:db_data:16.4 Nginx 反向代理配置
创建 nginx.conf:
events { worker_connections 1024; } http { upstream gin_app { server app:8080; } server { listen 80; server_name example.com; # 重定向到 HTTPS return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/ssl/cert.pem; ssl_certificate_key /etc/nginx/ssl/key.pem; # SSL 配置 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; # 日志 access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; # Gzip 压缩 gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; location / { proxy_pass http://gin_app; 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"; } # 静态文件 location /static/ { alias /var/www/static/; expires 30d; add_header Cache-Control "public, immutable"; } } } 16.5 使用 Kubernetes 部署
创建 deployment.yaml:
apiVersion: apps/v1 kind: Deployment metadata:name: gin-app spec:replicas:3selector:matchLabels:app: gin-app template:metadata:labels:app: gin-app spec:containers:-name: gin-app image: your-registry/gin-app:latest ports:-containerPort:8080env:-name: GIN_MODE value:"release"-name: DB_HOST valueFrom:configMapKeyRef:name: app-config key: db_host resources:requests:memory:"128Mi"cpu:"100m"limits:memory:"256Mi"cpu:"200m"livenessProbe:httpGet:path: /health port:8080initialDelaySeconds:30periodSeconds:10readinessProbe:httpGet:path: /ready port:8080initialDelaySeconds:5periodSeconds:5---apiVersion: v1 kind: Service metadata:name: gin-app-service spec:selector:app: gin-app ports:-protocol: TCP port:80targetPort:8080type: LoadBalancer 17. 实战项目:博客 API
17.1 项目结构
blog-api/ ├── main.go ├── config/ │ └── config.go ├── models/ │ ├── user.go │ ├── post.go │ └── comment.go ├── controllers/ │ ├── auth.go │ ├── user.go │ ├── post.go │ └── comment.go ├── middleware/ │ ├── auth.go │ ├── cors.go │ └── logger.go ├── routes/ │ └── routes.go ├── database/ │ └── database.go ├── utils/ │ ├── jwt.go │ ├── password.go │ └── response.go └── go.mod 17.2 配置管理
// config/config.gopackage config import("os""github.com/joho/godotenv")type Config struct{ DBHost string DBPort string DBUser string DBPassword string DBName string JWTSecret string Port string}funcLoadConfig()*Config { godotenv.Load()return&Config{ DBHost:getEnv("DB_HOST","localhost"), DBPort:getEnv("DB_PORT","3306"), DBUser:getEnv("DB_USER","root"), DBPassword:getEnv("DB_PASSWORD",""), DBName:getEnv("DB_NAME","blog"), JWTSecret:getEnv("JWT_SECRET","secret"), Port:getEnv("PORT","8080"),}}funcgetEnv(key, defaultValue string)string{if value := os.Getenv(key); value !=""{return value }return defaultValue }17.3 数据模型
// models/user.gopackage models import("gorm.io/gorm""time")type User struct{ ID uint`gorm:"primaryKey" json:"id"` Username string`gorm:"unique;not null" json:"username"` Email string`gorm:"unique;not null" json:"email"` Password string`gorm:"not null" json:"-"` Avatar string`json:"avatar"` Bio string`json:"bio"` Posts []Post `gorm:"foreignKey:AuthorID" json:"posts,omitempty"` Comments []Comment `gorm:"foreignKey:UserID" json:"comments,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`}// models/post.gopackage models import("gorm.io/gorm""time")type Post struct{ ID uint`gorm:"primaryKey" json:"id"` Title string`gorm:"not null" json:"title"` Content string`gorm:"type:text;not null" json:"content"` Excerpt string`json:"excerpt"` Slug string`gorm:"unique;not null" json:"slug"` AuthorID uint`gorm:"not null" json:"author_id"` Author User `gorm:"foreignKey:AuthorID" json:"author"` Comments []Comment `gorm:"foreignKey:PostID" json:"comments,omitempty"` Tags []Tag `gorm:"many2many:post_tags;" json:"tags,omitempty"` Published bool`gorm:"default:false" json:"published"` ViewCount int`gorm:"default:0" json:"view_count"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`}type Tag struct{ ID uint`gorm:"primaryKey" json:"id"` Name string`gorm:"unique;not null" json:"name"` Posts []Post `gorm:"many2many:post_tags;" json:"posts,omitempty"`}// models/comment.gopackage models import("gorm.io/gorm""time")type Comment struct{ ID uint`gorm:"primaryKey" json:"id"` Content string`gorm:"type:text;not null" json:"content"` PostID uint`gorm:"not null" json:"post_id"` Post Post `gorm:"foreignKey:PostID" json:"post,omitempty"` UserID uint`gorm:"not null" json:"user_id"` User User `gorm:"foreignKey:UserID" json:"user"` ParentID *uint`json:"parent_id"` Parent *Comment `gorm:"foreignKey:ParentID" json:"parent,omitempty"` Replies []Comment `gorm:"foreignKey:ParentID" json:"replies,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`}17.4 控制器
// controllers/post.gopackage controllers import("blog-api/database""blog-api/models""blog-api/utils""github.com/gin-gonic/gin""net/http""strconv")type PostController struct{}func(pc *PostController)GetPosts(c *gin.Context){var posts []models.Post page,_:= strconv.Atoi(c.DefaultQuery("page","1")) pageSize,_:= strconv.Atoi(c.DefaultQuery("page_size","10")) offset :=(page -1)* pageSize var total int64 database.DB.Model(&models.Post{}).Where("published = ?",true).Count(&total) database.DB.Where("published = ?",true).Preload("Author").Preload("Tags").Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&posts) utils.SuccessResponse(c, gin.H{"posts": posts,"pagination": gin.H{"page": page,"page_size": pageSize,"total": total,"total_page":(total +int64(pageSize)-1)/int64(pageSize),},})}func(pc *PostController)GetPost(c *gin.Context){ id := c.Param("id")var post models.Post if err := database.DB.Preload("Author").Preload("Tags").Preload("Comments.User").First(&post, id).Error; err !=nil{ utils.ErrorResponse(c, http.StatusNotFound,"Post not found")return}// 增加浏览次数 database.DB.Model(&post).Update("view_count", post.ViewCount+1) utils.SuccessResponse(c, post)}func(pc *PostController)CreatePost(c *gin.Context){var input struct{ Title string`json:"title" binding:"required"` Content string`json:"content" binding:"required"` Excerpt string`json:"excerpt"` Tags []string`json:"tags"`}if err := c.ShouldBindJSON(&input); err !=nil{ utils.ErrorResponse(c, http.StatusBadRequest, err.Error())return} userID := c.GetUint("user_id") slug := utils.GenerateSlug(input.Title) post := models.Post{ Title: input.Title, Content: input.Content, Excerpt: input.Excerpt, Slug: slug, AuthorID: userID,}// 处理标签var tags []models.Tag for_, tagName :=range input.Tags {var tag models.Tag database.DB.FirstOrCreate(&tag, models.Tag{Name: tagName}) tags =append(tags, tag)} post.Tags = tags if err := database.DB.Create(&post).Error; err !=nil{ utils.ErrorResponse(c, http.StatusInternalServerError,"Failed to create post")return} utils.SuccessResponse(c, post)}func(pc *PostController)UpdatePost(c *gin.Context){ id := c.Param("id")var post models.Post if err := database.DB.First(&post, id).Error; err !=nil{ utils.ErrorResponse(c, http.StatusNotFound,"Post not found")return}// 检查权限 userID := c.GetUint("user_id")if post.AuthorID != userID { utils.ErrorResponse(c, http.StatusForbidden,"Permission denied")return}var input struct{ Title string`json:"title"` Content string`json:"content"` Excerpt string`json:"excerpt"` Published bool`json:"published"` Tags []string`json:"tags"`}if err := c.ShouldBindJSON(&input); err !=nil{ utils.ErrorResponse(c, http.StatusBadRequest, err.Error())return} updates :=map[string]interface{}{"title": input.Title,"content": input.Content,"excerpt": input.Excerpt,"published": input.Published,} database.DB.Model(&post).Updates(updates)// 更新标签iflen(input.Tags)>0{var tags []models.Tag for_, tagName :=range input.Tags {var tag models.Tag database.DB.FirstOrCreate(&tag, models.Tag{Name: tagName}) tags =append(tags, tag)} database.DB.Model(&post).Association("Tags").Replace(tags)} utils.SuccessResponse(c, post)}func(pc *PostController)DeletePost(c *gin.Context){ id := c.Param("id")var post models.Post if err := database.DB.First(&post, id).Error; err !=nil{ utils.ErrorResponse(c, http.StatusNotFound,"Post not found")return}// 检查权限 userID := c.GetUint("user_id")if post.AuthorID != userID { utils.ErrorResponse(c, http.StatusForbidden,"Permission denied")return} database.DB.Delete(&post) utils.SuccessResponse(c, gin.H{"message":"Post deleted successfully"})}17.5 路由设置
// routes/routes.gopackage routes import("blog-api/controllers""blog-api/middleware""github.com/gin-gonic/gin")funcSetupRoutes(r *gin.Engine){// 中间件 r.Use(middleware.CORSMiddleware()) r.Use(middleware.LoggerMiddleware())// 公开路由 public := r.Group("/api"){// 认证 auth :=&controllers.AuthController{} public.POST("/register", auth.Register) public.POST("/login", auth.Login)// 文章(公开) post :=&controllers.PostController{} public.GET("/posts", post.GetPosts) public.GET("/posts/:id", post.GetPost)}// 需要认证的路由 protected := r.Group("/api") protected.Use(middleware.AuthMiddleware()){// 用户 user :=&controllers.UserController{} protected.GET("/profile", user.GetProfile) protected.PUT("/profile", user.UpdateProfile)// 文章管理 post :=&controllers.PostController{} protected.POST("/posts", post.CreatePost) protected.PUT("/posts/:id", post.UpdatePost) protected.DELETE("/posts/:id", post.DeletePost)// 评论 comment :=&controllers.CommentController{} protected.POST("/posts/:id/comments", comment.CreateComment) protected.DELETE("/comments/:id", comment.DeleteComment)}}17.6 主程序
// main.gopackage main import("blog-api/config""blog-api/database""blog-api/routes""github.com/gin-gonic/gin""log")funcmain(){// 加载配置 cfg := config.LoadConfig()// 初始化数据库 database.InitDB(cfg)// 设置 Gin 模式 gin.SetMode(gin.ReleaseMode)// 创建路由 r := gin.Default()// 设置路由 routes.SetupRoutes(r)// 启动服务器 log.Printf("Server starting on port %s", cfg.Port)if err := r.Run(":"+ cfg.Port); err !=nil{ log.Fatal("Failed to start server:", err)}}18. 最佳实践
18.1 项目结构最佳实践
project/ ├── cmd/ # 应用程序入口 │ └── api/ │ └── main.go ├── internal/ # 私有代码 │ ├── config/ # 配置 │ ├── models/ # 数据模型 │ ├── repository/ # 数据访问层 │ ├── service/ # 业务逻辑层 │ ├── handler/ # HTTP 处理器 │ └── middleware/ # 中间件 ├── pkg/ # 公共库 │ ├── logger/ │ ├── validator/ │ └── utils/ ├── api/ # API 定义 │ └── openapi.yaml ├── migrations/ # 数据库迁移 ├── scripts/ # 脚本 ├── docs/ # 文档 ├── .env.example ├── Dockerfile ├── docker-compose.yml ├── Makefile └── go.mod 18.2 代码规范
// 1. 使用有意义的变量名// 不好funcGetU(id int)(*User,error){var u User // ...}// 好funcGetUserByID(userID int)(*User,error){var user User // ...}// 2. 错误处理// 不好 user,_:=GetUser(id)// 好 user, err :=GetUser(id)if err !=nil{returnnil, fmt.Errorf("failed to get user: %w", err)}// 3. 使用常量// 不好if user.Role =="admin"{// ...}// 好const( RoleAdmin ="admin" RoleUser ="user")if user.Role == RoleAdmin {// ...}// 4. 接口设计type UserRepository interface{Create(user *User)errorGetByID(id uint)(*User,error)Update(user *User)errorDelete(id uint)errorList(page, pageSize int)([]User,error)}18.3 安全最佳实践
// 1. 密码加密import"golang.org/x/crypto/bcrypt"funcHashPassword(password string)(string,error){ bytes, err := bcrypt.GenerateFromPassword([]byte(password),14)returnstring(bytes), err }funcCheckPassword(password, hash string)bool{ err := bcrypt.CompareHashAndPassword([]byte(hash),[]byte(password))return err ==nil}// 2. SQL 注入防护(使用参数化查询)// 不好 db.Raw("SELECT * FROM users WHERE+ username +"'")// 好 db.Where("username = ?", username).Find(&users)// 3. XSS 防护import"html"funcSanitizeInput(input string)string{return html.EscapeString(input)}// 4. CSRF 防护import"github.com/gin-contrib/csrf"funcmain(){ r := gin.Default() r.Use(csrf.Middleware(csrf.Options{ Secret:"secret-key", ErrorFunc:func(c *gin.Context){ c.JSON(http.StatusForbidden, gin.H{"error":"CSRF token invalid"}) c.Abort()},}))}// 5. 限制请求大小 r.MaxMultipartMemory =8<<20// 8 MB18.4 性能最佳实践
// 1. 使用连接池 sqlDB,_:= db.DB() sqlDB.SetMaxIdleConns(10) sqlDB.SetMaxOpenConns(100) sqlDB.SetConnMaxLifetime(time.Hour)// 2. 使用索引type User struct{ Email string`gorm:"index"` Phone string`gorm:"uniqueIndex"`}// 3. 批量操作 users :=[]User{{Name:"user1"},{Name:"user2"}} db.CreateInBatches(users,100)// 4. 选择性加载字段 db.Select("id","name","email").Find(&users)// 5. 使用缓存var cachedData interface{}if cached, found := cache.Get("key"); found { cachedData = cached }else{// 从数据库获取 cachedData =fetchFromDB() cache.Set("key", cachedData,5*time.Minute)}18.5 监控和日志
// 1. 结构化日志 logger.Info("User logged in", zap.String("user_id", userID), zap.String("ip", clientIP), zap.Time("timestamp", time.Now()),)// 2. 性能监控funcPerformanceMiddleware() gin.HandlerFunc {returnfunc(c *gin.Context){ start := time.Now() c.Next() duration := time.Since(start)if duration >1*time.Second { logger.Warn("Slow request", zap.String("path", c.Request.URL.Path), zap.Duration("duration", duration),)}}}// 3. 健康检查 r.GET("/health",func(c *gin.Context){ c.JSON(http.StatusOK, gin.H{"status":"healthy","timestamp": time.Now(),})}) r.GET("/ready",func(c *gin.Context){// 检查数据库连接if err := db.DB().Ping(); err !=nil{ c.JSON(http.StatusServiceUnavailable, gin.H{"status":"not ready","error":"database connection failed",})return} c.JSON(http.StatusOK, gin.H{"status":"ready",})})总结
本教程涵盖了 Gin 框架从入门到精通的全部内容:
- 基础知识:路由、请求处理、响应处理
- 进阶功能:中间件、数据验证、数据库集成
- 高级特性:文件操作、会话管理、JWT 认证
- 工程实践:错误处理、日志系统、性能优化
- 部署运维:Docker、Kubernetes、Nginx
- 实战项目:完整的博客 API 示例
- 最佳实践:代码规范、安全、性能、监控
学习建议
- 循序渐进:从简单的 Hello World 开始,逐步深入
- 动手实践:每个示例都要亲自编写和运行
- 阅读源码:深入理解 Gin 的实现原理
- 参与社区:关注 GitHub issues 和讨论
- 持续学习:关注 Go 和 Gin 的最新发展
参考资源
祝你学习愉快,成为 Gin 框架专家!🚀