go1.23 新的正式特性 range over func 看了下没怎么看懂。。。
for range 返回一个函数,函数里面还是 for range 或者就 for 循环:
func Backward[E any](s []E) func(func(int, E) bool) {
return func(yield func(int, E) bool) {
for i := len(s) - 1; i >= 0; i-- {
if !yield(i, s[i]) {
return
}
}
return
}
}
func main() {
sl := []string{"hello", "world", "golang"}
for i, s := range Backward(sl) {
fmt.Printf("%d : %s\n", i, s)
}
}
Backward
这个函数这形式看起来还真有点复杂,不好理解。。。。可能例子不是相应场景下需要的,看半天没理解。
感觉一般都用不上。
1
BBCCBB 121 天前
python 里的生成器? 不过 python 不需要 fn 里再套一个 fn.
|
3
Leviathann 121 天前 11
这语法笑死
go 在探索下限这点上上限很高 |
4
lysShub 121 天前
可不要要写这种垃圾代码,这特性怎么通过的的,太傻逼了
|
5
sagaxu 121 天前 2
大道至简没能一以贯之啊,特性越加越多,以后不敢说看 Go 代码时语言层面的心智负担低了
|
6
dyllen OP 迭代器是一个函数,它将一个序列中的连续元素传递给一个回调函数,通常称为"yield"。迭代器函数会在序列结束或者 yield 回调函数返回 false(表示提前停止迭代)时停止。
|
7
wunonglin 121 天前 1
就是迭代器。和 js 里的 yield 差不多,但是语法真的难看
|
8
ZxykM 121 天前 4
我不敢相信这特性怎么通过的?写法也太丑了
|
10
dyllen OP @ZxykM 这个就是那个 rsc 自己提的提案,能不通过吗? https://github.com/golang/go/issues/61405
|
11
body007 121 天前
我喜欢 go ,但我讨厌这个特性。主要场景也不是你上面那种封装完立即 range 遍历,而是生成一个迭代器,放到代码其他位置去遍历,这种代码容易写出 bug ,还不好定位。你可以看下 iter 包,里面还有 iter.Pull 这种方法,就是把迭代器变成 next() 和 stop() 两个函数,意思是在其他代码位置一个个读取迭代器元素,最好在最上面写 defer stop()。go 容易阅读是很多使用者喜欢的原因,难以想象到处充斥这种迭代器的代码的阅读体验。生成迭代器和使用迭代器的代码位置可能相差十万八千里,调试打断点估计都不好找位置🤣
|
12
zhuangzhuang1988 121 天前
JS 也会这样写
Quill.js 编辑器的代码 https://github.com/slab/parchment/blob/main/src/collection/linked-list.ts#L117 |
13
PTLin 121 天前
看来不止我一个觉得丑,之前在一个群喷这个语法,一个大佬搬出了一堆文章和 x 上的博客证明这个 push 的迭代器多么先进。
|
14
pkoukk 121 天前
这迭代器太丑了,没眼看
|
15
assassing 121 天前
函数签名 3 个 func 看傻了,要是 2 个还能接受
|
16
ninjashixuan 121 天前
真真的太丑了,之前提案阶段就一堆人吐槽了。
|
17
dbpe 121 天前
问题来了...这 shit 的提案..怎么过的?
|
19
Donahue 121 天前
喜欢 go 语言,交叉编译二进制运行很好。但是语法真的丑。
|
20
Jinnrry 121 天前
我以前都会吹 go 大道至简,代码易读。但是看到这玩意,我以后不吹了
|
21
assassing 121 天前
按照写 Python 的经验,迭代器没有非用不可的场景,虽然它很简单。
Go 语言中宁可牺牲简单性,也不愿意引入新关键字,我是理解不了的。去原帖看了下,作者认为这样实现迭代器工作量比较小... |
22
hxtheone 121 天前 via iPhone
个人感觉 Go 语言一直对库的调用者比编写者更友好, 但是这个特性在写库的时候简直是太磨人了, 配上泛型和又臭又长的匿名函数简直是没眼看
|
23
kiripeng 121 天前 1
这特性就是一坨,我都懒得升级
|
25
KaynW 121 天前
确实丑的可以,层级这么多,}}}}看着丑的很
|
28
cmdOptionKana 121 天前
挺好的啊,Go 的这个函数就像一个外皮透明的动物,让你一眼看见里面的食道和胃具体是怎样消化食物的,非常直白。写 Go 真的让人头脑清晰,而且过一段时间回头看也条理清晰,比一堆语法糖好多了。
|
29
stevenshuang 121 天前
@dyllen yield 没有增加,yield 就是那个函数的命名。https://go.dev/ref/spec#Keywords
|
30
leonshaw 121 天前
@dyllen yield 是参数名。这东西看起来就是 for-range 简单地帮你生成了一个回调函数。
it := Backward(sl) cb := func(i int, s string) bool { fmt.Printf("%d : %s\n", i, s) return true } it(cb) |
32
assassing 121 天前
@cmdOptionKana 噗,你这比喻很到位,大中午的不想吃饭了都
@dyllen 不是关键字,随便叫什么都成,关键字你可以查看这里: https://github.com/golang/go/blob/release-branch.go1.23/src/builtin/builtin.go |
33
yqf0215 121 天前
@PTLin 这个特性,也被大佬抨得一塌糊涂,太复杂了。
这个特性除了炫耀自己编程厉害,并没有什么特殊的好处,其功能用其他方式同样能完成,还容易读。说与其弄这些,不如提升性能。 |
34
MoYi123 121 天前
Backward 里的 func 套 func 是函数式编程里的常见的惰性求值的写法. 不觉得有什么问题.
|
35
ChrisFreeMan 121 天前
我觉得还好,多看几眼觉得很合理。
|
36
sophos 121 天前
我感觉这个特性是很有用的,主要是用在提供常用操作的组件中,调用方的代码还是比较直观的
至于写组件的时候,可以直接用 iter.Seq 或 iter.Seq2 ,别直接用匿名函数就行,还好吧 |
37
dacapoday 121 天前
高阶函数而已,比较取巧,但合理。
|
38
Nazz 121 天前 1
笑了, 这么多人又菜又爱喷
经常写数据结构的人不会对这个接口感到陌生: ```go type Ranger[K comparable, V any] interface { Range(k K, v V) bool } ``` 有了 range over func 语法糖, 可以非常方便地返回上层函数 |
39
Trim21 121 天前 via Android
golang 为了不加这个 yield 关键词真是煞费苦心…
这个新功能还有个离谱的地方是标准库里 iter.Pull 的实现方法,直接魔改运行时… |
40
dule 120 天前
以前看过 go 语言的代码就觉得这玩意语法是真丑陋啊,太不直观了
|
41
28Sv0ngQfIE7Yloe 120 天前
有点炫技啊, 理解起来有一定的心智负担
|
42
zhwq 120 天前
看来又要换坑了,就像早年 PHP 支持 namaspace ,之后就是一日不一日。
语言有没有变的更高级我不知道,我只知道,用的越来越少了 |
43
cheng6563 120 天前
幸好泛型没用()
|
44
cmdOptionKana 120 天前
@Morii 这是一板一眼最“老实”的笨办法的写法,与炫技毫无关系。
|
45
me1onsoda 120 天前
go 的源码不是本来就很难看吗。。。
|
46
dyllen OP @cmdOptionKana 菜鸡还真看不出来,看着就挺复杂的。
|
47
LieEar 120 天前
func Backward[E any](s []E) func(func(int, E) bool) {
return func(yield func(int, E) bool) { 眼前一黑 |
48
cmdOptionKana 120 天前
@dyllen 什么菜不菜的,不习惯而已。
就像 react 、vue 刚出来的时候,一堆用习惯了 jQuery 的人说 react 复杂,等 react 流行后,又反过来一堆人说 jquery 啰嗦、乱。其实就是看你习惯哪个。 |
49
bv 120 天前
@LieEar 能理解 Java/JavaScript 中的 stream.filter(v -> v !=10 ) 应该对这种形式好理解了。(只是这种高阶函数的形式互通,不是说 go iter 和他们的 stream 功能类似)
|
50
CHTuring 120 天前
其实看起来还行吧,就是返回个 yield 的迭代器,不过一般也用不上。
|
51
Rehtt 120 天前
emmm 感觉也不是很难理解啊
|
52
Nazz 120 天前
@Morii 这样好理解吗
package main type Vector[T any] []T func (c Vector[T]) Range(f func(i int, v T) bool) { for i := 0; i < len(c); i++ { f(i, c[i]) } } func main() { var vec = Vector[int]{1, 3, 5, 7, 9} for i, v := range vec.Range { println(i, v) } } |
53
Nazz 120 天前
package main
type Vector[T any] []T func (c Vector[T]) Range(f func(i int, v T) bool) { for i := 0; i < len(c); i++ { if !f(i, c[i]) { return } } } func main() { var vec = Vector[int]{1, 3, 5, 7, 9} for i, v := range vec.Range { println(i, v) } } |
54
Elaina 120 天前 3
Golang 自从出了泛型之后就越来越依托答辩了,之前嘴硬说为了简洁,把代码搞得又臭又长,现在又开始整活
|
56
cmdOptionKana 120 天前
@Elaina 我怀疑你脱离事实硬黑。对泛型有需求的代码,现在用泛型,代码比以前用 interface 模拟泛型的代码更长?
|
57
Elaina 120 天前
@cmdOptionKana 你没看到我说的是之前么,我说之前为了简洁搞得代码又臭又长
|
58
cmdOptionKana 120 天前
|
59
Elaina 120 天前 1
@cmdOptionKana 这泛型的方括号你就说是不是依托答辩,而且最开始本身宣传的就是不过度设计,极简,现在不少新特性都跟之前的理念相悖
|
60
cmdOptionKana 120 天前 1
@Elaina 目标是尽量简单,现在虽然没有做到极致完美,但总的来说确实已经非常克制了,也没增加多少复杂的东西。
泛型也是社区每年调查,每年很多人提需求才增加的,也就是说,不加泛型一堆人骂,加了泛型也有人骂,神来了也没有更好的处理方法啊。你是希望不加泛型还是你有更好的泛型方案? |
61
Elaina 120 天前 1
@cmdOptionKana 我没说不能加泛型啊,写 parser 别偷懒用方括号啊
|
62
EAFA0 120 天前
``` golang
|
63
EAFA0 120 天前
研究了一下... 你把这块代码这样看:
``` golang func Backward[E any](s []E) func(func(int, E) bool) { return func(yield func(int, E) bool) { for i := len(s) - 1; i >= 0; i-- { if !yield(i, s[i]) { return } } return } } func main() { sl := []string{"hello", "world", "golang"} iter := Backward(sl) iter(func(index, value) bool { fmt.Printf("%d : %s\n", i, s) return true }) } ``` 是等效的, 不知道这个能不能帮助你理解, 实际上这个新特性看起来只是加了个编译时替换 (可能描述的不准确) |
64
cmdOptionKana 120 天前 1
|
65
laikick 120 天前
@cmdOptionKana 不管怎么说. range over func 确实感觉破坏了 golang 代码易读这个优点
|
66
Elaina 120 天前 1
@cmdOptionKana 这几点不冲突啊,我不喜欢方括号是因为可读性差啊,parser 又不是不能处理尖括号,所以我说它是依托答辩啊,你要搞就好好搞呗,搞了但又搞成依托答辩,以及不简洁不代表代码会又臭又长,简洁了不代表可读性就好
|
67
QXDM 120 天前 1
@cmdOptionKana #28 能清晰的看到依托 shit 的组成与产出
|
68
cmdOptionKana 120 天前
@laikick 这是个很普通的特性,也很直白,觉得不易读大概率是之前类似的写法看得比较少,不习惯而已。
|
69
cmdOptionKana 120 天前
|
70
cmdOptionKana 120 天前
@Elaina 你这一段很混乱,我理不清其中的逻辑,因此无法反驳。
|
71
vfs 120 天前
@cmdOptionKana 如果一开始 golang 就支持了泛型, 我可能根本不会去学它, 因为我完全可以去学习更牛逼的 Java 。 我之所以喜欢他, 是因为它有着像 c 一样简单的语法。 如今 golang 只是在慢慢的编程另外一个 java 而已, 而这个世界根本不需要另外一个 java 语言。这也就是为为什么对它支持泛型不太看好。今天能支持泛型, 明天就能支持异常。
|
72
cmdOptionKana 120 天前
@vfs golang 从来没有承诺不添加泛型。事实上在没有泛型的时候,确实有很多人不得不用 interface 来模拟泛型,这个需求是真实存在的。添加泛型后,社区文化也是尽量控制泛型的使用范围,能不用就不用,只在有明显帮助的时候才用。就目前来看还是比较乐观的,泛型不会蔓延得太厉害。
|
73
0x67cq 120 天前
@EAFA0 嗯,看你这个例子就感觉比较好理解,最上面那个很难理解的点在于 range 本身是一个语法糖。我没办法直接看出来,它构造了一个什么 yield 函数作为参数传入 Backword 执行后返回的结果函数用于执行。你这里把 `for k,v := range iter` 改写成 `iter ( func()bool{} return true)`这种方式就很明确。相当于把语法糖解开了。总的来说我不喜欢这个语法糖。
|
74
0x67cq 120 天前
@EAFA0 我读上面最开始卡到的地方其实就是,for k,v := range iter{fmt.Printf...} 这里,我无法理解 生成的 iter 是怎么得到 yield 这个参数函数的。简单来说就是,TMd 的哪来的 yield 。
|
75
victorc 120 天前 1
谁 tm 提的,这人是个纯 SB
c++被弄成现在这个鸟样就是有这群鸟人把持标准委员会 |
76
kk2syc 120 天前
大伙要像果粉学习,自适应很重要!丑归丑,能用好用性能好就行了……
|
77
kingcanfish 120 天前
@0x67cq #74 而且 yield 返回值是个 bool 值 tmd 拿来的布尔值 tmd 到底怎么跳出迭代 晕了
|
78
vfs 120 天前
@cmdOptionKana "很多人不得不用 interface 来模拟泛型": 其实我并不觉得这是什么问题, 就像在 c 里面用 void* 来指向任何地址的做法一样。
|
79
EAFA0 120 天前
@0x67cq 我一开始也很奇怪, 搓了几段代码后发现这个 for range 跟 py 那种 yield 语法可以说是没有一点关联性... 这个语法糖远不如直接调用直观, 有一点关联性的是 iter 包下面的 Pull / Pull2 方法, 这两个方法的效果才跟其他语言的迭代器有点类似
|
80
MoYi123 120 天前
@Nazz 如果这个数据结构基于硬盘, 提供的接口是 async 的, 这种传入回调函数的方式可以大幅减少 await 的次数, 性能会比 await iter.next() 好很多.
虽然 go 里用不到这个. |
81
sagaxu 120 天前
@vfs 之前说好的 Go 不需要泛型,结果加了,加了迭代器之后,也许异常也会加进去,defer+异常+panic 组合下的坑够博主们写 100 篇了。这次加的迭代器,易读性比 Java 和 Python 都低不少,参数是函数还返回函数的高阶函数,倒不是说理解不了,只不过读到那里思路卡顿一下才过得去。
“不直观”程度跟经典的 signal 函数定义有的一拼 void ( *signal(int signum, void (*handler)(int)) ) (int); |
82
voidmnwzp 120 天前 1
这种傻逼代码都能作为官方示例?看来 golang 药丸
|
84
bugfan 120 天前
|
85
label 120 天前 4
|
86
xz410236056 120 天前
这不就是返回一个闭包(高阶函数)其他语言叫生成器、装饰器。这不是很正常的语法吗?
|
87
xz410236056 120 天前
@assassing #21 学两天 swift 就老实了,关键字多到可以出一本书了,完全就是两个极端
|
89
assassing 120 天前
@xz410236056 #87 那肯定 Go 更好。
只不过这个迭代器实现太难用,支持了和没支持一样。像同时另一个 range 改进:for i := range n 替代 for i := 0; i < n; i++,我就觉得挺好,当然肯定也有人喷糖撒过头了。 |
90
lxdlam 120 天前 7
首先是一个时间线:
- 一个 standard 的 iterator 是一个社区一直在讨论跟推进的,正式的 proposal 在 2022 年就已经成型并进行了广泛讨论: https://github.com/golang/go/discussions/54245 。 - rsc 选择了使用 push-func 来实现的讨论,针对于他对*既有* stdlib 的代码的观察 https://github.com/golang/go/discussions/56413 。 - 关于最终加入 spec 的讨论,从 go1.22 到 1.23 release ,数百条关于实现方式、问题、语法选择的讨论都可以在 https://github.com/golang/go/issues/61405 看到。 在这条帖子的讨论中,mix 了非常多的对于不同方向的讨论: - 该不该加入 Iterator ?这个问题相信毋庸置疑,一个标准化的 iterator 定义能够极大方便用户和库开发者。举个非常简单的例子,我们可以将 `sql.Rows` 传入一个 `slices.Chunk` 函数中,使得只需要 ``` for batch := range slices.Chunk(sql.Rows() ,6) { // do something with current batch } ```` 就能非常简单将 sql 返回的结果分批处理。类似的场景在各种延迟计算、数据获取、基于数据流向的调度中非常常见。 - 该不该使用 push-func 实现? push-func 跟 pull-func 虽然在表达能力上是一致的,但是 rsc 认为: > Although push and pull functions are duals, they have important differences. Push functions are easier to write and somewhat more powerful to invoke, because they can store state on the stack and can automatically clean up when the traversal is over. That cleanup is made explicit by the stop callback when converting to the pull form. push-func 更容易转成 pull-func ,且更容易实现。当然,直观程度上,push-func 是弱于 pull-func 的,这导致了这次语法看起来更加奇怪。(来自于 Java 的例子是,stdlib 对 iterator 的实现是 pull based 的: https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/util/Iterator.html ) - 该不该使用 rangefunc 实现 iterator ?这个是社区最核心的讨论点之一。援引一个老哥的文章: https://www.gingerbill.org/article/2024/06/17/go-iterator-design/,社区核心始终认为,这种函数破坏了既有用户群体对这个语言的 philosophy (宗旨?方法论?不知道怎么去定义这个词比较好)的认知,一部分群体认为 Go 就是个基于过程的语言,不应该如此“函数式”;同样也有人认为,这种基于完全隐式的控制流(依赖 function stack frame 控制生命周期,range Func 完全依靠 compiler 重写)是引入新的 Bug 的万恶之源。 就我个人来说,我并不赞成使用这种方式实现,如果有一个编译器可以认知的新的 iterator 对象,那无论从实现还是使用角度都会简单很多,而且就 go 整个语言的发展历史来看,为了一些之前没考虑到的事情,动态开洞擦屁股这种事,也不是发生一次两次了。但是这条回复的目的还是给大家一个清晰的认知,也建议讨论设计问题前,先搞清楚前因后果时间脉络,减少鸡同鸭讲。 |
91
povsister 120 天前
#90
概括的非常好了,虽然你可以说 go 是 googelang ,因为他确实也塞了很多私货,甚至按照 k8s 的需求定制特性也不是没有先例。 但观察 go 社区的讨论,你会发现没人像 V2EX 发言一样:随性发表观点看法而不带任何论据。 甚至 rsc 本人也会对你提出的疑问认真回复(有幸被回复过,只能说大佬看问题的 level 就是不一样) 与其去喷泛型,喷 iter ,不如看看你的使用场景到底有没有必要用到这些特性。 作为一个内部基础库维护者,我真得感谢泛型替我省了不少 garbage 代码和注释的口水,编译器保证比我额外加一堆检查要省事的多,而且 iter 本身,在重数据处理的流程中也是非常好用的特性。 你的业务场景用不到,那就不用。没必要跳出来喷一句:go 的语言设计就是一坨屎山。 如果你有意见,请积极参与到社区标准落地中去,或者,zig ,请。 |
92
FYFX 120 天前
https://go.dev/wiki/RangefuncExperiment ,OP 应该在一开始就把官方给的例子贴全了
说起来要是我理解的没问题 for i,s := range Backward(sl){...} 这里的 i,s 不是从 Backward 函数返回的,更像是给回调函数声明用的?然后编译器再往回调函数里面补个 return true? |
93
james122333 120 天前 via Android
我也觉得这过于複杂
|
94
james122333 120 天前 via Android
|
95
jiangzm 120 天前
语法过于丑漏
|
96
DOLLOR 120 天前
把 OP 的 go 翻译成 TS ,大概是这样子的
const Backward = <E>(s: E[]): (myYield: (i: number, e: E) => boolean) => void => { ⬜return (myYield: (i: number, e: E) => boolean) => { ⬜⬜for (let i = s.length - 1; i >= 0; i -= 1) { ⬜⬜⬜if (!(myYield(i, s[i]))) { ⬜⬜⬜⬜return ⬜⬜⬜} ⬜⬜} ⬜⬜return ⬜} } const main = () => { ⬜const sl = ["hello", "world", "golang"] ⬜forRange(Backward(sl), (i, s) => { ⬜⬜console.log(`${i} : ${s}`) ⬜⬜return true ⬜}) } // 模拟 for range 的行为 const forRange = <E>( ⬜iter: (myYield: (i: number, e: E) => boolean) => void, ⬜body: (i: number, s: E) => boolean ) => { ⬜iter(body) } main() 也就是说,迭代器并没有增加任何新的语法,就是单纯满足签名为 func(func(int, E) bool)的函数。 你说它繁杂、丑陋嘛,确实,要写大量的模板代码。 但你说它大道至简嘛,确实,没有增加任何新的语法。😂 |
97
collery 120 天前
不好意思,方法签名数括号没数过来~~~
低智 javaer |
100
yumenaka 120 天前 via Android
作为一个调库的低水平用户,range over func 让我少写了不少模板代码。
因为调库的地方,总是比写库的地方多,我的项目的阅读难度其实是下降了。 复杂性从来都不会消失,只是被封装。 抱怨第三方库会因此变难的用户,技能应该比我熟练,经常阅读第三方库。但做的工作,又没有涉及更底层的部分吧。 至于语法与格式…… 用 go 的人,不是从一开始就接受了 go fmt 吗? |