go 新特性 range over func 没怎么看懂。。。。写库的时候用吗?

307 天前
 dyllen

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这个函数这形式看起来还真有点复杂,不好理解。。。。可能例子不是相应场景下需要的,看半天没理解。 感觉一般都用不上。

11711 次点击
所在节点    Go 编程语言
105 条回复
sagaxu
306 天前
@vfs 之前说好的 Go 不需要泛型,结果加了,加了迭代器之后,也许异常也会加进去,defer+异常+panic 组合下的坑够博主们写 100 篇了。这次加的迭代器,易读性比 Java 和 Python 都低不少,参数是函数还返回函数的高阶函数,倒不是说理解不了,只不过读到那里思路卡顿一下才过得去。

“不直观”程度跟经典的 signal 函数定义有的一拼
void ( *signal(int signum, void (*handler)(int)) ) (int);
voidmnwzp
306 天前
这种傻逼代码都能作为官方示例?看来 golang 药丸
bugfan
306 天前
@voidmnwzp #82 8 月初,golang 技术主管换人了。感觉要走下坡路了😭
bugfan
306 天前
label
306 天前
xz410236056
306 天前
这不就是返回一个闭包(高阶函数)其他语言叫生成器、装饰器。这不是很正常的语法吗?
xz410236056
306 天前
@assassing #21 学两天 swift 就老实了,关键字多到可以出一本书了,完全就是两个极端
vipfts
306 天前
@Nazz #38 虽然我看不懂, 但给你点赞就对了
assassing
306 天前
@xz410236056 #87 那肯定 Go 更好。

只不过这个迭代器实现太难用,支持了和没支持一样。像同时另一个 range 改进:for i := range n 替代 for i := 0; i < n; i++,我就觉得挺好,当然肯定也有人喷糖撒过头了。
lxdlam
306 天前
首先是一个时间线:
- 一个 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 整个语言的发展历史来看,为了一些之前没考虑到的事情,动态开洞擦屁股这种事,也不是发生一次两次了。但是这条回复的目的还是给大家一个清晰的认知,也建议讨论设计问题前,先搞清楚前因后果时间脉络,减少鸡同鸭讲。
povsister
306 天前
#90
概括的非常好了,虽然你可以说 go 是 googelang ,因为他确实也塞了很多私货,甚至按照 k8s 的需求定制特性也不是没有先例。

但观察 go 社区的讨论,你会发现没人像 V2EX 发言一样:随性发表观点看法而不带任何论据。
甚至 rsc 本人也会对你提出的疑问认真回复(有幸被回复过,只能说大佬看问题的 level 就是不一样)

与其去喷泛型,喷 iter ,不如看看你的使用场景到底有没有必要用到这些特性。
作为一个内部基础库维护者,我真得感谢泛型替我省了不少 garbage 代码和注释的口水,编译器保证比我额外加一堆检查要省事的多,而且 iter 本身,在重数据处理的流程中也是非常好用的特性。

你的业务场景用不到,那就不用。没必要跳出来喷一句:go 的语言设计就是一坨屎山。
如果你有意见,请积极参与到社区标准落地中去,或者,zig ,请。
FYFX
306 天前
https://go.dev/wiki/RangefuncExperiment ,OP 应该在一开始就把官方给的例子贴全了
说起来要是我理解的没问题
for i,s := range Backward(sl){...} 这里的 i,s 不是从 Backward 函数返回的,更像是给回调函数声明用的?然后编译器再往回调函数里面补个 return true?
james122333
306 天前
我也觉得这过于複杂
james122333
306 天前
@vfs

有泛型还是挺省力的 不用写太多 switch type 和反射 异常有 panic recover 即可不用太多东西
jiangzm
306 天前
语法过于丑漏
DOLLOR
306 天前
把 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)的函数。
你说它繁杂、丑陋嘛,确实,要写大量的模板代码。
但你说它大道至简嘛,确实,没有增加任何新的语法。😂
collery
306 天前
不好意思,方法签名数括号没数过来~~~
低智 javaer
mark2025
306 天前
@DOLLOR ts 看起来似乎顺眼些~
Trim21
306 天前
@Elaina #61 那 map[string]int 要不要改尖括号啊?
yumenaka
306 天前
作为一个调库的低水平用户,range over func 让我少写了不少模板代码。
因为调库的地方,总是比写库的地方多,我的项目的阅读难度其实是下降了。

复杂性从来都不会消失,只是被封装。
抱怨第三方库会因此变难的用户,技能应该比我熟练,经常阅读第三方库。但做的工作,又没有涉及更底层的部分吧。

至于语法与格式……
用 go 的人,不是从一开始就接受了 go fmt 吗?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://ex.noerr.eu.org/t/1066038

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX