惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
Netflix TechBlog - Medium
I
InfoQ
Engineering at Meta
Engineering at Meta
Jina AI
Jina AI
Recent Announcements
Recent Announcements
T
The Blog of Author Tim Ferriss
P
Proofpoint News Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
D
Docker
Microsoft Security Blog
Microsoft Security Blog
宝玉的分享
宝玉的分享
Last Week in AI
Last Week in AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
博客园 - Franky
博客园 - 聂微东
Microsoft Azure Blog
Microsoft Azure Blog
博客园 - 叶小钗
酷 壳 – CoolShell
酷 壳 – CoolShell
B
Blog RSS Feed
WordPress大学
WordPress大学
MyScale Blog
MyScale Blog
月光博客
月光博客
罗磊的独立博客

鸟窝

鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 鸟窝 LLM 究竟是如何工作的? Go 实验特性详解 amd64 微架构级别对 Go 程序性能提升多少?
鸟窝
by smallnest · 2026-06-25 · via 鸟窝

等了十年的 Go 链式管道,终于来了:seq 让你像写 Scala 一样写 Go

seq 库的一行代码,从左读到右。写过 lo.Map(lo.Filter(...)) 的人,大概会愣一下。

这个库的开发我昨天晚上在微信上了做了直播,展示我如何使用Loop Engineering 从 0 构建出来。使用的是火山引擎的coding plan, GLM-5.2模型,花了 2 小时,耗费Token 7.23M。你可以查看直播回放:

你也可以访问这个库的项目地址: https://github.com/smallnest/seq, tasks目录中有需求文档和设计文档。

先看一个让你眼花的写法

要做的事很简单:把切片里的偶数挑出来,平方,求和。

用过 Go 里最火的工具库 samber/lo 的话,你多半会写成这样:

1
2
3
4
5
6
sum := lo.Sum(
lo.Map(
lo.Filter([]int{1, 2, 3, 4, 5, 6}, func(x int, _ int) bool { return x%2 == 0 }),
func(x int, _ int) int { return x * x },
),
)

发现问题了吗?

数据是从里往外流的,你的眼睛也得从里往外读。就像层层包裹的洋葱,让你的眼睛流泪

逻辑明明是「过滤 → 映射 → 求和」,写出来却是「求和(映射(过滤(...)))」,阅读顺序和数据流向正好相反。括号一层套一层,改一行还得数括号配不配对。我以前一直以为这是 lo 作者图省事,后来才知道不是。

现在看看用 seq 写同样的逻辑:

1
2
3
sum := seq.From([]int{1, 2, 3, 4, 5, 6}).
Filter(func(x int) bool { return x%2 == 0 }).
SumBy(func(x int) int { return x * x })

从左到右,数据怎么流,代码就怎么读。seq 是一个基于 Go 标准库 iter.Seq 的链式惰性集合库。


为什么 Go 等了十年才能这么写?

很多人以为 lo 的「从内往外」是作者偷懒,其实不是。这是 Go 泛型语法卡住的结果。

Go 1.27 之前,方法不能声明自己的类型参数。也就是说,一个想把 Seq[int] 变成 Seq[string].Map() 方法,在语法层面根本写不出来:

1
2
3


func (s Seq[T]) Map[U any](f func(T) U) Seq[U]

方法带不了类型参数,lo 这类库就只能把所有操作做成顶层自由函数,于是有了那串嵌套括号。不是谁懒,是当时只有这一条路。

泛型方法提案 golang/go#77273 在 Go 1.27 里落地,这条限制才算解除。链式、惰性、还能被编辑器自动补全的管道,到这时候才写得出来。seq 就是冲着这个来的。

一句大实话:seq 需要 Go 1.27,目前还是 go1.27rc1。提案官方已经接受并实现了,只差正式版发布。把这个库的Go版本设定在 1.27,是为这条方法链付的过路费。


它到底有多省心?来看一组实战

下面这些例子我都从 seq 的测试用例里抠出来的,每行都是真跑得通的。

例 1:数据分析三连——分组、去重、统计

把一串数字按奇偶分组:

1
2
3
4
5
6
7
8
groups := seq.From([]int{1, 2, 3, 4, 5, 6}).
GroupBy(func(x int) string {
if x%2 == 0 {
return "even"
}
return "odd"
})

去重之后再求和:

