CHAPTER 4 · GIN MYSQL API
04
现在,让 Gin 从 MySQL 查询相册,并把结果返回给浏览器。
先看一个完整接口:连接 MySQL,查询相册列表,扫描为 Go 结构体,最后返回 JSON。
第一步:准备表和依赖
相册表已经存在,Go 服务只负责查询它。
CREATE TABLE albums (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
name VARCHAR(100) NOT NULL,
visibility TINYINT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_albums_user_created(user_id, created_at)
);go get github.com/gin-gonic/gin
go get github.com/go-sql-driver/mysql完整代码
先跑通,再拆解。
点击代码中的高亮行,查看它在接口链路中的作用。
package main
import (
"database/sql"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
)
type Album struct {
ID uint64 `json:"id"`
UserID uint64 `json:"userId"`
Name string `json:"name"`
Visibility int8 `json:"visibility"`
CreatedAt time.Time `json:"createdAt"`
}
func main() {
if err != nil {
log.Fatal("create database pool failed:", err)
}
defer db.Close()
router := gin.Default()
router.GET("/api/albums", func(c *gin.Context) {
if err != nil {
log.Printf("query albums failed: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"code": "INTERNAL_ERROR", "message": "查询相册失败",
})
return
}
albums := make([]Album, 0)
for rows.Next() {
var album Album
c.JSON(http.StatusInternalServerError, gin.H{
"code": "INTERNAL_ERROR", "message": "读取相册数据失败",
})
return
}
albums = append(albums, album)
}
})
if err := router.Run(":8080"); err != nil {
log.Fatal(err)
}
}浏览器访问
http://localhost:8080/api/albums
{
"code": "OK",
"message": "success",
"data": [
{
"id": 12,
"userId": 1001,
"name": "2026 暑假",
"visibility": 1,
"createdAt": "2026-07-17T09:30:00+08:00"
}
]
}先围绕代码提问
这些问题比逐行背代码更重要。
为什么查询字段没有使用 SELECT *?
为什么需要 parseTime=true?
为什么结构体要定义 JSON 标签?
为什么不能直接返回 rows?
为什么必须调用 rows.Close()?
为什么循环结束后还要检查 rows.Err()?
为什么数据库错误不能直接返回给前端?
为什么连接池在应用启动时创建?
为什么接口只返回 20 条数据?
如果查询超过 10 秒,接口会发生什么?
一次请求的完整链路
MySQL 返回数据库行,Gin 返回 JSON。
中间必须完成查询、扫描和结构转换。数据库行不会自动变成接口 JSON。
连接池
数据库连接不是每次请求都创建。
错误实现
func ListAlbums(c *gin.Context) {
db, _ := sql.Open("mysql", dsn)
defer db.Close()
rows, _ := db.Query("SELECT * FROM albums")
c.JSON(200, rows)
}推荐方式
应用启动
↓
创建一个 sql.DB
↓
配置连接池
↓
传递给 Repository
↓
所有请求复用
↓
应用退出时统一关闭
每个请求都 sql.Open
sql.DB 是连接池管理对象,应复用。
sql.Open 成功
它主要创建连接池对象,真正检查可用性要调用 db.Ping()。
直接返回 rows
需要扫描为结构体,再序列化为 JSON。
超时与安全
数据库调用要有边界,外部输入不能拼 SQL。
增加查询超时
ctx, cancel := context.WithTimeout(
c.Request.Context(),
3*time.Second,
)
defer cancel()
rows, err := db.QueryContext(ctx, query)所有外部依赖调用,包括数据库查询,都应该有明确的超时边界。
使用参数化查询
rows, err := db.QueryContext(
c.Request.Context(),
`SELECT id, user_id, name
FROM albums
WHERE user_id = ?
LIMIT 20`,
userID,
)外部输入不能直接拼接进 SQL,应使用 ? 占位符。
错误写法:
query := "SELECT * FROM albums WHERE user_id = " + userID引入 GORM
普通 CRUD 场景,优先用 ORM 提高效率。
原生 SQL 需要手写 SQL、参数绑定、扫描、字段映射、错误处理、结果组装和分页逻辑。普通查询可以交给 GORM 简化。
原生 SQL
rows, err := db.QueryContext(...)
for rows.Next() {
rows.Scan(...)
}GORM
gormDB.
Select("id", "user_id", "name").
Order("created_at DESC").
Limit(20).
Find(&albums)根据主键查询、创建、更新、删除
优先 GORM减少重复样板代码。
复杂统计报表、窗口函数、特殊锁
考虑 SQL原生 SQL 更容易精确控制。
性能敏感查询
具体评估先看最终 SQL、索引和执行计划。
ORM 解决的是开发效率问题,不会自动解决性能、权限、事务和数据建模问题。
重要警示
不要在服务启动时执行 AutoMigrate。
gormDB.AutoMigrate(
&User{},
&Album{},
&Photo{},
)AutoMigrate 可以用于个人原型或临时实验,但不应该作为正式上线系统的数据库变更方案。
服务每次启动都修改数据库结构,安全吗?
多个实例同时启动会发生什么?
表结构修改是否经过审核?
生产数据库变更失败后如何回滚?
代码回滚是否意味着数据库也能自动回滚?
数据库变更是否有完整记录?
推荐方式:显式 SQL + 审核流程
database/
├── migrations/
│ ├── 001_create_users.sql
│ ├── 002_create_albums.sql
│ ├── 003_create_photos.sql
│ └── 004_add_album_index.sql
└── rollback/
├── 004_remove_album_index.sql
└── ...编写 SQL → 代码评审 → 测试验证 → 评估影响与回滚方案 → 审核执行 → 记录结果
数据库结构变化必须是可审计的发布行为。
推荐代码结构
不要把所有数据库逻辑写在 Gin Handler 中。
请求 → Handler → Service → Repository → GORM / MySQL
type AlbumRepository struct {
db *gorm.DB
}
func (r *AlbumRepository) List(limit int) ([]Album, error) {
var albums []Album
result := r.db.
Select("id", "user_id", "name", "visibility", "created_at").
Order("created_at DESC").
Limit(limit).
Find(&albums)
return albums, result.Error
}本章最终结论
- Gin Handler 可以通过数据库连接池执行查询并返回 JSON。
sql.DB是连接池,应在应用启动时创建并复用。- 原生 SQL 需要正确处理参数、扫描、关闭、错误和超时。
- 普通 CRUD 场景优先使用 GORM,提高开发效率。
- 使用 GORM 后仍然必须理解 SQL、索引、事务和查询性能。
- 正式项目禁止依赖
AutoMigrate自动修改生产数据库结构。 - 数据库变更必须通过显式 SQL、代码评审、测试验证和审核流程。
接口已经能查询数据库了,但用户可以修改 URL 中的相册 ID。
后端如何保证他只能访问自己的数据?