Go Gin MySQL Web API 项目开发教程
干中学。在 AI 的帮助下使用 Go 语言完成了后端项目的开发,项目已经成功上线运行。但面对实际代码,我仍感到一头雾水。还是需要 “先自己,后 AI” 的(基本)练习将夯实基础,提升独立开发能力。
📚 教程简介
本教程将带你从零开始构建一个完整的 Go Web API 项目,使用 Gin 框架和 MySQL 数据库。通过这个项目,你将学习到:
- Go 语言 Web 开发基础
- Gin 框架的使用
- MySQL 数据库操作
- 分层架构设计
- RESTful API 设计
- 项目结构组织
🎯 项目目标
我们将构建一个用户和文章管理系统,包含以下功能:
- 用户注册、查询、更新、删除
- 文章创建、查询、更新、删除
- 用户与文章的关联查询
- 数据库事务处理
📋 前置要求
在开始之前,请确保你已经安装了以下软件:
🚀 第一步:环境准备
1.1 验证 Go 环境
打开终端,验证 Go 是否正确安装:
go version
应该看到类似输出:
go version go1.24.0 darwin/amd64
1.2 设置 Go 工作区
# 创建项目目录
mkdir golang-gin-mysql-tutorial
cd golang-gin-mysql-tutorial
# 初始化 Go 模块
go mod init github.com/yourname/golang-gin-mysql
1.3 安装项目依赖
# 安装 Gin Web 框架
go get -u github.com/gin-gonic/gin
# 安装 MySQL 驱动
go get -u github.com/go-sql-driver/mysql
# 安装配置管理库
go get -u github.com/spf13/viper
🗄️ 第二步:数据库准备
2.1 创建数据库
打开 MySQL 客户端,执行以下 SQL 语句:
-- 创建数据库
CREATE DATABASE IF NOT EXISTS dev_learning
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
-- 使用数据库
USE dev_learning;
2.2 创建数据表
-- 用户表
CREATE TABLE IF NOT EXISTS users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(64) NOT NULL UNIQUE,
email VARCHAR(128) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
-- 文章表(每篇文章属于一个用户)
CREATE TABLE IF NOT EXISTS articles (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
2.3 数据库设计说明
- BIGINT UNSIGNED:防止负数溢出,提供更大的 ID 范围
- ON DELETE CASCADE:删除用户时自动删除其文章,保证数据一致性
- created_at/updated_at:自动维护时间戳,便于数据追踪
- 外键约束:保证数据完整性,防止孤立数据
📁 第三步:项目结构设计
3.1 创建项目目录结构
mkdir -p cmd/server
mkdir -p internal/{config,db,handler,model,repository,service}
mkdir -p pkg
3.2 项目结构说明
golang-gin-mysql/
├── cmd/ # 应用程序入口
│ └── server/
│ └── main.go # 主程序入口
├── internal/ # 内部包(不对外暴露)
│ ├── config/ # 配置管理
│ ├── db/ # 数据库连接
│ ├── handler/ # HTTP 处理器
│ ├── model/ # 数据模型
│ ├── repository/ # 数据访问层
│ └── service/ # 业务逻辑层
├── pkg/ # 公共包(可对外暴露)
├── config.yaml # 配置文件
├── go.mod # Go 模块文件
└── README.md # 项目说明
3.3 分层架构说明
- Handler 层:处理 HTTP 请求和响应
- Service 层:处理业务逻辑
- Repository 层:处理数据访问
- Model 层:定义数据结构
⚙️ 第四步:配置管理模块
4.1 创建配置文件
创建 config.yaml 文件:
# 应用程序配置文件
server:
# 服务器监听端口
port: ":8080"
mysql:
# 数据库连接字符串
dsn: "root:password@tcp(127.0.0.1:3306)/dev_learning?parseTime=true&loc=Local"
4.2 创建配置结构体
创建 internal/config/config.go:
package config
import "github.com/spf13/viper"
// Config 应用程序配置结构体
type Config struct {
Server struct {
Port string `mapstructure:"port"`
}
MySQL struct {
DSN string `mapstructure:"dsn"`
}
}
// Load 加载配置文件
func Load(path string) (*Config, error) {
v := viper.New()
v.SetConfigFile(path)
v.SetConfigType("yaml")
if err := v.ReadInConfig(); err != nil {
return nil, err
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}
🗃️ 第五步:数据库连接模块
5.1 创建数据库连接
创建 internal/db/db.go:
package db
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
// NewMySql 创建 MySQL 数据库连接
func NewMySql(dsn string) (*sql.DB, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
// 测试连接
if err := db.Ping(); err != nil {
return nil, err
}
return db, nil
}
📊 第六步:数据模型定义
6.1 用户模型
创建 internal/model/user.go:
package model
import "time"
// User 用户结构体
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
6.2 文章模型
创建 internal/model/article.go:
package model
import "time"
// Article 文章结构体
type Article struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
Title string `json:"title"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
🔧 第七步:Repository 层开发
Repository 层负责数据访问,封装数据库操作,为上层提供统一的数据访问接口。
7.1 用户 Repository
创建 internal/repository/user_repository.go:
package repository
import (
"database/sql"
"github.com/openapphub/golang-gin-mysql/internal/model"
)
type UserRepository struct {
db *sql.DB
}
func NewUserRepo(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
// Create 创建用户
func (r *UserRepository) Create(user *model.User) error {
query := `INSERT INTO users (username, email) VALUES (?, ?)`
result, err := r.db.Exec(query, user.Username, user.Email)
if err != nil {
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
user.ID = id
return nil
}
// GetByID 根据ID获取用户
func (r *UserRepository) GetByID(id int64) (*model.User, error) {
query := `SELECT id, username, email, created_at, updated_at FROM users WHERE id = ?`
row := r.db.QueryRow(query, id)
user := &model.User{}
err := row.Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt, &user.UpdatedAt)
if err != nil {
return nil, err
}
return user, nil
}
// Update 更新用户
func (r *UserRepository) Update(user *model.User) error {
query := `UPDATE users SET username = ?, email = ? WHERE id = ?`
_, err := r.db.Exec(query, user.Username, user.Email, user.ID)
return err
}
// Delete 删除用户
func (r *UserRepository) Delete(id int64) error {
query := `DELETE FROM users WHERE id = ?`
_, err := r.db.Exec(query, id)
return err
}
7.2 文章 Repository
创建 internal/repository/article_repository.go:
package repository
import (
"database/sql"
"github.com/openapphub/golang-gin-mysql/internal/model"
)
type ArticleRepository struct {
db *sql.DB
}
func NewArticleRepo(db *sql.DB) *ArticleRepository {
return &ArticleRepository{db: db}
}
// Create 创建文章
func (r *ArticleRepository) Create(article *model.Article) error {
query := `INSERT INTO articles (user_id, title, content) VALUES (?, ?, ?)`
result, err := r.db.Exec(query, article.UserID, article.Title, article.Content)
if err != nil {
return err
}
id, err := result.LastInsertId()
if err != nil {
return err
}
article.ID = id
return nil
}
// GetByID 根据ID获取文章
func (r *ArticleRepository) GetByID(id int64) (*model.Article, error) {
query := `SELECT id, user_id, title, content, created_at, updated_at FROM articles WHERE id = ?`
row := r.db.QueryRow(query, id)
article := &model.Article{}
err := row.Scan(&article.ID, &article.UserID, &article.Title, &article.Content, &article.CreatedAt, &article.UpdatedAt)
if err != nil {
return nil, err
}
return article, nil
}
// GetByUserID 获取用户的所有文章
func (r *ArticleRepository) GetByUserID(userID int64) ([]*model.Article, error) {
query := `SELECT id, user_id, title, content, created_at, updated_at FROM articles WHERE user_id = ?`
rows, err := r.db.Query(query, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var articles []*model.Article
for rows.Next() {
article := &model.Article{}
err := rows.Scan(&article.ID, &article.UserID, &article.Title, &article.Content, &article.CreatedAt, &article.UpdatedAt)
if err != nil {
return nil, err
}
articles = append(articles, article)
}
return articles, nil
}
// Update 更新文章
func (r *ArticleRepository) Update(article *model.Article) error {
query := `UPDATE articles SET title = ?, content = ? WHERE id = ?`
_, err := r.db.Exec(query, article.Title, article.Content, article.ID)
return err
}
// Delete 删除文章
func (r *ArticleRepository) Delete(id int64) error {
query := `DELETE FROM articles WHERE id = ?`
_, err := r.db.Exec(query, id)
return err
}
7.3 Repository 层设计要点
- 单一职责:每个 Repository 只负责一个实体的数据访问
- 接口隔离:提供最小必要的接口
- 错误处理:统一处理数据库操作错误
- 资源管理:正确关闭数据库连接和结果集
🏢 第八步:Service 层开发
Service 层处理业务逻辑,协调多个 Repository 操作,管理事务。
8.1 用户服务
创建 internal/service/user_service.go:
package service
import (
"database/sql"
"github.com/openapphub/golang-gin-mysql/internal/model"
"github.com/openapphub/golang-gin-mysql/internal/repository"
)
type UserService struct {
db *sql.DB
userRepo *repository.UserRepository
articleRepo *repository.ArticleRepository
}
func NewUserService(db *sql.DB, userRepo *repository.UserRepository, articleRepo *repository.ArticleRepository) *UserService {
return &UserService{
db: db,
userRepo: userRepo,
articleRepo: articleRepo,
}
}
// CreateUser 创建用户(包含默认文章)
func (s *UserService) CreateUser(user *model.User) error {
// 开始事务
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// 创建用户
if err := s.userRepo.Create(user); err != nil {
return err
}
// 创建默认文章
defaultArticle := &model.Article{
UserID: user.ID,
Title: "欢迎使用系统",
Content: "这是您的第一篇文章,欢迎使用我们的系统!",
}
if err := s.articleRepo.Create(defaultArticle); err != nil {
return err
}
// 提交事务
return tx.Commit()
}
// GetUser 获取用户信息
func (s *UserService) GetUser(id int64) (*model.User, error) {
return s.userRepo.GetByID(id)
}
// UpdateUser 更新用户信息
func (s *UserService) UpdateUser(user *model.User) error {
return s.userRepo.Update(user)
}
// DeleteUser 删除用户
func (s *UserService) DeleteUser(id int64) error {
return s.userRepo.Delete(id)
}
// GetUserWithArticles 获取用户及其文章列表
func (s *UserService) GetUserWithArticles(id int64) (*model.User, []*model.Article, error) {
user, err := s.userRepo.GetByID(id)
if err != nil {
return nil, nil, err
}
articles, err := s.articleRepo.GetByUserID(id)
if err != nil {
return nil, nil, err
}
return user, articles, nil
}
8.2 Service 层设计要点
- 业务逻辑封装:将复杂的业务规则封装在 Service 层
- 事务管理:确保数据一致性
- 依赖注入:通过构造函数注入依赖
- 错误处理:统一处理业务逻辑错误
🌐 第九步:Handler 层开发
Handler 层处理 HTTP 请求和响应,参数验证,调用 Service 层。
9.1 用户处理器
创建 internal/handler/user_handler.go:
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/openapphub/golang-gin-mysql/internal/model"
"github.com/openapphub/golang-gin-mysql/internal/service"
)
type UserHandler struct {
userService *service.UserService
}
func NewUserHandler(userService *service.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
// Create 创建用户
func (h *UserHandler) Create(c *gin.Context) {
var user model.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.userService.CreateUser(&user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, user)
}
// Get 获取用户信息
func (h *UserHandler) Get(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
user, articles, err := h.userService.GetUserWithArticles(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
return
}
response := gin.H{
"user": user,
"articles": articles,
}
c.JSON(http.StatusOK, response)
}
// Update 更新用户信息
func (h *UserHandler) Update(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
var user model.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user.ID = id
if err := h.userService.UpdateUser(&user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, user)
}
// Delete 删除用户
func (h *UserHandler) Delete(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
if err := h.userService.DeleteUser(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"})
}
9.2 文章处理器
创建 internal/handler/article_handler.go:
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/openapphub/golang-gin-mysql/internal/model"
"github.com/openapphub/golang-gin-mysql/internal/repository"
)
type ArticleHandler struct {
articleRepo *repository.ArticleRepository
}
func NewArticleHandler(articleRepo *repository.ArticleRepository) *ArticleHandler {
return &ArticleHandler{articleRepo: articleRepo}
}
// Create 创建文章
func (h *ArticleHandler) Create(c *gin.Context) {
var article model.Article
if err := c.ShouldBindJSON(&article); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.articleRepo.Create(&article); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, article)
}
// Get 获取单篇文章
func (h *ArticleHandler) Get(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid article ID"})
return
}
article, err := h.articleRepo.GetByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Article not found"})
return
}
c.JSON(http.StatusOK, article)
}
// Update 更新文章
func (h *ArticleHandler) Update(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid article ID"})
return
}
var article model.Article
if err := c.ShouldBindJSON(&article); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
article.ID = id
if err := h.articleRepo.Update(&article); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, article)
}
// Delete 删除文章
func (h *ArticleHandler) Delete(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid article ID"})
return
}
if err := h.articleRepo.Delete(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Article deleted successfully"})
}
// ListByUser 获取用户的所有文章
func (h *ArticleHandler) ListByUser(c *gin.Context) {
userID, err := strconv.ParseInt(c.Param("uid"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"})
return
}
articles, err := h.articleRepo.GetByUserID(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, articles)
}
9.3 Handler 层设计要点
- 参数验证:验证请求参数的有效性
- 错误处理:统一的错误响应格式
- 状态码:使用正确的 HTTP 状态码
- JSON 序列化:自动处理 JSON 序列化
🚀 第十步:主程序入口
10.1 创建主程序
创建 cmd/server/main.go:
package main
import (
"log"
"github.com/gin-gonic/gin"
"github.com/openapphub/golang-gin-mysql/internal/config"
"github.com/openapphub/golang-gin-mysql/internal/db"
"github.com/openapphub/golang-gin-mysql/internal/handler"
"github.com/openapphub/golang-gin-mysql/internal/repository"
"github.com/openapphub/golang-gin-mysql/internal/service"
)
func main() {
// 1. 加载配置
cfg, err := config.Load("config.yaml")
if err != nil {
log.Fatalf("[CONFIG] load failed: %v", err)
}
// 2. 初始化数据库连接
sqlDB, err := db.NewMySql(cfg.MySQL.DSN)
if err != nil {
log.Fatalf("[DB] connect failed: %v", err)
}
defer sqlDB.Close()
// 3. 创建 Repository 层实例
userRepo := repository.NewUserRepo(sqlDB)
articleRepo := repository.NewArticleRepo(sqlDB)
// 4. 创建 Service 层实例
userSvc := service.NewUserService(sqlDB, userRepo, articleRepo)
// 5. 初始化 Gin 引擎
r := gin.Default()
// 6. 注册路由
userHandler := handler.NewUserHandler(userSvc)
userGroup := r.Group("/users")
{
userGroup.POST("", userHandler.Create)
userGroup.GET("/:id", userHandler.Get)
userGroup.PUT("/:id", userHandler.Update)
userGroup.DELETE("/:id", userHandler.Delete)
}
articleHandler := handler.NewArticleHandler(articleRepo)
articleGroup := r.Group("/articles")
{
articleGroup.POST("", articleHandler.Create)
articleGroup.GET("/:id", articleHandler.Get)
articleGroup.PUT("/:id", articleHandler.Update)
articleGroup.DELETE("/:id", articleHandler.Delete)
articleGroup.GET("/user/:uid", articleHandler.ListByUser)
}
// 7. 启动服务器
log.Printf("[SERVER] listening on %s", cfg.Server.Port)
if err := r.Run(cfg.Server.Port); err != nil {
log.Fatalf("[SERVER] run failed: %v", err)
}
}
10.2 主程序设计要点
- 依赖注入:按层次注入依赖
- 错误处理:启动失败时记录日志并退出
- 资源管理:正确关闭数据库连接
- 路由组织:使用路由组组织相关接口
🧪 第十一步:测试和调试
11.1 启动服务器
go run cmd/server/main.go
看到以下输出表示启动成功:
[CONFIG] load success
[DB] connect success
[SERVER] listening on :8080
11.2 测试 API 接口
创建用户
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"username": "john_doe", "email": "john@example.com"}'
获取用户信息
curl http://localhost:8080/users/1
创建文章
curl -X POST http://localhost:8080/articles \
-H "Content-Type: application/json" \
-d '{"user_id": 1, "title": "我的第一篇文章", "content": "这是文章内容..."}'
获取用户的所有文章
curl http://localhost:8080/articles/user/1
11.3 常见问题排查
-
数据库连接失败
- 检查 MySQL 服务是否启动
- 验证配置文件中的数据库连接信息
- 确认数据库和表是否已创建
-
端口被占用
- 修改
config.yaml中的端口配置 - 使用
lsof -i :8080查看端口占用情况
- 修改
-
依赖安装失败
- 检查网络连接
- 确认 Go 版本是否符合要求
- 使用
go mod tidy重新整理依赖
📚 第十二步:学习总结
12.1 项目架构回顾
通过这个项目,我们学习了:
- 分层架构设计:Handler → Service → Repository → Model
- 依赖注入:通过构造函数注入依赖
- 错误处理:Go 语言的错误处理机制
- 数据库操作:使用 database/sql 包
- RESTful API:标准的 REST 接口设计
- 配置管理:使用 Viper 管理配置
12.2 扩展建议
- 添加中间件:日志、认证、限流等
- 数据验证:使用 validator 库
- 单元测试:为每个层添加测试
- API 文档:使用 Swagger 生成文档
- 容器化:使用 Docker 部署
12.3 最佳实践
- 代码组织:按功能模块组织代码
- 错误处理:统一的错误处理机制
- 日志记录:适当的日志记录
- 配置管理:外部化配置
- 数据库设计:合理的数据表设计
🎉 恭喜!
你已经成功构建了一个完整的 Go Web API 项目!这个项目展示了 Go 语言 Web 开发的最佳实践,包括:
- 清晰的项目结构
- 分层架构设计
- 数据库操作
- RESTful API 设计
- 错误处理
继续学习更多 Go 语言特性和 Web 开发技术,构建更复杂的应用程序!
Happy Coding! 🚀
运行: