辰風依恛
文章35
标签0
分类11
Gin快速上手

Gin快速上手

Gin快速上手

从Express/Koa/NestJS到Go语言Web开发

前言

如果你已经熟悉Express、Koa或NestJS等Node.js框架,学习Gin框架将会非常顺畅。Gin是Go语言中最流行的Web框架之一,以其高性能、简洁的API和丰富的中间件生态著称。

环境搭建

1. Go语言环境安装

1
2
3
4
5
6
7
8
# 下载并安装Go (https://golang.org/dl/)
# 验证安装
$ go version
go version go1.22.0 darwin/amd64

# 设置GOPATH和GOROOT(现代Go版本通常不需要手动设置)
$ go env GOPATH
/Users/username/go

2. 创建项目

1
2
3
4
5
6
7
8
# 创建项目目录
$ mkdir gin-demo && cd gin-demo

# 初始化Go模块
$ go mod init gin-demo

# 安装Gin框架(推荐使用Go Modules)
$ go get github.com/gin-gonic/gin@latest

3. 开发工具推荐

  • VS Code + Go插件
  • GoLand (JetBrains IDE)
  • Goland (社区版)

Go语言基础(对比JavaScript/TypeScript)

变量声明差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Go语言
package main

import "fmt"

func main() {
// 变量声明(类型在后)
var name string = "Gin"
age := 25 // 类型推断

// 常量
const version = "1.0.0"

fmt.Println(name, age, version)
}
1
2
3
4
5
6
// JavaScript对比
const name = "Gin";
let age = 25;
const version = "1.0.0";

console.log(name, age, version);

函数定义差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Go语言函数
func add(a int, b int) int {
return a + b
}

// 多返回值
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("除数不能为零")
}
return a / b, nil
}

// 匿名函数
func main() {
square := func(x int) int {
return x * x
}
fmt.Println(square(5))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// JavaScript函数对比
function add(a, b) {
return a + b;
}

// 现代JS(箭头函数)
const divide = (a, b) => {
if (b === 0) {
throw new Error("除数不能为零");
}
return a / b;
};

// 匿名函数
const square = (x) => x * x;
console.log(square(5));

错误处理差异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Go语言错误处理
func readFile(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("读取文件失败: %v", err)
}

// 处理数据
fmt.Println(string(data))
return nil
}

// 使用错误
func main() {
if err := readFile("config.yaml"); err != nil {
log.Fatal(err)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// JavaScript错误处理(async/await)
async function readFile(filename) {
try {
const data = await fs.promises.readFile(filename, 'utf8');
console.log(data);
} catch (error) {
throw new Error(`读取文件失败: ${error.message}`);
}
}

// 使用
readFile('config.yaml').catch(error => {
console.error(error);
});

Gin框架基础

1. 第一个Gin应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// main.go
package main

import "github.com/gin-gonic/gin"

func main() {
// 创建Gin实例(类似Express的app = express())
r := gin.Default()

// 定义路由(对比Express的app.get())
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello Gin!",
})
})

// 启动服务器(默认端口8080)
r.Run() // 相当于 app.listen(8080)
}

2. 路由定义对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Gin路由定义
r.GET("/users", getUsers)
r.POST("/users", createUser)
r.PUT("/users/:id", updateUser)
r.DELETE("/users/:id", deleteUser)

// 路由组(类似Express的Router)
api := r.Group("/api")
{
api.GET("/users", getUsers)
api.POST("/users", createUser)
}

// 路径参数
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id") // 类似Express的req.params.id
c.JSON(200, gin.H{"id": id})
})

// 查询参数
r.GET("/search", func(c *gin.Context) {
query := c.Query("q") // 类似Express的req.query.q
c.JSON(200, gin.H{"query": query})
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Express对比
app.get('/users', getUsers);
app.post('/users', createUser);
app.put('/users/:id', updateUser);
app.delete('/users/:id', deleteUser);

// 路由组
const apiRouter = express.Router();
apiRouter.get('/users', getUsers);
apiRouter.post('/users', createUser);
app.use('/api', apiRouter);

// 路径参数
app.get('/users/:id', (req, res) => {
const id = req.params.id;
res.json({ id });
});

// 查询参数
app.get('/search', (req, res) => {
const query = req.query.q;
res.json({ query });
});

3. 请求和响应处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 请求体解析(JSON)
type User struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}

r.POST("/users", func(c *gin.Context) {
var user User

// 绑定JSON(类似Express的body-parser)
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}

c.JSON(201, gin.H{"user": user})
})

