Skip to content

开始

  • 在本地创建 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/mysql

2. 编写数据库连接工具

架构师思维: 不要把连接数据库的代码直接写在 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. 运行与验证

  1. 准备数据库: 确保你本地 MySQL 已经启动,并且手动创建了一个叫 user_db 的空数据库(GORM 可以建表,但通常不会自动建库)。

    • SQL: CREATE DATABASE user_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
  2. 运行程序: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/gin

2. 快速实现一个 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 完整链路的微型系统了!

  1. 重新运行 go run cmd/api-server/main.go
  2. 使用 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 响应。