有没有一种方法可以迭代一系列整数?

286

Go的range可以迭代地遍历maps和slices,但我想知道是否有一种方法可以迭代数值范围,就像这样:

for i := range [1..10] {
    fmt.Println(i)
}

在Go语言中是否有一种像Ruby的Range类那样表示整数范围的方法?


关于C++的同样问题(最初发布于2013年),仅供比较参考。 - einpoklum
13个回答

375

在 Go 中惯用的方法是编写类似这样的 for 循环。

for i := 1; i <= 10; i++ {
    fmt.Println(i)
}

使用范围确实有优势,并且它们在许多其他语言中得到了使用,但Go的设计原则是仅在好处显著超过成本(包括使语言更大的成本)的情况下引入抽象。对于范围的成本和收益,理智的人会有不同的看法,但这个答案是我试图描述Go惯用语的方式。


382
我认为大多数人不会认为这个三表达式版本比 @Vishnu 写的更简单。除非经过多年的 C 或 Java 教育洗礼后才可能如此认为;-) - Thomas Ahle
18
重点是你总是需要使用这个三表达式的for循环版本(也就是说,你可以用它做更多的事情,OP中的语法仅适用于数字范围的更受限制的情况,在任何语言中,你都需要这个扩展版本),它已经足以完成相同的任务,而且并没有显著的区别,那么为什么要学习或记忆另一种语法呢。如果你正在进行一个大型和复杂的项目编码,你已经有足够的问题需要处理了,不需要因为像一个简单的循环这样的东西而与编译器纠缠不清。 - Brad Peabody
14
@BradPeabody 这实际上是一种偏好问题。Python没有3表达式循环也能正常工作。许多人认为for-each语法要比传统的for循环更容易避免错误,而且它本身并不低效。 - VinGarcia
3
@necromancer,这是Rob Pike的一篇文章,他的论点与我的回答差不多。链接为https://groups.google.com/d/msg/golang-nuts/7J8FY07dkW0/goWaNVOkQU0J。也许Go语言社区并不认同我的回答,但如果这个回答得到了语言作者之一的支持,那么应该不会太糟糕。 - Paul Hankin
6
@PaulHankin,这完全取决于你是否将编码视为一门科学或一种信仰。不幸的是,Rob在这里犯了错误并处于防御性的权力旅行中,而你正在盲目地传递他的观点。为了你自己的工程利益,请不要忽视在Rob的循环中更改i的值会导致它自我销毁,而使用range方法则不会受到影响。此外,“DRY”是一种被广泛接受的语言设计原则,在Rob的循环中,你可以看到i出现了3次!无论如何,现在看着我在重复自己。我将把第一个评论中的每个赞视为礼貌的反对票。 - necromancer
显示剩余8条评论

65

Mark Mishyn建议使用slice,但是没有必要使用make创建数组并在for中使用它返回的切片,因为可以使用通过文字创建的数组,并且更短

for i := range [5]int{} {
        fmt.Println(i)
}