// 文件上传
r.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": "文件上传失败"})
return
}

// 保存文件
c.SaveUploadedFile(file, "./uploads/"+file.Filename)
c.JSON(200, gin.H{"message": "文件上传成功"})
})

// 响应类型
r.GET("/response", func(c *gin.Context) {
// JSON响应
c.JSON(200, gin.H{"type": "json"})

// 字符串响应
c.String(200, "Hello World")

// HTML响应
c.HTML(200, "index.html", gin.H{"title": "Gin"})

// 文件下载
c.File("./downloads/file.zip")
})

中间件系统

1. 内置中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func main() {
r := gin.New()

// 使用默认中间件(日志和恢复)
r.Use(gin.Logger(), gin.Recovery())

// 静态文件服务(类似Express的express.static())
r.Static("/static", "./public")

// 单个路由中间件
r.GET("/admin", authMiddleware(), adminHandler)

r.Run()
}

// 自定义中间件(类似Express的(req, res, next))
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")

if token == "" {
c.JSON(401, gin.H{"error": "未授权"})
c.Abort() // 类似Express的next()但用于终止
return
}

// 验证token逻辑
c.Set("user", "authenticated-user") // 类似Express的req.user
c.Next() // 继续执行下一个中间件/处理器
}
}

2. 中间件执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
r := gin.New()

// 全局中间件(按添加顺序执行)
r.Use(loggerMiddleware)
r.Use(authMiddleware)

// 路由组中间件
admin := r.Group("/admin")
admin.Use(adminAuthMiddleware)
{
admin.GET("/dashboard", dashboardHandler)
}

r.Run()
}

func loggerMiddleware(c *gin.Context) {
start := time.Now()
c.Next()
latency := time.Since(start)
log.Printf("请求 %s 耗时 %v", c.Request.URL.Path, latency)
}

数据库操作

1. 多数据库连接配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// config/database.go
package config

import (
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)

