开始
- 在本地创建
user-system文件夹。 - 执行
go mod init user-system。 - 按照我上面的建议,创建
cmd,internal,pkg目录。 - 在
internal/model中定义一个User结构体。
定义结构体(Struct)
Go 语言中最基础但也最核心的部分,它相当于其他语言中的“类(Class)”或“数据实体(Entity)”。
🎯 目标:定义一个 User 模型
我们要解决的问题是:如何让 Go 程序知道“用户”长什么样,并且能顺利地把用户数据存入数据库(如 MySQL),或者转换成 JSON 格式发给前端。
第一步:创建文件
请在你的项目中创建文件:internal/model/user.go
第二步:编写代码
把下面的代码复制进去。仔细看代码中的注释,每一行都是知识点。
Go
package model
import (
"time"
)
// User 定义了用户的数据模型
// 在 Go 中,结构体名称首字母大写,表示可以被其他包访问 (Public)
type User struct {
// ID 通常使用 int64 以防数据量过大溢出,或者使用 string (UUID)
// gorm:"primaryKey" 是告诉数据库插件,这是主键
ID int64 `gorm:"primaryKey;autoIncrement" json:"id"`
// Username 用户名
// json:"username" 表示当把结构体转成 JSON 时,字段名显示为 "username" 而不是 "Username"
Username string `gorm:"type:varchar(100);not null;unique" json:"username"`
// Password 密码 (注意:实际开发中永远不要明文存密码,这里仅作演示)
// json:"-" 表示这个字段永远不会被转成 JSON 返回给前端(安全隐患保护)
Password string `gorm:"type:varchar(255);not null" json:"-"`
// Points 用户积分
Points int `gorm:"default:0" json:"points"`
// CreatedAt 创建时间
// time.Time 是 Go 处理时间的标准类型
CreatedAt time.Time `json:"created_at"`
// UpdatedAt 更新时间
UpdatedAt time.Time `json:"updated_at"`
}
// TableName 这是一个可选方法
// 告诉 GORM(ORM库)这张表在数据库里叫什么名字
// 如果不写,GORM 默认会把它变成复数 "users"
func (User) TableName() string {
return "users"
}💡 核心知识点拆解(必读)
1. 大写 vs 小写(可见性)
这是 Go 语言最独特的地方,也是新手最大的坑。
- 首字母大写 (如
Username):公开 (Public/Exported)。其他包(如service层)可以访问,json库也可以读取它。 - 首字母小写 (如
username):私有 (Private/Unexported)。只有同一个包(model)内的代码能访问。
如果你把
Username写成了username,当你把结构体转成 JSON 发给前端时,这个字段会直接凭空消失!因为 JSON 库无法读取私有字段。
2. 反引号与标签 (Tags)
也就是这一串:gorm:"..." json:"..."。 Go 语言本身不知道数据库列名叫什么,也不知道前端想要什么格式。我们通过 Tag 来打小报告:
json:"id": 告诉 JSON 编码器:“嘿,虽然我叫ID,但你输出 JSON 的时候请用小写的id。”json:"-": 告诉 JSON 编码器:“这个字段(如密码)是秘密,千万不要输出到 JSON 里。”gorm:"...": 告诉 ORM 库(数据库工具):“建表的时候,把这个字段设为主键、设为唯一索引”等。
🛑 架构师经验:开发中的“坑”
在定义结构体时,我有三个“血泪经验”要告诉你:
坑一:零值陷阱 (Zero Value)
Go 语言中,所有变量声明后都有默认值(0, false, ""),不会是 undefined。
- 场景:假如你有一个字段
IsAdmin bool。 - 问题:如果前端传来的 JSON 里没有
is_admin这个字段,Go 会自动把它设为false。 - 后果:你无法区分“前端显式传了 false”和“前端压根没传这个值”。
- 解决:在高级开发中,如果必须区分,我们会使用指针
*bool。如果没传,它就是nil。
坑二:时间类型的时区问题
- 场景:
CreatedAt time.Time。 - 问题:数据库通常存的是 UTC 时间,而你本地是 CST (北京时间)。
- 解决:确保你的数据库连接字符串里配置了
parseTime=True&loc=Local,否则取出来的时间可能会乱,导致前端显示差8小时。
坑三:滥用嵌套
你可能会看到有人用 gorm.Model,像这样:
Go
type User struct {
gorm.Model // 自动包含了 ID, CreatedAt, UpdatedAt, DeletedAt
Username string
}新手建议:先不要用这个! 虽然它省事,但它引入了 DeletedAt(软删除),这会给新手的查询逻辑增加复杂度。初学者建议像我上面那样,显式地写出每一个字段,这样你对数据结构有 100% 的掌控感。
连接数据库 (MySQL)
GORM - Go 社区最流行的 ORM 框架,它能帮你把我们在
model里写的User结构体,自动变成数据库里的一张users表(这个过程叫 Migration)。
1. 安装“装备” (依赖包)
在你的项目根目录下(也就是 go.mod 所在的目录),打开终端执行以下命令。这会下载 GORM 核心库和 MySQL 驱动:
Bash
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql2. 编写数据库连接工具
架构师思维: 不要把连接数据库的代码直接写在 main 函数里。数据库连接是一个基础设施,应该封装在 pkg 目录下,方便全项目复用。
创建文件:pkg/database/mysql.go
Go
package database
import (
"fmt"
"log"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// 全局 DB 变量,方便其他包调用(实际项目中建议通过依赖注入传递,这里为简化暂用全局变量)
var DB *gorm.DB
// InitMySQL 初始化 MySQL 连接
// dsn: Data Source Name (用户名:密码@tcp(地址:端口)/库名?配置...)
func InitMySQL(dsn string) {
var err error
// 1. 打开连接
// gorm.Config{Logger: logger.Default.LogMode(logger.Info)}
// 开启 Info 级别的日志,这样你能在控制台看到 GORM 生成的每一条 SQL 语句,这对新手调试极其重要!
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
// 如果连不上数据库,直接 Panic 报错退出。
// 因为对于后端服务来说,连不上数据库通常意味着无法提供服务。
log.Panic("Failed to connect to database: ", err)
}
// 2. 配置连接池 (Connection Pool) —— 企业级开发的关键!
sqlDB, err := DB.DB()
if err != nil {
log.Panic("Failed to get sql.DB: ", err)
}
// 设置空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(10)
// 设置打开数据库连接的最大数量
sqlDB.SetMaxOpenConns(100)
// 设置了连接可复用的最大时间
sqlDB.SetConnMaxLifetime(time.Hour)
fmt.Println("🚀 Database connected successfully!")
}🛑 架构师经验:连接池的坑 很多初学者只会写
gorm.Open,却忘了配置下面的SetMax...。
- 后果: 你的程序运行一段时间后,可能会因为创建了太多数据库连接(Connection Leak)把数据库拖垮,或者因为没有空闲连接复用,导致每次请求都要重新握手,性能极差。
- 经验: 永远要配置连接池参数。
3. 在入口处组装 (Main)
现在我们回到 cmd/api-server/main.go(如果没有这个目录请创建)。这是程序的启动点。
你需要修改下面的 dsn 变量,填入你本地真实的 MySQL 用户名、密码和数据库名。
Go
package main
import (
"fmt"
// 引入我们刚才写的 database 包
// 请将 "user-system" 替换为你 go.mod 里的 module 名字
"user-system/internal/model"
"user-system/pkg/database"
)
func main() {
// 1. 配置数据库连接串 (DSN)
// 格式: 用户名:密码@tcp(127.0.0.1:3306)/数据库名?charset=utf8mb4&parseTime=True&loc=Local
// ⚠️ 注意:parseTime=True 是必须的,否则 time.Time 类型无法正确解析
dsn := "root:123456@tcp(127.0.0.1:3306)/user_db?charset=utf8mb4&parseTime=True&loc=Local"
// 2. 初始化数据库
database.InitMySQL(dsn)
// 3. 自动迁移 (Auto Migrate) —— 见证奇迹的时刻
// GORM 会根据 model.User 结构体,自动在数据库里建表
fmt.Println("Migrating database...")
err := database.DB.AutoMigrate(&model.User{})
if err != nil {
panic("Migration failed: " + err.Error())
}
fmt.Println("✅ Migration finished!")
// 保持程序运行(暂时)
// select {}
}4. 运行与验证
准备数据库: 确保你本地 MySQL 已经启动,并且手动创建了一个叫
user_db的空数据库(GORM 可以建表,但通常不会自动建库)。- SQL:
CREATE DATABASE user_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
- SQL:
运行程序:Bash
go run cmd/api-server/main.go
你应该看到的输出: 如果你看到类似下面的 SQL 日志,说明成功了!
Plaintext
[info] ... CREATE TABLE `users` (`id` bigint AUTO_INCREMENT,`username` varchar(100) ... PRIMARY KEY (`id`))
✅ Migration finished!你可以打开你的数据库管理工具(如 Navicat, DBeaver),查看 user_db 库,里面应该多了一张 users 表,字段和我们在 Go 里面定义的一模一样。
Web 接口开发 (Gin)
既然数据库已经通了,接下来我们完成 B 计划:写一个 Web 接口,让我们可以通过浏览器访问到数据。
我们将使用 Gin 框架,它是目前 Go 领域最流行、性能最好的 Web 框架之一。
1. 安装 Gin
Bash
go get -u github.com/gin-gonic/gin2. 快速实现一个 Create User 接口
为了让你快速看到效果,我们暂时先把逻辑写在 main.go 里(稍后我们会重构到 handler 层,那是架构师的坚持)。
修改 cmd/api-server/main.go,在 AutoMigrate 之后加入 Web 服务代码:
Go
package main
import (
"net/http"
"user-system/internal/model"
"user-system/pkg/database"
"github.com/gin-gonic/gin"
)
func main() {
// ... (前面的数据库代码保持不变) ...
// dsn := ...
// database.InitMySQL(dsn)
// database.DB.AutoMigrate(&model.User{})
// --- 下面是新增的 Web 部分 ---
// 1. 创建 Gin 实例
r := gin.Default()
// 2. 定义路由:POST /users (创建用户)
r.POST("/users", func(c *gin.Context) {
var user model.User
// ShouldBindJSON 会读取 HTTP 请求体里的 JSON,并自动赋值给 user 结构体
// 这里的 c *gin.Context 是 Web 请求的上下文
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 使用 GORM 创建数据
result := database.DB.Create(&user)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
return
}
// 返回成功响应
c.JSON(http.StatusOK, gin.H{
"message": "User created successfully",
"data": user, // Go 会自动把 struct 转回 JSON
})
})
// 3. 启动服务,默认在 8080 端口
fmt.Println("Starting server on :8080...")
r.Run(":8080")
}🧪 动手测试 (Actionable Next Step)
现在你的后端已经是一个具备 Model -> DB -> API 完整链路的微型系统了!
- 重新运行
go run cmd/api-server/main.go。 - 使用 Postman 或终端
curl发送一个请求来创建一个用户:
Curl 命令:
Bash
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"username": "ArchitectureMaster", "points": 100}'预期结果: 你会收到一个包含 id, created_at 等字段的完整 JSON 响应。