Go 1.23 的 range over func 要求函数签名必须为 func(yield func(T) bool) 或其键值变体,否则编译报错;func() T 等非标准签名不被识别为迭代器。

Go 1.23 的 range over func 不是“给任意函数加个 for 就能遍历”,而是有硬性签名约束:必须是 func(yield func(T) bool) 或带键值的变体,否则编译直接报 cannot range over。
为什么写成 func() T 会编译失败
这是最常踩的坑。Go 编译器只认特定签名——它靠函数参数是否含 yield func(T) bool 来判断“这玩意儿是不是迭代器”,不看函数名、不看注释、不推理返回值类型。
-
func() int:错,编译器当普通函数,range拒绝接收 -
func() (int, bool):Go 1.22 试验期写法,1.23 已弃用,报错cannot use ... as iter.Seq[int] value -
func(yield func(int) bool):对,标准迭代器签名,range能识别 - 建议直接用
iter.Seq[int]类型别名,语义清晰,避免手写冗长签名
数据库分页迭代器怎么写才不查爆内存
核心原则:DB 查询必须放在 yield 回调内部,而不是函数开头一次性查完。否则每次 range 迭代都触发全量查询,性能直接崩。
- 错误示范:
rows := db.Query(...); for rows.Next() { ... }; return func(yield ...) { ... }—— 一调用就查完所有页 - 正确做法:把
offset和缓存切片作为闭包变量,在yield循环体内按需查下一页 - 查不到数据或出错时,直接
return,yield不会被调用,range自然终止,不用panic或显式 error 返回 - 示例中
db.Select(&ids, "... LIMIT ? OFFSET ?", limit, offset)必须在for循环内,且offset += limit在 yield 完本批后更新
多个迭代器组合时别嵌套 range
想对每个用户 ID 查详情?别写 for id := range UserIDs() { for detail := range GetUserDetail(id) { ... } } —— 这是典型的 N+1 查询,且 GetUserDetail(id) 返回的很可能不是 iter.Seq[T],直接报错。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
-
GetUserDetail(id)若返回func(yield func(User) bool),它才是合法迭代器;否则不能用于range - 组合推荐用
iter.Map:比如iter.Map(UserIDs(db, 100), func(id int64) User { return loadUserByID(id) }) - 手动包装也行:写一个新迭代器,闭包里持
UserIDs的结果,每次yield前查一次详情,控制并发和错误传播 - 嵌套
range还会丢失迭代控制权:外层 break 不影响内层,资源清理难做
调试时卡死或没输出?先检查 yield 返回值
yield 回调返回 false 表示“用户要求提前退出”,比如 for v := range it { if v > 100 { break } } 编译后等价于 if !yield(v) { return }。如果忘了在 yield 后判断返回值,循环就永远不退出。
- 常见漏写:
yield(v)而不是if !yield(v) { return } - yield 内部若 panic,整个
range中断,但不会自动 recover;业务逻辑需自行包裹 - yield 外部的耗时操作(如日志、网络请求)会阻塞整个迭代,应尽量轻量
- 测试时可用小 limit + 手动 break 验证退出逻辑是否生效
真正难的不是写出符合签名的函数,而是把状态管理、错误边界、资源释放全塞进闭包里——DB 连接、文件句柄、goroutine 生命周期,都得自己扛,iter.Seq 不帮你管这些。

