1
2
3
4
total := seq.Numbers(seq.From([]int{1, 2, 2, 3, 3, 3})).
Distinct().
Sum()

例 2:惰性求值,无限序列也能玩

seq 是惰性的。不调用终结操作,整条管道一行都不跑。所以你能定义一个无限序列,再只取你要的那几个:

1
2
3
4
5
6


powers := seq.Iterate(1, func(x int) int { return x * 2 }).
Take(5).
Collect()

惰性带来的真正好处是短路。FindAnyAll 拿到答案就停,后面的元素根本不碰。百万级数据里找第一个满足条件的,不会陪你白跑到底。

例 3:前缀和?一个 Scan 搞定

1
2
3
4
5

prefix := seq.From([]int{1, 2, 3, 4}).
Scan(0, func(acc, x int) int { return acc + x }).
Collect()

例 4:滑动窗口,时序数据的最爱

做监控、做时序分析的应该会喜欢,滑动窗口是内置的:

1
2
3

wins := seq.From([]int{1, 2, 3, 4, 5}).Window(3, 1)

定长分块同样开箱即用:

1
2
chunks := seq.From([]int{1, 2, 3, 4, 5}).Chunk(2)

例 5:两个序列配对——Zip 的优雅

把两个序列「拉链」拼起来,做成 map:

1
2
3
4
5
m := seq.ZipMap(
seq.From([]string{"a", "b", "c"}),
seq.From([]int{1, 2, 3}),
)

或者配对后用自定义函数合并,还能顺手改变类型:

1
2
3
4
5
6
merged := seq.ZipWith(
seq.From([]int{1, 2}),
seq.From([]string{"x", "y"}),
func(a int, b string) string { return b + string(rune('0'+a)) },
).Collect()

短的那个序列耗尽就停,完全不用手动判长度。


藏在优雅背后的「神级设计」

如果你只当它是个链式语法糖,那有点可惜。seq 真正有意思的地方,是它把 Go 泛型的那些限制,反过来当成了设计的骨架。

零成本类型转换

seq 的核心类型不是 struct 包装,而是标准库迭代器的定义类型:

1
2
type Seq[T any]     iter.Seq[T]      
type Seq2[K, V any] iter.Seq2[K, V]

意思是任何 iter.Seq[T] 都能零成本当作 seq.Seq[T] 使用,反过来也行。结果直接喂给 slices.Collectmaps.Keys 都没问题,没有任何转换开销。

一句话就能判断:它该是方法还是函数

先说清楚一个词,下面会反复用到:自由泛型函数

「自由」是相对「方法」说的,指不绑在某个接收者上的顶层函数;「泛型」指它带自己的类型参数和约束。两者合起来,就是 func Distinct[T comparable](s Seq[T]) Seq[T] 这种写法——它在包级别,自己声明 [T comparable],把序列当普通参数收进来,而不是写成 s.Distinct() 挂在 Seq 上。Go 的 slicesmaps 标准库包里那些 slices.Sort[S ...]maps.Keys[...] 全是这个形态。

为什么有些操作只能这么写,看下面这条规则就懂了。

因为 Seq[T any]T 固定成了 any,方法没办法反过来给 T 加约束。于是我干脆立了一条判断标准:

问一句:这个操作约束了 T 本身吗?约束了,就得是自由泛型函数;没约束,才能做成方法。

  • 需要 comparablecmp.Ordered 的操作(如 DistinctMaxSum)→ 自由泛型函数
  • 只用方法自带类型参数的操作(如 GroupBy[K]Map[U])→ 方法

整套 API 谁是方法、谁是函数,全按这一条推出来,没有例外。不用记,照着问一遍就知道答案,比拍脑袋舒服多了。

但是这样不就有回退到lo这个泛型库的痛点了么?

最妙的一招:用「子类型」把链式找回来,解决痛点

这条规则有个副作用:SumDistinct 变成自由泛型函数之后,管道又退回从内往外读了(Sum(Distinct(...)))。我的解法是引入三个约束型子类型,把约束提前钉在类型上:

1
2
3
4

sum := seq.Numbers(seq.From([]int{1, 2, 2, 3})).
Distinct().
Sum()

Numbers() 一进门,后面的 .Distinct().Sum() 又变回方法了。Numeric ⊂ Ordered ⊂ comparable 这层关系还能用 .Ordered().Comparable() 往下降,链一次都不断。绕是绕了点,但在 Go 现有的类型系统里,这大概是能想到的最干净的招了。