type DatabaseConfig struct {
Driver string `yaml:"driver"`
Host string `yaml:"host"`
Port string `yaml:"port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Database string `yaml:"database"`
}

func ConnectDB(config DatabaseConfig) (*gorm.DB, error) {
var dialector gorm.Dialector

switch config.Driver {
case "mysql":
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
config.Username, config.Password, config.Host, config.Port, config.Database)
dialector = mysql.Open(dsn)

case "postgres":
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
config.Host, config.Username, config.Password, config.Database, config.Port)
dialector = postgres.Open(dsn)

case "sqlite":
dialector = sqlite.Open(config.Database)

default:
return nil, fmt.Errorf("不支持的数据库驱动: %s", config.Driver)
}

return gorm.Open(dialector, &gorm.Config{})
}

// 多数据库连接
var (
PrimaryDB *gorm.DB
ReadOnlyDB *gorm.DB
AnalyticsDB *gorm.DB
)

func InitDatabases() error {
var err error

// 主数据库(读写)
PrimaryDB, err = ConnectDB(DatabaseConfig{
Driver: "mysql",
Host: "localhost",
Port: "3306",
Username: "root",
Password: "password",
Database: "primary_db",
})

// 只读数据库副本
ReadOnlyDB, err = ConnectDB(DatabaseConfig{
Driver: "mysql",
Host: "readonly-host",
Port: "3306",
Username: "readonly_user",
Password: "password",
Database: "primary_db",
})

// 分析数据库
AnalyticsDB, err = ConnectDB(DatabaseConfig{
Driver: "postgres",
Host: "analytics-host",
Port: "5432",
Username: "analytics_user",
Password: "password",
Database: "analytics_db",
})

return err
}

2. 模型定义和CRUD操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// models/user.go
package models

import (
"gorm.io/gorm"
"time"
)

type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:100;not null" json:"name"`
Email string `gorm:"size:255;uniqueIndex;not null" json:"email"`
Password string `gorm:"size:255;not null" json:"-"` // 不序列化到JSON
Age int `gorm:"default:0" json:"age"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}

// 用户表名
func (User) TableName() string {
return "users"
}

// 业务逻辑方法
func (u *User) BeforeCreate(tx *gorm.DB) error {
u.CreatedAt = time.Now()
u.UpdatedAt = time.Now()
return nil
}

func (u *User) BeforeUpdate(tx *gorm.DB) error {
u.UpdatedAt = time.Now()
return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// handlers/user.go
package handlers

import (
"gin-demo/models"
"net/http"
"strconv"

"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

type UserHandler struct {
DB *gorm.DB
}

func NewUserHandler(db *gorm.DB) *UserHandler {
return &UserHandler{DB: db}
}

// 获取用户列表(GET /users)
func (h *UserHandler) GetUsers(c *gin.Context) {
var users []models.User

// 分页参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
offset := (page - 1) * limit

// 查询
result := h.DB.Offset(offset).Limit(limit).Find(&users)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"})
return
}

// 获取总数
var total int64
h.DB.Model(&models.User{}).Count(&total)

c.JSON(http.StatusOK, gin.H{
"users": users,
"pagination": gin.H{
"page": page,
"limit": limit,
"total": total,
},
})
}

// 获取单个用户(GET /users/:id)
func (h *UserHandler) GetUser(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的用户ID"})
return
}

var user models.User
result := h.DB.First(&user, id)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"})
}
return
}

c.JSON(http.StatusOK, gin.H{"user": user})
}

// 创建用户(POST /users)
func (h *UserHandler) CreateUser(c *gin.Context) {
var user models.User

if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// 密码加密(在实际项目中应该使用bcrypt)
// user.Password = hashPassword(user.Password)

result := h.DB.Create(&user)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建用户失败"})
return
}

c.JSON(http.StatusCreated, gin.H{"user": user})
}

// 更新用户(PUT /users/:id)
func (h *UserHandler) UpdateUser(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的用户ID"})
return
}

var user models.User
result := h.DB.First(&user, id)
if result.Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}

var updateData map[string]interface{}
if err := c.ShouldBindJSON(&updateData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// 更新用户
h.DB.Model(&user).Updates(updateData)

c.JSON(http.StatusOK, gin.H{"user": user})
}

// 删除用户(DELETE /users/:id)
func (h *UserHandler) DeleteUser(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的用户ID"})
return
}

result := h.DB.Delete(&models.User{}, id)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"})
return
}

if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}

c.JSON(http.StatusOK, gin.H{"message": "用户删除成功"})
}

JWT认证和加密

1. JWT工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// utils/jwt.go
package utils

import (
"time"
"github.com/golang-jwt/jwt/v5"
)

var jwtSecret = []byte("your-secret-key")

type Claims struct {
UserID uint `json:"user_id"`
jwt.RegisteredClaims
}

// 生成JWT Token
func GenerateToken(userID uint) (string, error) {
nowTime := time.Now()
expireTime := nowTime.Add(24 * time.Hour)

claims := Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expireTime),
IssuedAt: jwt.NewNumericDate(nowTime),
Issuer: "gin-demo",
},
}

tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := tokenClaims.SignedString(jwtSecret)

return token, err
}

// 解析JWT Token
func ParseToken(token string) (*Claims, error) {
tokenClaims, err := jwt.ParseWithClaims(token, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})

if tokenClaims != nil {
if claims, ok := tokenClaims.Claims.(*Claims); ok && tokenClaims.Valid {
return claims, nil
}
}

return nil, err
}

// 验证Token
func ValidateToken(token string) bool {
claims, err := ParseToken(token)
if err != nil {
return false
}

// 检查token是否过期
return claims.ExpiresAt.Time.After(time.Now())
}

2. 密码加密工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// utils/password.go
package utils

import (
"golang.org/x/crypto/bcrypt"
)

// 加密密码
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}

// 验证密码
func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

3. 认证中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// middleware/auth.go
package middleware

import (
"gin-demo/utils"
"net/http"
"strings"

"github.com/gin-gonic/gin"
)

func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 从Header获取token
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请提供认证token"})
c.Abort()
return
}

// 检查Bearer格式
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
c.JSON(http.StatusUnauthorized, gin.H{"error": "token格式错误"})
c.Abort()
return
}

// 解析token
claims, err := utils.ParseToken(parts[1])
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的token"})
c.Abort()
return
}

// 将用户信息存入上下文
c.Set("userID", claims.UserID)
c.Next()
}
}

4. 认证路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// handlers/auth.go
package handlers

import (
"gin-demo/models"
"gin-demo/utils"
"net/http"

"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

type AuthHandler struct {
DB *gorm.DB
}

func NewAuthHandler(db *gorm.DB) *AuthHandler {
return &AuthHandler{DB: db}
}

// 注册
func (h *AuthHandler) Register(c *gin.Context) {
var user models.User

if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// 检查邮箱是否已存在
var existingUser models.User
if err := h.DB.Where("email = ?", user.Email).First(&existingUser).Error; err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱已存在"})
return
}

// 加密密码
hashedPassword, err := utils.HashPassword(user.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
return
}
user.Password = hashedPassword

// 创建用户
if err := h.DB.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建用户失败"})
return
}

c.JSON(http.StatusCreated, gin.H{
"message": "注册成功",
"user": gin.H{
"id": user.ID,
"name": user.Name,
"email": user.Email,
},
})
}

// 登录
func (h *AuthHandler) Login(c *gin.Context) {
var loginData struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}

if err := c.ShouldBindJSON(&loginData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// 查找用户
var user models.User
if err := h.DB.Where("email = ?", loginData.Email).First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"})
return
}

// 验证密码
if !utils.CheckPasswordHash(loginData.Password, user.Password) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"})
return
}

// 生成token
token, err := utils.GenerateToken(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "token生成失败"})
return
}

c.JSON(http.StatusOK, gin.H{
"message": "登录成功",
"token": token,
"user": gin.H{
"id": user.ID,
"name": user.Name,
"email": user.Email,
},
})
}

// 获取当前用户信息
func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
userID, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
return
}

var user models.User
if err := h.DB.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}

c.JSON(http.StatusOK, gin.H{
"user": gin.H{
"id": user.ID,
"name": user.Name,
"email": user.Email,
},
})
}

项目结构和配置

1. 完整的项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
gin-demo/
├── main.go # 应用入口
├── go.mod # Go模块文件
├── go.sum # 依赖校验
├── config/
│ ├── config.go # 配置文件
│ └── database.go # 数据库配置
├── models/
│ ├── user.go # 用户模型
│ └── product.go # 产品模型
├── handlers/
│ ├── user.go # 用户处理器
│ ├── auth.go # 认证处理器
│ └── product.go # 产品处理器
├── middleware/
│ ├── auth.go # 认证中间件
│ ├── logger.go # 日志中间件
│ └── cors.go # CORS中间件
├── utils/
│ ├── jwt.go # JWT工具
│ ├── password.go # 密码工具
│ └── response.go # 响应工具
├── routes/
│ └── routes.go # 路由定义
└── tests/
├── user_test.go # 用户测试
└── auth_test.go # 认证测试

2. 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// config/config.go
package config

import (
"log"
"os"
"strconv"
"github.com/joho/godotenv"
)

type Config struct {
AppPort string
DatabaseURL string
JWTSecret string
DebugMode bool
}

func LoadConfig() *Config {
// 加载.env文件(开发环境)
if err := godotenv.Load(); err != nil {
log.Println("未找到.env文件,使用环境变量")
}

debugMode, _ := strconv.ParseBool(getEnv("DEBUG", "false"))

return &Config{
AppPort: getEnv("APP_PORT", "8080"),
DatabaseURL: getEnv("DATABASE_URL", "root:password@tcp(localhost:3306)/gin_demo"),
JWTSecret: getEnv("JWT_SECRET", "your-secret-key"),
DebugMode: debugMode,
}
}

func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}

3. 路由定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// routes/routes.go
package routes

import (
"gin-demo/config"
"gin-demo/handlers"
"gin-demo/middleware"

"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

func SetupRoutes(router *gin.Engine, db *gorm.DB, cfg *config.Config) {
// 初始化处理器
userHandler := handlers.NewUserHandler(db)
authHandler := handlers.NewAuthHandler(db)

// 公共路由
public := router.Group("/api")
{
// 认证路由
auth := public.Group("/auth")
{
auth.POST("/register", authHandler.Register)
auth.POST("/login", authHandler.Login)
}

// 公开的用户路由
public.GET("/users", userHandler.GetUsers)
public.GET("/users/:id", userHandler.GetUser)
}

// 需要认证的路由
protected := public.Group("")
protected.Use(middleware.JWTAuth())
{
// 用户管理
users := protected.Group("/users")
{
users.POST("", userHandler.CreateUser)
users.PUT("/:id", userHandler.UpdateUser)
users.DELETE("/:id", userHandler.DeleteUser)
}

// 获取当前用户
protected.GET("/me", authHandler.GetCurrentUser)
}

// 健康检查
router.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
}

4. 主程序入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// main.go
package main

import (
"gin-demo/config"
"gin-demo/models"
"gin-demo/routes"
"log"

"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

func main() {
// 加载配置
cfg := config.LoadConfig()

// 设置Gin模式
if cfg.DebugMode {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}

// 创建Gin实例
router := gin.Default()

// 连接数据库
db, err := config.ConnectDB()
if err != nil {
log.Fatal("数据库连接失败:", err)
}

// 自动迁移(开发环境)
if cfg.DebugMode {
err = db.AutoMigrate(&models.User{})
if err != nil {
log.Fatal("数据库迁移失败:", err)
}
}

// 设置路由
routes.SetupRoutes(router, db, cfg)

// 启动服务器
log.Printf("服务器启动在端口 %s", cfg.AppPort)
if err := router.Run(":" + cfg.AppPort); err != nil {
log.Fatal("服务器启动失败:", err)
}
}

测试和部署

1. 单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// tests/user_test.go
package tests

import (
"gin-demo/handlers"
"gin-demo/models"
"net/http"
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)

func setupTestDB() *gorm.DB {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
panic("测试数据库连接失败")
}

// 迁移表
db.AutoMigrate(&models.User{})
return db
}

func TestGetUsers(t *testing.T) {
// 设置测试数据库
db := setupTestDB()

// 创建测试数据
db.Create(&models.User{Name: "测试用户", Email: "test@example.com"})

// 创建处理器
handler := handlers.NewUserHandler(db)

// 创建Gin测试上下文
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)

// 执行处理器
handler.GetUsers(c)

// 验证响应
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "测试用户")
}

func TestCreateUser(t *testing.T) {
db := setupTestDB()
handler := handlers.NewUserHandler(db)

w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)

// 设置JSON请求体
c.Request = httptest.NewRequest("POST", "/users",
strings.NewReader(`{"name":"新用户","email":"new@example.com"}`))
c.Request.Header.Set("Content-Type", "application/json")

handler.CreateUser(c)

assert.Equal(t, http.StatusCreated, w.Code)
assert.Contains(t, w.Body.String(), "新用户")
}

2. 集成测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// tests/integration_test.go
package tests

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"gin-demo/main"
"github.com/stretchr/testify/assert"
)

func TestUserCRUDIntegration(t *testing.T) {
// 启动测试服务器
router := main.SetupRouter()

// 测试创建用户
userData := map[string]interface{}{
"name": "集成测试用户",
"email": "integration@test.com",
}

jsonData, _ := json.Marshal(userData)
req, _ := http.NewRequest("POST", "/api/users", bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")

w := httptest.NewRecorder()
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusCreated, w.Code)

// 解析响应获取用户ID
var response map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &response)
user := response["user"].(map[string]interface{})
userID := user["id"].(float64)

// 测试获取用户
req, _ = http.NewRequest("GET", fmt.Sprintf("/api/users/%.0f", userID), nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "集成测试用户")
}

3. Docker部署配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Dockerfile
FROM golang:1.22-alpine AS builder

# 安装必要的工具
RUN apk add --no-cache git

# 设置工作目录
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 config/config.yaml ./

# 暴露端口
EXPOSE 8080

# 运行应用
CMD ["./main"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# docker-compose.yml
version: '3.8'

services:
app:
build: .
ports:
- "8080:8080"
environment:
- DATABASE_URL=mysql://user:password@db:3306/gin_demo
- JWT_SECRET=your-secret-key
depends_on:
- db

db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: gin_demo
MYSQL_USER: user
MYSQL_PASSWORD: password
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql

volumes:
db_data:

4. 环境配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# config/config.yaml
app:
port: "8080"
env: "production"
debug: false

database:
host: "localhost"
port: "3306"
username: "user"
password: "password"
name: "gin_demo"

jwt:
secret: "your-production-secret-key"
expires_in: "24h"

logging:
level: "info"
file: "./logs/app.log"
1
2
3
4
5
# .env.example
APP_PORT=8080
DATABASE_URL=mysql://user:password@localhost:3306/gin_demo
JWT_SECRET=your-secret-key
DEBUG=false

性能优化和最佳实践

1. 连接池配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// config/database.go (扩展)
import "gorm.io/gorm"
import "gorm.io/driver/mysql"

func ConnectDBWithPool(config DatabaseConfig) (*gorm.DB, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
config.Username, config.Password, config.Host, config.Port, config.Database)

db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
// 禁用默认事务(提高性能)
SkipDefaultTransaction: true,
// 预编译SQL
PrepareStmt: true,
})

if err != nil {
return nil, err
}

// 获取底层sql.DB设置连接池
sqlDB, err := db.DB()
if err != nil {
return nil, err
}

// 设置连接池参数
sqlDB.SetMaxIdleConns(10) // 最大空闲连接数
sqlDB.SetMaxOpenConns(100) // 最大打开连接数
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大生命周期

return db, nil
}

2. 缓存策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// utils/cache.go
package utils

import (
"context"
"time"
"github.com/go-redis/redis/v8"
)

type Cache struct {
client *redis.Client
ctx context.Context
}

func NewCache(addr, password string, db int) *Cache {
client := redis.NewClient(&redis.Options{
Addr: addr,
Password: password,
DB: db,
})

return &Cache{
client: client,
ctx: context.Background(),
}
}

// 设置缓存
func (c *Cache) Set(key string, value interface{}, expiration time.Duration) error {
return c.client.Set(c.ctx, key, value, expiration).Err()
}

// 获取缓存
func (c *Cache) Get(key string) (string, error) {
return c.client.Get(c.ctx, key).Result()
}

// 删除缓存
func (c *Cache) Delete(key string) error {
return c.client.Del(c.ctx, key).Err()
}

// 缓存用户信息示例
func CacheUser(user models.User, cache *Cache) error {
userJSON, err := json.Marshal(user)
if err != nil {
return err
}

return cache.Set("user:"+strconv.FormatUint(uint64(user.ID), 10),
userJSON, 30*time.Minute)
}

3. 性能监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// middleware/metrics.go
package middleware

import (
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"time"
)

var (
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP请求耗时",
},
[]string{"method", "endpoint"},
)

requestCount = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "HTTP请求总数",
},
[]string{"method", "endpoint", "status"},
)
)

func init() {
prometheus.MustRegister(requestDuration, requestCount)
}

func MetricsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()

// 继续处理请求
c.Next()

duration := time.Since(start).Seconds()
status := c.Writer.Status()

// 记录指标
requestDuration.WithLabelValues(
c.Request.Method,
c.FullPath(),
).Observe(duration)

requestCount.WithLabelValues(
c.Request.Method,
c.FullPath(),
strconv.Itoa(status),
).Inc()
}
}

// 暴露指标端点
func MetricsHandler() gin.HandlerFunc {
h := promhttp.Handler()
return func(c *gin.Context) {
h.ServeHTTP(c.Writer, c.Request)
}
}

总结

Gin框架优势

  1. 高性能: 基于Go语言的并发特性,性能远超Node.js框架
  2. 简洁API: 类似Express的直观API设计,学习成本低
  3. 丰富中间件: 内置常用中间件,生态完善
  4. 强类型: Go语言的类型安全减少运行时错误

从Node.js迁移建议

  1. 错误处理: Go使用显式错误返回,替代JS的try-catch
  2. 并发模型: 理解goroutine和channel,替代async/await
  3. 类型系统: 充分利用Go的强类型优势
  4. 依赖管理: 使用Go Modules替代npm

下一步学习

  1. 微服务架构: 学习gRPC和微服务通信
  2. 容器化部署: 掌握Docker和Kubernetes
  3. 监控告警: 集成Prometheus和Grafana
  4. CI/CD: 建立自动化部署流程

这份笔记涵盖了Gin框架的核心概念和实际应用,帮助你快速从Node.js开发转向Go语言Web开发。通过实际项目练习,你将能够熟练掌握Gin框架并构建高性能的Web应用。

本文作者:辰風依恛
本文链接:https://766187397.github.io/2025/10/12/Gin%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×