结构体 struct:用代码描述用户、订单、座位

🥰个人主页:会编程的土豆(欢迎来访)

💎作者简介:后端学习者

❄️个人专栏:数据结构与算法数据库leetcode

那些你一个人走过的夜路,终将化作照亮未 来的光

系列:《影院票务 GO》知识点博客 · 第 5 篇

对应文件:models/models.go,以及 dao/* 里的 Scan、handlers/* 里的 JSON/模板


写在前面

数据库有表,HTTP 有 JSON,浏览器有表单------三套「形状」描述同一件事。

Go 内存里也需要一种类型把字段捆在一起,这个类型就是 struct(结构体)

学长可能问:

「为什么 User 要单独一个文件?json:"-" 是什么意思?Seat 里为什么用指针?」

本篇从零讲 struct、字段标签、常量、方法、指针接收者,并对照票务系统的核心模型。

读完后,打开 models/models.go 应能一行行对应到 MySQL 列和 API 字段


1. 什么是结构体?

把多个不同含义的字段捆成一种新类型:

复制代码
type User struct {
    ID       int64
    Username string
    Nickname string
    Role     int
}

可以读作:「用户有这些属性」

1.1 创建与访问

复制代码
u := User{Username: "user", Nickname: "电影爱好者", Role: 0}
fmt.Println(u.Username) // user

u.Nickname = "新昵称"   // 修改字段

对比其它复合类型:

类型 特点 例子
数组 / 切片 一堆同类元素 []int{1,2,3}
map 键值对,键类型统一 map[string]int
struct 固定字段名,类型可不同 User, Order

票务里「订单号 + 金额 + 状态」语义不同,用 struct 比用 []interface{} 清晰得多。

1.2 零值

未赋值的 struct 字段各有零值:0""nil(指针)、零时间等。

Scan 进 struct 前,常 u := &models.User{} 得到干净对象。


2. 本项目核心模型一览

models/models.go业务名词的代码词典。和数据库表的对应:

结构体 关键字段直觉
User users Role:0 顾客 / 1 管理员
MovieGroup movie_groups 电影分类
Movie movies RatingAvgGroupID 可空
Hall halls 影厅行列数
Schedule schedules 开场时间、票价
Seat seats Status、锁座字段
Order orders PayDeadlineSeatInfo
Comment comments ParentID 回复
Rating ratings 1--10 分

2.1 状态常量:拒绝魔法数字

复制代码
const (
    SeatFree   = 0
    SeatLocked = 1
    SeatSold   = 2

    OrderPending   = 0
    OrderPaid      = 1
    OrderCancelled = 2
    OrderTimeout   = 3
)

数据库 TINYINT status 和 Go 常量数值一致,dao 里写:

复制代码
if status == models.SeatSold {
    return nil, errors.New("座位已售出")
}

答辩加分:

「状态用命名常量,读代码的人不用猜 2 是已售还是已取消。」


3. 完整 User 结构体与标签

复制代码
type User struct {
    ID        int64     `json:"id"`
    Username  string    `json:"username"`
    Password  string    `json:"-"`
    Salt      string    `json:"-"`
    Nickname  string    `json:"nickname"`
    Phone     string    `json:"phone"`
    Role      int       `json:"role"`
    CreatedAt time.Time `json:"created_at"`
}

func (u *User) IsAdmin() bool { return u.Role == RoleAdmin }
字段 数据库 JSON API 说明
Password 不输出 json:"-" 绝不能下发前端
Salt 不输出 校验密码用,保密
Role TINYINT 数字 0/1 前端可据此跳转 admin

4. 字段标签 json:"xxx"

复制代码
Nickname string `json:"nickname"`
Password string `json:"-"`
GroupName string `json:"group_name,omitempty"`
标签 含义
json:"id" 序列化时键名为 id(蛇形或驼峰按约定)
json:"-" 忽略该字段
omitempty 零值时不输出(减少 JSON 体积)

utils.OK(w, user) 内部 json.NewEncoder(w).Encode(...) 会靠反射读标签。

拓展:

生态里还有 db:"column"(sqlx)、form:"username"(绑定表单)等。

本项目 SQL 手写 + Scan,没用 ORM 标签------字段顺序要和 SELECT 列顺序一致。


5. 指针字段与数据库 NULL

复制代码
type Seat struct {
    ID         int64      `json:"id"`
    ScheduleID int64      `json:"schedule_id"`
    RowNo      int        `json:"row_no"`
    ColNo      int        `json:"col_no"`
    Status     int        `json:"status"`
    LockUserID *int64     `json:"lock_user_id,omitempty"`
    LockUntil  *time.Time `json:"lock_until,omitempty"`
    OrderID    *int64     `json:"order_id,omitempty"`
}

对应 schema.sql

复制代码
lock_user_id BIGINT NULL,
lock_until   DATETIME NULL,
order_id     BIGINT NULL,
Go SQL
nil 指针 NULL
非 nil 有值

dao 扫描时常用中转:

复制代码
var lockUID sql.NullInt64
var lockUntil sql.NullTime
rows.Scan(..., &lockUID, &lockUntil, ...)
if lockUID.Valid {
    s.LockUserID = &lockUID.Int64
}

*Movie.GroupID int64 同理:电影可以暂时不属于任何分组(group_id NULL)。

为什么不用 int64 + 0 表示空?

0 可能是合法 id(虽本项目 id 从 1 自增);NULL 语义是「未知 / 不适用」,和 0 不同。


6. Order 与展示字段

复制代码
type Order struct {
    ID          int64      `json:"id"`
    OrderNo     string     `json:"order_no"`
    UserID      int64      `json:"user_id"`
    ScheduleID  int64      `json:"schedule_id"`
    MovieTitle  string     `json:"movie_title,omitempty"`  // 非表列,JOIN 填充
    HallName    string     `json:"hall_name,omitempty"`
    StartTime   time.Time  `json:"start_time,omitempty"`
    SeatInfo    string     `json:"seat_info"`
    Amount      float64    `json:"amount"`
    Status      int        `json:"status"`
    PayDeadline time.Time  `json:"pay_deadline"`
    CreatedAt   time.Time  `json:"created_at"`
    PaidAt      *time.Time `json:"paid_at,omitempty"`
}

两种字段来源:

  1. 表列直接映射OrderNo, Amount, Status
  2. 查询时 JOIN 带上MovieTitle, HallName------方便列表页一次展示,不必再查

SeatInfo 是字符串 "3排5座,3排6座"------冗余但利于订单列表快速显示;精确座位仍以 seats 表为准(思考题会问)。

6.1 方法 StatusText

复制代码
func (o *Order) StatusText() string {
    switch o.Status {
    case OrderPending:
        return "待支付"
    case OrderPaid:
        return "已支付"
    // ...
    }
}

模板注册函数:

复制代码
"statusText": func(status int) string {
    o := models.Order{Status: status}
    return o.StatusText()
},

HTML 里:{``{statusText .Status}}------行为挂在类型上,比散落 switch 好维护。


7. Comment 与「不在表里的字段」

复制代码
type Comment struct {
    ID        int64     `json:"id"`
    MovieID   int64     `json:"movie_id"`
    UserID    int64     `json:"user_id"`
    Nickname  string    `json:"nickname,omitempty"`  // JOIN users
    ParentID  *int64    `json:"parent_id,omitempty"`
    Content   string    `json:"content"`
    CreatedAt time.Time `json:"created_at"`
    Replies   []Comment `json:"replies,omitempty"`   // 内存里组树,无此列
}

Replies []Comment数据库没有 replies 列

dao 查出扁平列表后,在内存里按 ParentID 拼成树,再 JSON 给前端------struct 可以比表「宽」,但不能在 models 里写 SQL 拼树(那是 dao 的事)。


8. 值接收者 vs 指针接收者

复制代码
func (u *User) IsAdmin() bool { return u.Role == RoleAdmin }
func (o *Order) StatusText() string { ... }
值接收者 (u User) 指针接收者 (u *User)
修改原对象 改副本 可以改原对象
大 struct 拷贝成本高 更常见
nil 接收者 较少 需注意 nil pointer

本项目只读方法(IsAdminStatusText)用指针接收者,和 *User 调用方一致。

若方法需要改字段(如 func (u *User) SetNickname),必须用指针。

拓展:

Go 会自动对值取地址调指针方法(user.IsAdmin() 当 user 是值也能工作),但 nil 指针调用会 panic。


9. 匿名 struct:请求 DTO

Handler 里常见临时形状,不必进 models:

复制代码
var req struct {
    Username string `json:"username"`
    Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
    utils.Fail(w, "参数错误")
    return
}

小项目足够。企业可能拆 LoginRequestCreateOrderRequest 文件------models 保持「持久化实体」,DTO 保持「接口契约」。


10. 结构体与分层的关系

复制代码
MySQL 行  --Scan-->  models.Xxx  --JSON/模板-->  浏览器
         <--Exec/UPDATE--              <--解析--
              dao                      handlers

models 是中线:

  • dao 负责填满 struct(Scan、组装树)
  • handlers 负责把 struct 变成 HTML 或 JSON
  • 不要在 models 里写 SQL------保持「哑数据 + 少量纯方法」

违反例子(不要学):

复制代码
func (u *User) Save() error { return db.DB.Exec(...) } // 坏味道

11. 拓展:嵌入 struct(本项目几乎不用)

Go 支持嵌入做组合:

复制代码
type AdminUser struct {
    User
    Permissions []string
}

课程项目保持独立 struct + 显式字段 ,避免「伪继承」迷惑初学者。

学长若问继承:答「Go 倾向组合;我们用 Role 字段 + IsAdmin(),不用嵌入层次。」


12. 类型选择对照表(和 schema 一致)

业务 Go 类型 SQL 类型
id int64 BIGINT
用户名 string VARCHAR
密码哈希 string CHAR(32)
角色/状态 int TINYINT
票价/金额 float64 DECIMAL(Scan 转 float)
时间 time.Time DATETIME
可空外键 *int64 BIGINT NULL
长简介 string TEXT

金额用 float64 的局限:

DECIMAL 精确到分;float64 有误差。本项目金额量级课程够用;金融级要用 shopspring/decimal 或整数「分」存储。


13. 常见误区

  1. 「struct 字段顺序无所谓」

    Scan 顺序必须和 SELECT 列顺序一致。

  2. 「json 标签只影响输出不影响输入」

    Decode 同样看标签。

  3. 「Password 小写就安全」

    小写只是包内不可 export;JSON 仍可能泄漏,必须 json:"-"

  4. 「Replies 可以持久化到一列 JSON」

    可以但本项目用关系表 + 内存组树,更利于 SQL 查询单条评论。


14. 项目代码对照

你想理解 打开
全部模型定义 models/models.go
User Scan dao/user.goGetUserByUsername
Seat 可空字段 dao/schedule.goListSeats
Order JOIN 字段 dao/order.goscanOrder
JSON 输出 utils/json.go + 各 API*
模板里 IsAdmin handlers/app.go + templates/*.html
常量用在锁座 dao/order.gomodels.SeatSold

数据流口述(电影详情页):

复制代码
dao 查 Movie → models.Movie
handlers 塞进 pageData → Render 模板
用户评分 POST → 解析 JSON → models.Rating 字段 → dao.Insert

15. 小结口述稿

struct 把业务概念变成 Go 类型;models 包描述用户、电影、场次、座位、订单等。

状态和角色用命名常量,和数据库 TINYINT 数值一致。

json 标签控制 API 输出;密码和盐用 json:"-" 隐藏。

可空列用指针;JOIN 来的展示字段可以不在表里,Scan 时填入。

IsAdminStatusText 等方法让模板和业务更可读;models 不写 SQL。


16. 思考题

  1. 为什么 SeatInfo 用字符串存「3排5座」,同时还要有 seats 表?
  2. Comment.Replies []Comment 不是数据库列,为什么可以出现在 struct?谁负责填充?
  3. 若忘记 Passwordjson:"-",会有什么安全风险?
  4. Status 改成字符串 "pending"/"paid" 在 Go 和 MySQL 里各有什么利弊?
  5. Movie.GroupID*int64 而不是 int64,在 Scan NULL 时会发生什么区别?

17. 下一篇预告

《06 · MySQL 入门:库、表、行、主键》 ------四级概念、InnoDB、utf8mb4、schema 与 seed,以及和 db.Init DSN 的对应关系

相关推荐
William_cl1 小时前
WinForms EF Core 上下文频繁释放引发的异常排查与规范方案(MES 项目实录)
服务器·数据库·oracle
许彰午2 小时前
01-Oracle 19c静默安装全流程
数据库·oracle
许彰午2 小时前
02-Oracle连接池配置实战
数据库·oracle
Flandern11112 小时前
HTTPS(TLS 1.3)完整流程
数据库·网络协议·计算机网络·https
Bonnie_12152 小时前
Navicat Premium连接sql server数据库
数据库
其实防守也摸鱼2 小时前
前端应用的离线暂停更新策略:构建稳定可靠的渐进式部署方案
服务器·前端·数据库·学习·ai·命令行·linux系统
Leighteen3 小时前
ORDER BY + LIMIT 的坑:为什么加了 `LIMIT` 结果顺序还乱
数据库
一个天蝎座 白勺 程序猿3 小时前
复盘之我在金仓生产环境踩过的SQL暗坑,和攒了六年的编码规矩
数据库·sql·kingbasees
Mico183 小时前
MySQL 8.0.35 GTID + 延迟复制 + 过滤复制搭建
mysql·dba