它老老实实告诉你,哪些事它不做

现在的开源项目动不动就「全能」「最强」,seq 的文档反而把不做的事一条条列了出来:

  • 不做错误处理链,在惰性迭代器上没找到优雅解,留作以后的独立提案
  • 不做并行执行,这版只有顺序惰性管道
  • 不提供「就地改原数据」的操作,seq 全程只读、不动你的原切片,每一步都产出新序列
  • 不做任意深度展平,Go 类型系统表达不了,只给一层 Flatten
  • 元组到 Tuple4 封顶,再多请自己定义 struct
  • 不是所有操作都惰性。像排序、逆序、取末尾 n 个、滑动窗口这类要回看整条序列才能出结果的操作,会先把源收集成 slice 再处理,做不到边读边算。库里专门用一个 intermediate_materializing.go 文件把它们集中放着,注释也标了「内部物化」——该惰性的惰性,该物化的老实物化,不糊弄

它甚至把 gofmtgo1.27rc1 上会对泛型方法签名误报这件事也记下来了,说明这是 RC 阶段工具链的 bug,编译和测试都过,正式版应该会消失。

一个肯告诉你边界在哪、连工具链 bug 都标注清楚的库,用起来心里有底。


上手只需一行

1
go get github.com/smallnest/seq

记得准备 Go 1.27 工具链,当前是 go1.27rc1

趁手的操作我按用途分了组,扫一眼就知道有没有你要的。

造序列
From(切片)、Of(可变参数)、EmptyRange / RangeStep(整数区间)、Repeat / RepeatInf(重复)、Generate / Iterate(无限生成)、FromChannel(channel)、FromMap(map → 键值序列)

变换 / 过滤(惰性,边读边算)
Map(换类型)、FlatMapFilterMap(过滤+变换合一)、Filter / RejectTake / DropTakeWhile / DropWhileScan(前缀累积)、Peek(旁路调试)、Intersperse(插分隔)、Concat(尾部追加)、DistinctBy(按 key 去重)

取子集 / 排序(需回看,内部物化)
TakeRight / DropRightSlice(区间)、Init / TailSortBy / ReverseChunk(定长分块)、Window(滑动窗口)、Enumerate(配索引)

聚合 / 归约 / 统计
Collect(收成 slice)、Fold / ReduceCount / CountBySumBy / MeanByMaxBy / MinBy / MaxByKey / MinByKeyJoin(拼字符串)、ForEach / ForEachIndexed

查找 / 判断(短路)
Find / FindLast / FindIndex / FindLastIndexAny / All / NoneFirst / Last / NthIsEmpty

分组 / 分区
GroupByKeyBy(建索引)、GroupCount(分组计数)、Partition(按谓词二分)、Span(首个不满足处切)

去重 / 集合运算(约束 comparable,自由泛型函数)
DistinctContains / IndexOf / LastIndexOfCountValues / ToSetUnion / Intersect / Difference / SymmetricDifferenceCompact(去零值)、Without(排除指定值)、Equal

数值 / 排序(约束 Ordered / Numeric,自由泛型函数)
Max / MinSum / Product / MeanSort

想链式调用上面这些约束操作?走子类型入口
Comparable / Ordered / Numbers 进入,之后 .Distinct().Max().Sum() 都变回方法,再用 .Ordered() / .Comparable() / .Seq() 降级

多序列组合
Zip / Zip3 / Zip4ZipWith(配对并合并)、ZipMap(配成 map)、Unzip(拆分)、Flatten(展平一层)、Concat / Interleave(交错)

键值序列 Seq2
MapValues / MapKeys / MapFilterKeys / ValuesToMap / CollectPairs / EntriesAssociate(Seq[T]Seq2)

完整签名、每条一句语义,以及设计文档都在仓库里:


写在最后

lo 是个好库,在语言能力不够的那些年,它已经做到了能做的极限。「从内往外」是它绕不过去的坎,不怪它。

seq 站在 Go 1.27 泛型方法这块新地基上,重新试了一次:Go 的集合操作能写得多顺。第一次写完 From(...).Filter(...).Map(...).Sum() 那一行,我盯着屏幕想了两秒——这真的是 Go语言吗?

觉得有意思的话,去仓库给作者点个 Star。