终端分页需用游标式拉取而非Offset/Limit,配合keyboard库监听方向键,分页结构体应含LastID、Limit和Direction字段。

gorm 本身不提供终端分页能力,它只负责数据库查询;所谓“终端分页”,实际是 Go 程序在命令行中对查询结果做分批输出 + 用户交互控制,和 Web 分页的 Offset/Limit 逻辑不同——你得自己接管滚动、清屏、按键响应这些事。
终端分页 ≠ 数据库分页,别混用 Offset/Limit
很多人一看到“分页”就本能写 db.Offset().Limit(),但在 CLI 场景下这会直接导致:
- 用户翻到第 2 页时,程序重新查一遍数据库,跳过前 N 条——但中间可能有新数据插入/删除,结果错乱
- 无法支持「上一页」「搜索跳转」「动态刷新」等终端常见操作
- 一次性查全量再本地分页?内存爆炸,尤其面对几万行日志或审计记录
WHERE id > ? ORDER BY id LIMIT N),每次只查增量,配合终端状态机控制显示。
用 gommon/term 或 github.com/eiannone/keyboard 做按键监听
终端分页的核心交互是捕获 ↑、↓、q、space 等键,而不是等用户输完回车。标准 fmt.Scanln 不行,必须用底层键盘监听库:
-
github.com/eiannone/keyboard:轻量、无依赖、支持 Linux/macOS/Windows,初始化后调keyboard.GetSingleKey()即可阻塞读单键 -
golang.org/x/term(Go 1.22+):自带,但只支持读整行,需配合term.MakeRaw才能捕获方向键(较麻烦) - 避免用
bufio.NewReader(os.Stdin)—— 它默认行缓冲,方向键会变成乱码字符串如^[[A
err := keyboard.Open()
if err != nil {
log.Fatal(err)
}
defer keyboard.Close()
<p>for {
char, key, err := keyboard.GetKey()
if err != nil {
log.Fatal(err)
}
switch key {
case keyboard.KeyArrowUp:
// 上一页
case keyboard.KeyArrowDown:
// 下一页
case keyboard.KeyEsc, keyboard.KeyCtrlC:
return
}
}分页结构体要带 cursor 字段,不是 page_num
CLI 分页不维护“当前第几页”,而是维护“最后一条显示记录的 ID 或时间戳”,用于下一次查询:
- 定义结构体时,去掉
Page和PageSize,换成LastID uint、Limit int、Direction string("next"/"prev") - 首次加载:查
ORDER BY id ASC LIMIT 20,记下users[len(users)-1].ID作为LastID - 按 ↓:查
WHERE id > LastID ORDER BY id ASC LIMIT 20 - 按 ↑:查
WHERE id ,再反转切片 - 必须给
id加索引,否则游标查询退化成全表扫描
清屏、光标定位、状态栏要用 ANSI Escape Code
纯文本终端没 UI 框架,靠 ANSI 控制序列实现“伪分页”效果:
- 清屏:
fmt.Print("\033[2J") - 光标归位:
fmt.Print("\033[H") - 隐藏光标:
fmt.Print("\033[?25l"),退出前记得"\033[?25h" - 状态栏固定在底部一行:查完数据后,用
\033[s保存光标位置,\033[u恢复,再\033[?25l隐藏光标
os/exec.Command("clear") —— Windows 不认,且有闪屏。ANSI 是跨平台最稳的方案。
终端分页真正的难点不在 GORM 查询,而在状态同步:你要确保「用户看到的最后一条」和「下一次查询的游标值」严格一致,中间不能被并发写入干扰。哪怕只差 1 行,滚动就会跳变或卡死。


















