Gin 框架从入门到精通完整教程
目录
- [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 语言
bash
# 下载 Go(访问 https://golang.org/dl/)
# Linux/Mac 安装
wget https://golang.org/dl/go1.21.0.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz
# 配置环境变量
export PATH=$PATH:/usr/local/go/bin
export GOPATH=$HOME/go
export GO111MODULE=on
export GOPROXY=https://goproxy.cn,direct
# 验证安装
go version
2.2 安装 Gin 框架
bash
# 创建项目目录
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 应用
go
// main.go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
// 创建默认的 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")
}
运行应用:
bash
go run main.go
访问 http://localhost:8080/,你将看到 JSON 响应。
3.2 不使用默认中间件
go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
// 创建不带中间件的引擎
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 自定义端口和地址
go
// 方式 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 基本路由
go
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
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 路由参数
go
func main() {
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 查询参数
go
func main() {
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 路由分组
go
func main() {
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")
}
func getUsers(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"version": "v1", "users": []string{}})
}
func getUser(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"id": id})
}
func createUser(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{"message": "User created"})
}
func updateUser(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"message": "User updated", "id": id})
}
func deleteUser(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"message": "User deleted", "id": id})
}
func getUsersV2(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"version": "v2", "users": []string{}})
}
func createUserV2(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{"message": "User created (v2)"})
}
func getDashboard(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"dashboard": "data"})
}
func getAdminUsers(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"admin_users": []string{}})
}
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 认证逻辑
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
c.Next()
}
}
4.5 路由注册的其他方式
go
func main() {
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 获取表单数据
go
func main() {
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 数据
go
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"`
}
func main() {
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 数据
go
type Article struct {
XMLName xml.Name `xml:"article"`
Title string `xml:"title" binding:"required"`
Content string `xml:"content" binding:"required"`
Author string `xml:"author"`
}
func main() {
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 绑定查询参数和表单
go
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"`
}
func main() {
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 参数
go
type UserID struct {
ID int `uri:"id" binding:"required,gte=1"`
}
func main() {
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 自定义验证器
go
import (
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
// 自定义验证函数
func customValidator(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"`
}
func main() {
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 响应
go
func main() {
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 响应
go
func main() {
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 响应
go
func main() {
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:
html
<!DOCTYPE html>
<html>
<head>
<title>{{ .title }}</title>
</head>
<body>
<h1>Hello, {{ .name }}!</h1>
</body>
</html>
6.4 文件响应
go
func main() {
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 重定向
go
func main() {
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 流式响应
go
func main() {
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)
}
return false
})
})
r.Run(":8080")
}
7. 中间件
7.1 全局中间件
go
func Logger() gin.HandlerFunc {
return func(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)
}
}
func main() {
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 路由级中间件
go
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Authorization required",
})
c.Abort()
return
}
// 验证 token
if token != "valid-token" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Invalid token",
})
c.Abort()
return
}
c.Next()
}
}
func main() {
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 分组中间件
go
func main() {
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")
}
func getInfo(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"info": "public"})
}
func getProfile(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"profile": "user profile"})
}
func updateProfile(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Profile updated"})
}
func getAllUsers(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"users": []string{}})
}
func AdminRequired() gin.HandlerFunc {
return func(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 中间件
go
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, 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()
}
}
func main() {
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 限流中间件
go
import (
"golang.org/x/time/rate"
"sync"
)
func RateLimitMiddleware(r rate.Limit, b int) gin.HandlerFunc {
limiters := &sync.Map{}
return func(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()
}
}
func main() {
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 基本验证标签
go
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 常用验证标签
go
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 自定义错误消息
go
type LoginForm struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required,min=6"`
}
func main() {
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] = "密码不能为空"
} else if 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:
bash
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 连接数据库
go
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"`
}
func InitDB() {
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{})
}
func main() {
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")
}
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
}
if err := DB.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, user)
}
func getUsers(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,
})
}
func getUser(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)
}
func updateUser(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)
}
func deleteUser(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"})
}
func atoi(s string) int {
i, _ := strconv.Atoi(s)
return i
}
9.3 关联查询
go
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
}
func getUserWithPosts(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 单文件上传
go
func main() {
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")
}
func isAllowedFileType(filename string) bool {
allowedTypes := []string{".jpg", ".jpeg", ".png", ".gif", ".pdf"}
ext := strings.ToLower(filepath.Ext(filename))
for _, allowed := range allowedTypes {
if ext == allowed {
return true
}
}
return false
}
func generateUniqueFilename(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 多文件上传
go
func main() {
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 []string
for _, 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 文件下载
go
func main() {
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
go
func main() {
r := gin.Default()
r.GET("/cookie/set", func(c *gin.Context) {
c.SetCookie(
"session_id", // name
"abc123", // value
3600, // maxAge (秒)
"/", // path
"localhost", // domain
false, // secure
true, // 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)
bash
go get github.com/gin-contrib/sessions
go get github.com/gin-contrib/sessions/cookie
go
import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
)
func main() {
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 认证
bash
go get github.com/golang-jwt/jwt/v5
go
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
}
func GenerateToken(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)
}
func ParseToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, jwt.ErrSignatureInvalid
}
func JWTAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
c.Abort()
return
}
// 移除 "Bearer " 前缀
if len(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()
}
}
func main() {
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 统一错误处理
go
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
// 检查是否有错误
if len(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",
})
}
}
}
}
func main() {
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 自定义错误类型
go
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
}
func NewAppError(code int, message string, err error) *AppError {
return &AppError{
Code: code,
Message: message,
Err: err,
}
}
func HandleError(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 自定义日志格式
go
func main() {
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 日志写入文件
go
func main() {
// 创建日志文件
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 日志库
bash
go get -u go.uber.org/zap
go
import (
"go.uber.org/zap"
)
var logger *zap.Logger
func InitLogger() {
var err error
logger, err = zap.NewProduction()
if err != nil {
panic(err)
}
}
func LoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
latency := time.Since(start)
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()),
)
}
}
func main() {
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 使用连接池
go
func InitDB() {
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 压缩
bash
go get github.com/gin-contrib/gzip
go
import "github.com/gin-contrib/gzip"
func main() {
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 缓存
go
import (
"github.com/patrickmn/go-cache"
"time"
)
var cacheStore *cache.Cache
func InitCache() {
// 创建缓存(默认过期时间 5 分钟,清理间隔 10 分钟)
cacheStore = cache.New(5*time.Minute, 10*time.Minute)
}
func CacheMiddleware(duration time.Duration) gin.HandlerFunc {
return func(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 优化路由
go
func main() {
// 使用 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 单元测试
go
// handlers_test.go
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func SetupRouter() *gin.Engine {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
return r
}
func TestPingRoute(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 测试
go
func TestCreateUser(t *testing.T) {
router := SetupRouter()
// 准备测试数据
jsonData := `{"username":"testuser","email":"test@example.com"}`
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 集成测试
go
func TestUserFlow(t *testing.T) {
router := SetupRouter()
// 1. 创建用户
createData := `{"username":"testuser","password":"password123","email":"test@example.com"}`
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 编译应用
bash
# 编译当前平台
go build -o app main.go
# 交叉编译 Linux
GOOS=linux GOARCH=amd64 go build -o app-linux main.go
# 交叉编译 Windows
GOOS=windows GOARCH=amd64 go build -o app.exe main.go
# 交叉编译 Mac
GOOS=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:
ini
[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
启动服务:
bash
sudo systemctl daemon-reload
sudo systemctl enable gin-app
sudo systemctl start gin-app
sudo systemctl status gin-app
16.3 使用 Docker 部署
创建 Dockerfile:
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:
yaml
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.0
environment:
- 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:
nginx
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:
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: gin-app
spec:
replicas: 3
selector:
matchLabels:
app: gin-app
template:
metadata:
labels:
app: gin-app
spec:
containers:
- name: gin-app
image: your-registry/gin-app:latest
ports:
- containerPort: 8080
env:
- 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: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: gin-app-service
spec:
selector:
app: gin-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: 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 配置管理
go
// config/config.go
package config
import (
"os"
"github.com/joho/godotenv"
)
type Config struct {
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
JWTSecret string
Port string
}
func LoadConfig() *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"),
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
17.3 数据模型
go
// models/user.go
package 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.go
package 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.go
package 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 控制器
go
// controllers/post.go
package 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)
// 更新标签
if len(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 路由设置
go
// routes/routes.go
package routes
import (
"blog-api/controllers"
"blog-api/middleware"
"github.com/gin-gonic/gin"
)
func SetupRoutes(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 主程序
go
// main.go
package main
import (
"blog-api/config"
"blog-api/database"
"blog-api/routes"
"github.com/gin-gonic/gin"
"log"
)
func main() {
// 加载配置
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 代码规范
go
// 1. 使用有意义的变量名
// 不好
func GetU(id int) (*User, error) {
var u User
// ...
}
// 好
func GetUserByID(userID int) (*User, error) {
var user User
// ...
}
// 2. 错误处理
// 不好
user, _ := GetUser(id)
// 好
user, err := GetUser(id)
if err != nil {
return nil, 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) error
GetByID(id uint) (*User, error)
Update(user *User) error
Delete(id uint) error
List(page, pageSize int) ([]User, error)
}
18.3 安全最佳实践
go
// 1. 密码加密
import "golang.org/x/crypto/bcrypt"
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
func CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// 2. SQL 注入防护(使用参数化查询)
// 不好
db.Raw("SELECT * FROM users WHERE username = '" + username + "'")
// 好
db.Where("username = ?", username).Find(&users)
// 3. XSS 防护
import "html"
func SanitizeInput(input string) string {
return html.EscapeString(input)
}
// 4. CSRF 防护
import "github.com/gin-contrib/csrf"
func main() {
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 MB
18.4 性能最佳实践
go
// 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 监控和日志
go
// 1. 结构化日志
logger.Info("User logged in",
zap.String("user_id", userID),
zap.String("ip", clientIP),
zap.Time("timestamp", time.Now()),
)
// 2. 性能监控
func PerformanceMiddleware() gin.HandlerFunc {
return func(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 框架专家!🚀