在Go中创建迭代器的最惯用方法是什么?

Kug*_*gel 42 iterator go

一种选择是使用渠道.通道在某种程度上类似于迭代器,您可以使用range关键字迭代它们.但是当你发现你不能在没有泄漏goroutine的情况下突破这个循环时,使用会变得有限.

在go中创建迭代器模式的惯用方法是什么?

编辑:

渠道的根本问题在于它们是推模型.迭代器是拉模型.你不必告诉iterator停止.我正在寻找一种以一种很好的表达方式迭代集合的方法.我还想链接迭代器(map,filter,fold alternative).

Son*_*nia 49

通道很有用,但闭包通常更合适.

package main

import "fmt"

func main() {
    gen := newEven()
    fmt.Println(gen())
    fmt.Println(gen())
    fmt.Println(gen())
    gen = nil // release for garbage collection
}

func newEven() func() int {
    n := 0
    // closure captures variable n
    return func() int {
        n += 2
        return n
    }
}
Run Code Online (Sandbox Code Playgroud)

游乐场:http://play.golang.org/p/W7pG_HUOzw

不喜欢闭嘴吗?使用带有方法的命名类型:

package main

import "fmt"

func main() {
    gen := even(0)
    fmt.Println(gen.next())
    fmt.Println(gen.next())
    fmt.Println(gen.next())
}

type even int

func (e *even) next() int {
    *e += 2
    return int(*e)
}
Run Code Online (Sandbox Code Playgroud)

游乐场:http://play.golang.org/p/o0lerLcAh3

这三种技术之间存在权衡,因此您无法提名一种惯用语.使用最符合您需求的东西.

链接很容易,因为函数是一流的对象.这是闭包示例的扩展.我为整数生成器添加了一个类型intGen,它清楚地说明了生成器函数用作参数和返回值的位置.mapInt以一般方式定义,将任何整数函数映射到整数生成器.其他功能,如过滤器和折叠,可以类似地定义.

package main

import "fmt"

func main() {
    gen := mapInt(newEven(), square)
    fmt.Println(gen())
    fmt.Println(gen())
    fmt.Println(gen())
    gen = nil // release for garbage collection
}

type intGen func() int

func newEven() intGen {
    n := 0
    return func() int {
        n += 2
        return n
    }
}

func mapInt(g intGen, f func(int) int) intGen {
    return func() int {
        return f(g())
    }
}

func square(i int) int {
    return i * i
}
Run Code Online (Sandbox Code Playgroud)

游乐场:http://play.golang.org/p/L1OFm6JuX0

  • 因此,我在这里随意地运用了“迭代器”的含义,试图展示一些有用的技术。对于追逐愿望并没有按要求回答问题表示歉意。在我第二次尝试回答时,我试图遵循更严格的定义。 (3认同)
  • 非常好.但是你不能在关闭时使用range关键字.你能提供一个for循环的例子吗?迭代如何停止?您可能需要第二个返回值来表示. (2认同)
  • 范围适用于内置类型,而不是用户定义的函数.你是对的,你可以使用第二个返回值来停止迭代.这将是清楚和个性的.当您找到所需内容或处理了所需数据量时,您可能也会停止迭代. (2认同)
  • 请注意,如果您确实知道要处理多少数据,那么将它们全部存储在一个切片中可能是有意义的。这应该没问题,除非您正在处理大量不适合内存的数据。如果需要,请确保在处理完数据后释放对数据的所有引用,然后可以对其进行垃圾收集。当然,对于切片,您可以使用范围。 (2认同)

wld*_*svc 16

TL; DR:忘记关闭和通道,太慢了.如果可以通过索引访问集合的各个元素,那么请在类似数组的类型上进行经典的C迭代.如果没有,请实现有状态迭代器.

我需要迭代一些集合类型,其中确切的存储实现尚未确定.这个,以及从客户端抽象实现细节的其他原因,让我用各种迭代方法做一些测试.这里是完整代码,包括一些将错误用作值的实现.以下是基准测试结果:

  • 类似数组结构的经典C迭代.该类型提供了ValueAt()和Len()方法:

    l := Len(collection)
    for i := 0; i < l; i++ { value := collection.ValueAt(i) }
    // benchmark result: 2492641 ns/op
    
    Run Code Online (Sandbox Code Playgroud)
  • 关闭样式迭代器.集合的Iterator方法返回next()函数(对集合和游标的闭包)和hasNext布尔值.next()返回下一个值和一个hasNext布尔值.请注意,这比使用单独的next()和hasNext()闭包返回单个值要快得多:

    for next, hasNext := collection.Iterator(); hasNext; {
        value, hasNext = next()
    }
    // benchmark result: 7966233 ns/op !!!
    
    Run Code Online (Sandbox Code Playgroud)
  • 有状态迭代器.一个带有两个数据字段的简单结构,集合和游标,以及两个方法:Next()和HasNext().这次集合的Iterator()方法返回一个指向正确初始化的迭代器结构的指针:

    for iter := collection.Iterator(); iter.HasNext(); {
        value := iter.Next()
    }
    // benchmark result: 4010607 ns/op
    
    Run Code Online (Sandbox Code Playgroud)