18
如果您不打算使用变量,也可以省略左侧并使用 for range [5]int{} { - blockloop
21
缺点是这里的 5 是一个字面值,无法在运行时确定。 - Steve Powell
它是否比普通的三表达式for循环更快或者相当? - Amit Tripathi
@AmitTripathi 是的,它是可比较的,对于数十亿次迭代,执行时间几乎相同。 - Daniil Grankin
2
看起来不错,但是它使用了O(n)的内存吗? - Ian
显示剩余2条评论

57

这里有一个比较目前为止提出的两种方法的程序

import (
    "fmt"

    "github.com/bradfitz/iter"
)

func p(i int) {
    fmt.Println(i)
}

func plain() {
    for i := 0; i < 10; i++ {
        p(i)
    }
}

func with_iter() {
    for i := range iter.N(10) {
        p(i)
    }
}

func main() {
    plain()
    with_iter()
}

按照这样的方式进行编译,可以生成反汇编代码。

go build -gcflags -S iter.go

这里是简化版(我已从列表中删除非说明内容)

设置

0035 (/home/ncw/Go/iter.go:14) MOVQ    $0,AX
0036 (/home/ncw/Go/iter.go:14) JMP     ,38

循环

0037 (/home/ncw/Go/iter.go:14) INCQ    ,AX
0038 (/home/ncw/Go/iter.go:14) CMPQ    AX,$10
0039 (/home/ncw/Go/iter.go:14) JGE     $0,45
0040 (/home/ncw/Go/iter.go:15) MOVQ    AX,i+-8(SP)
0041 (/home/ncw/Go/iter.go:15) MOVQ    AX,(SP)
0042 (/home/ncw/Go/iter.go:15) CALL    ,p+0(SB)
0043 (/home/ncw/Go/iter.go:15) MOVQ    i+-8(SP),AX
0044 (/home/ncw/Go/iter.go:14) JMP     ,37
0045 (/home/ncw/Go/iter.go:17) RET     ,

这里是 with_iter

设置

0052 (/home/ncw/Go/iter.go:20) MOVQ    $10,AX
0053 (/home/ncw/Go/iter.go:20) MOVQ    $0,~r0+-24(SP)
0054 (/home/ncw/Go/iter.go:20) MOVQ    $0,~r0+-16(SP)
0055 (/home/ncw/Go/iter.go:20) MOVQ    $0,~r0+-8(SP)
0056 (/home/ncw/Go/iter.go:20) MOVQ    $type.[]struct {}+0(SB),(SP)
0057 (/home/ncw/Go/iter.go:20) MOVQ    AX,8(SP)
0058 (/home/ncw/Go/iter.go:20) MOVQ    AX,16(SP)
0059 (/home/ncw/Go/iter.go:20) PCDATA  $0,$48
0060 (/home/ncw/Go/iter.go:20) CALL    ,runtime.makeslice+0(SB)
0061 (/home/ncw/Go/iter.go:20) PCDATA  $0,$-1
0062 (/home/ncw/Go/iter.go:20) MOVQ    24(SP),DX
0063 (/home/ncw/Go/iter.go:20) MOVQ    32(SP),CX
0064 (/home/ncw/Go/iter.go:20) MOVQ    40(SP),AX
0065 (/home/ncw/Go/iter.go:20) MOVQ    DX,~r0+-24(SP)
0066 (/home/ncw/Go/iter.go:20) MOVQ    CX,~r0+-16(SP)
0067 (/home/ncw/Go/iter.go:20) MOVQ    AX,~r0+-8(SP)
0068 (/home/ncw/Go/iter.go:20) MOVQ    $0,AX
0069 (/home/ncw/Go/iter.go:20) LEAQ    ~r0+-24(SP),BX
0070 (/home/ncw/Go/iter.go:20) MOVQ    8(BX),BP
0071 (/home/ncw/Go/iter.go:20) MOVQ    BP,autotmp_0006+-32(SP)
0072 (/home/ncw/Go/iter.go:20) JMP     ,74

循环

0073 (/home/ncw/Go/iter.go:20) INCQ    ,AX
0074 (/home/ncw/Go/iter.go:20) MOVQ    autotmp_0006+-32(SP),BP
0075 (/home/ncw/Go/iter.go:20) CMPQ    AX,BP
0076 (/home/ncw/Go/iter.go:20) JGE     $0,82
0077 (/home/ncw/Go/iter.go:20) MOVQ    AX,autotmp_0005+-40(SP)
0078 (/home/ncw/Go/iter.go:21) MOVQ    AX,(SP)
0079 (/home/ncw/Go/iter.go:21) CALL    ,p+0(SB)
0080 (/home/ncw/Go/iter.go:21) MOVQ    autotmp_0005+-40(SP),AX
0081 (/home/ncw/Go/iter.go:20) JMP     ,73
0082 (/home/ncw/Go/iter.go:23) RET     ,

所以你可以看到,即使iter解决方案在设置阶段完全内联,它的成本仍然高得多。 在循环阶段,循环中有一个额外的指令,但情况并不太糟。

我会使用简单的for循环。


12
你的计算迭代解的方法较为耗时我并未看出。你用于计数Go伪汇编指令的方法有缺陷。请进行基准测试。 - peterSO
13
有一个解决方案调用runtime.makeslice,而另一个则没有 - 我不需要进行基准测试就知道它会慢得多! - Nick Craig-Wood
6
是的,runtime.makeslice 非常聪明,如果您要求分配零大小的内存,它不会分配任何内存。但是上面的代码仍在调用它,并且根据您的基准测试,在我的机器上需要比原来多花费 10 纳秒的时间。 - Nick Craig-Wood
6
这让我想起了一些人建议出于性能原因使用C而不是C ++的情况。 - necromancer
11
在Goland中,争论纳秒级CPU操作的运行时性能似乎对我来说有点荒谬。在易读性之后,我会将这视为非常遥远的最后考虑因素。即使CPU性能是相关的,for循环的内容几乎总是会淹没由循环本身引起的任何差异。 - Jonathan Hartley
显示剩余2条评论

23

iter 是一个非常小的包,它提供了一种在整数上迭代的句法不同的方式。

for i := range iter.N(4) {
    fmt.Println(i)
}

Go语言的作者Rob Pike曾批评过它

似乎每当有人提出一种避免使用类似for循环的惯用方式来完成某些任务,因为这种方式感觉过于冗长或麻烦时,结果几乎总是比所谓更短的东西打的按键数还要多。[...] 更不用说所有这些“改进”带来的疯狂开销了。


31
Pike的批评过于简单化,只涉及到按键次数而未考虑不断重新声明范围所带来的心智负荷。此外,对于大部分现代编辑器来说,使用iter版本实际上需要的按键次数更少,因为rangeiter会自动完成。 - Chris Redford
1
@lang2 在类Unix系统中,与Go不同,“for”循环不是一流公民。另外,与“for”不同,“seq”会将一系列数字流式传输到标准输出中。是否对它们进行迭代取决于使用者。虽然在Shell中“for i in $(seq 1 10); do ... done”很常见,但这只是执行for循环的一种方式,而for循环本身仅是使用“seq”输出的一种方式,尽管非常常见。 - erik258
3
此外,派克(Pike)没有考虑到编译器可以按照语言规范中包含的区间语法来构建编译器,以便将 i in range(10) 正好处理成类似于 i := 0; i < 10; i++ 的形式。请注意,本文只是翻译,不包括任何解释或其他内容。 - Robsdedude
1
FYI:“iter”包在2014年发布时本意是一个教育性的玩笑。人们没有理解其中的玩笑部分,开始依赖它。我想这没关系。(这是互联网。)但这有点奇怪。这只是一行代码,甚至不符合Go语言的习惯用法。 - Carl Winbäck

9
这是一个基准测试,用于比较使用ForClause的Go for语句和Go range语句,使用iter包。请参考以下内容: iter_test.go
package main

import (
    "testing"

    "github.com/bradfitz/iter"
)

const loops = 1e6

func BenchmarkForClause(b *testing.B) {
    b.ReportAllocs()
    j := 0
    for i := 0; i < b.N; i++ {
        for j = 0; j < loops; j++ {
            j = j
        }
    }
    _ = j
}

func BenchmarkRangeIter(b *testing.B) {
    b.ReportAllocs()
    j := 0
    for i := 0; i < b.N; i++ {
        for j = range iter.N(loops) {
            j = j
        }
    }
    _ = j
}

// It does not cause any allocations.
func N(n int) []struct{} {
    return make([]struct{}, n)
}

func BenchmarkIterAllocs(b *testing.B) {
    b.ReportAllocs()
    var n []struct{}
    for i := 0; i < b.N; i++ {
        n = iter.N(loops)
    }
    _ = n
}

输出:

$ go test -bench=. -run=.
testing: warning: no tests to run
PASS
BenchmarkForClause      2000       1260356 ns/op           0 B/op          0 allocs/op
BenchmarkRangeIter      2000       1257312 ns/op           0 B/op          0 allocs/op
BenchmarkIterAllocs 20000000            82.2 ns/op         0 B/op          0 allocs/op
ok      so/test 7.026s
$

5
如果你将循环次数设置为10并重新运行基准测试,你会看到显著的差异。在我的机器上,ForClause花费5.6纳秒,而Iter花费15.4纳秒,因此调用分配器(即使它足够聪明而不分配任何东西)仍然需要花费10ns和大量额外的I-cache清空代码。 - Nick Craig-Wood
我很感兴趣看到您对我在答案中创建并引用的包进行基准测试和批评的结果。链接 - Chris Redford

8
如果您想要遍历一个范围而不使用索引或其他内容,这个示例代码对我来说非常有效。无需额外的声明,也不需要_。尽管我没有检查过性能。
for range [N]int{} {
    // Body...
}

P.S. 第一次学习Go语言。如果这是错误的方法,请批评指正。


1
到目前为止(版本1.13.6),它还没有起作用。抛出“非常量数组边界”错误。 - WHS
N必须是一个常量:对于范围[5]int{}{} - elulcao
他要求采用惯用的方法。 - Aaron

6
虽然我理解你对缺少这种语言特性的担忧,但你可能只需要使用普通的for循环。随着你编写更多 Go 代码,你可能会发现这样做比你想象中更好。
我编写了这个 iter 包,它由一个简单、惯用的 for 循环支持,通过一个 chan int 返回值。这是为了改进在 https://github.com/bradfitz/iter 中发现的设计问题,该库存在缓存和性能问题,以及一个巧妙但奇怪和不直观的实现方式。我的版本操作方式相同。
package main

import (
    "fmt"
    "github.com/drgrib/iter"
)

func main() {
    for i := range iter.N(10) {
        fmt.Println(i)
    }
}

然而,基准测试表明使用通道是一种非常昂贵的选择。可以从我的包中的iter_test.go运行的3种方法进行比较。

go test -bench=. -run=.

量化了它的性能差距

BenchmarkForMany-4                   5000       329956 ns/op           0 B/op          0 allocs/op
BenchmarkDrgribIterMany-4               5    229904527 ns/op         195 B/op          1 allocs/op
BenchmarkBradfitzIterMany-4          5000       337952 ns/op           0 B/op          0 allocs/op

BenchmarkFor10-4                500000000         3.27 ns/op           0 B/op          0 allocs/op
BenchmarkDrgribIter10-4            500000      2907 ns/op             96 B/op          1 allocs/op
BenchmarkBradfitzIter10-4       100000000        12.1 ns/op            0 B/op          0 allocs/op

在这个过程中,此基准测试还显示了在循环大小为10时,与内置的for语句相比,bradfitz解决方案表现不佳。
简而言之,到目前为止似乎没有发现一种方法可以复制内置的for语句的性能,同时提供类似于Python和Ruby中所发现的[0,n)的简单语法。
这很遗憾,因为Go团队可能很容易向编译器添加一个简单规则来更改如下行:
for i := range 10 {
    fmt.Println(i)
}

该代码将被编译为与for i := 0; i < 10; i++相同的机器码。

然而,公平地说,在编写自己的 iter.N(但在对其进行基准测试之前),我回顾了最近编写的程序,看看所有可以使用它的地方。实际上并不多。只有一个位置,在我的代码中非关键部分,我可以不使用更完整的默认for子句。

因此,虽然从原则上看,这可能看起来是语言上的巨大失望,但您可能会发现 - 就像我一样 - 在实践中您实际上并不真正需要它。就像 Rob Pike 对泛型所说的那样,您可能并不会像您认为的那样非常想念这个功能。


2
使用通道进行迭代非常昂贵; goroutine 和通道是便宜的,但它们不是免费的。如果通道上的迭代范围提前终止,则 goroutine 永远不会结束(goroutine 泄漏)。vector 包中已删除了 Iter 方法。"container/vector: remove Iter() from interface (Iter() is almost never the right mechanism to call)." 你的 iter 解决方案总是最昂贵的。 - peterSO

1
你也可以查看github.com/wushilin/stream。它是类似于java.util.stream的延迟流概念。
// It doesn't really allocate the 10 elements.
stream1 := stream.Range(0, 10)

// Print each element.
stream1.Each(print)

// Add 3 to each element, but it is a lazy add.
// You only add when consume the stream
stream2 := stream1.Map(func(i int) int {
    return i + 3
})

// Well, this consumes the stream => return sum of stream2.
stream2.Reduce(func(i, j int) int {
    return i + j
})

// Create stream with 5 elements
stream3 := stream.Of(1, 2, 3, 4, 5)

// Create stream from array
stream4 := stream.FromArray(arrayInput)

// Filter stream3, keep only elements that is bigger than 2,
// and return the Sum, which is 12
stream3.Filter(func(i int) bool {
    return i > 2
}).Sum()

希望这有所帮助。

1
我已经用Golang编写了一个包,模仿了Python的range函数:
https://github.com/thedevsaddam/iter
package main

import (
    "fmt"

    "github.com/thedevsaddam/iter"
)

func main() {
    // sequence: 0-9
    for v := range iter.N(10) {
        fmt.Printf("%d ", v)
    }
    fmt.Println()
    // output: 0 1 2 3 4 5 6 7 8 9

    // sequence: 5-9
    for v := range iter.N(5, 10) {
        fmt.Printf("%d ", v)
    }
    fmt.Println()
    // output: 5 6 7 8 9

    // sequence: 1-9, increment by 2
    for v := range iter.N(5, 10, 2) {
        fmt.Printf("%d ", v)
    }
    fmt.Println()
    // output: 5 7 9

    // sequence: a-e
    for v := range iter.L('a', 'e') {
        fmt.Printf("%s ", string(v))
    }
    fmt.Println()
    // output: a b c d e
}


注意:我是为了好玩而写的!顺便说一下,有时可能会有帮助。

1
这里有一个紧凑、动态的版本,不依赖于iter(但工作方式类似):
package main

import (
    "fmt"
)

// N is an alias for an unallocated struct
func N(size int) []struct{} {
    return make([]struct{}, size)
}

func main() {
    size := 1000
    for i := range N(size) {
        fmt.Println(i)
    }
}

通过一些调整,size 可以是 uint64 类型(如果需要的话),但这就是要点。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接