尽管我喜欢关闭,性能明智但它是一个禁忌.至于设计模式,Gophers更喜欢"惯用的方式"这个词,这是有充分理由的.另外grep the it source tree for iterators:如此少的文件提到名称,迭代器肯定不是Go的东西.

另请查看此页面:http://ewencp.org/blog/golang-iterators/

无论如何,接口在这里没有任何帮助,除非你想定义一些Iterable接口,但这是一个完全不同的主题.

  • 我喜欢双关语,“性能明智,这是不行的”,但我不确定我是否同意这一结论。查看您的数字,我发现所有数字彼此之间相差约3倍,而我的第一个想法是,您应该任意选择您认为最具有表现力的那个,除非您已实际分析了代码并确定此迭代是应用程序中的一个有意义的瓶颈。在许多应用程序中,循环内部的工作将占主导地位,而这一级别的迭代差异将可忽略不计。 (3认同)

Son*_*nia 14

TL; DR:迭代器在Go中不是惯用的.将它们留给其他语言.

然后,维基百科条目"Iterator模式"开始,"在面向对象的编程中,迭代器模式是一种设计模式......"两个红旗:首先,面向对象的编程概念通常不能很好地转化为Go第二,许多Go程序员对设计模式没有太多考虑.第一段还包括"迭代器模式从容器中解耦算法",但只是在声明"迭代器[访问]容器的元素之后.那么它是什么?如果算法正在访问容器的元素,它几乎不能声称是解耦的.许多语言的答案都涉及某种泛型,它允许语言概括为类似的数据结构.Go中的答案是接口.接口通过拒绝访问结构并要求所有交互都基于行为来强制执行更严格的算法和对象解耦行为意味着通过数据方法表达的能力.

对于最小迭代器类型,所需的功能是Next方法.Go接口可以通过简单地指定此单个方法签名来表示迭代器对象.如果希望容器类型可迭代,则必须通过实现接口的所有方法来满足迭代器接口.(我们这里只有一个,实际上接口通常只有一个方法.)

最小的工作示例:

package main

import "fmt"

// IntIterator is an iterator object.
// yes, it's just an interface.
type intIterator interface {
    Next() (value int, ok bool)
}

// IterableSlice is a container data structure
// that supports iteration.
// That is, it satisfies intIterator.
type iterableSlice struct {
    x int
    s []int
}

// iterableSlice.Next implements intIterator.Next,
// satisfying the interface.
func (s *iterableSlice) Next() (value int, ok bool) {
    s.x++
    if s.x >= len(s.s) {
        return 0, false
    }
    return s.s[s.x], true
}

// newSlice is a constructor that constructs an iterable
// container object from the native Go slice type.
func newSlice(s []int) *iterableSlice {
    return &iterableSlice{-1, s}
}

func main() {
    // Ds is just intIterator type.
    // It has no access to any data structure.
    var ds intIterator

    // Construct.  Assign the concrete result from newSlice
    // to the interface ds.  ds has a non-nil value now,
    // but still has no access to the structure of the
    // concrete type.
    ds = newSlice([]int{3, 1, 4})

    // iterate
    for {
        // Use behavior only.  Next returns values
        // but without insight as to how the values
        // might have been represented or might have
        // been computed.
        v, ok := ds.Next()
        if !ok {
            break
        }
        fmt.Println(v)
    }
}
Run Code Online (Sandbox Code Playgroud)

游乐场:http://play.golang.org/p/AFZzA7PRDR

这是接口的基本思想,但迭代切片是荒谬的过度杀伤.在很多情况下,你会达到在其他语言中的迭代器,你写Go代码使用内置的直接遍历基本类型的语言原语.您的代码保持清晰简洁.如果变得复杂,请考虑您真正需要的功能.你需要在某些函数中从随机位置发出结果吗?通道提供类似产量的功能,允许这样做.您需要无限列表或懒惰评估吗?闭包效果很好.您是否有不同的数据类型,并且需要它们透明地支持相同的操作?接口提供.通道,函数和接口都是一流的对象,这些技术都很容易组合.那么最惯用的方式是什么呢?它是尝试不同的技术,熟悉它们,并以最简单的方式使用满足您需求的任何东西.无论如何,面向对象意义上的迭代器几乎从不是最简单的.

  • 您太重视模式的定义了。迭代器的主要目的之一是不将整个事物保存在内存中,同时仍然使值的生成可重用。任何能做到这一点的东西都可以被认为是某种迭代器,Go 绝对支持这一点。 (3认同)
  • 好吧,我同意 C++ 迭代器没那么简单。但与 Python 生成器或 C# 中的 IEnumerable&lt;T&gt; 相比。你的最后一段很棒,也许我需要尝试用不同的技术来代替传统的迭代器。 (2认同)
  • 迭代器在Go中非常惯用,标准库中有很多示例,其中包括bufio.Scanner和tar.Reader。 (2认同)