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

时间:2012-12-22 06:08:49

标签: iterator go

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

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

修改

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

8 个答案:

答案 0 :(得分:46)

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

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
    }
}

游乐场: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)
}

游乐场: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
}

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

答案 1 :(得分: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)
    }
}

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

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

答案 2 :(得分:14)

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

我需要迭代某些集合类型,而这些集合类型的确切存储实现尚未确定。这个,以及从客户端抽象实现细节的其他原因,让我用各种迭代方法进行一些测试。 Full code here,包括一些使用errors as values的实现。以下是基准测试结果:

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

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

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

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

尽管我喜欢关闭,但表现明智,它是一个禁忌。至于设计模式,Gophers更喜欢术语&#34;习惯的做法&#34;有充分理由的东西。另外grep the it source tree for iterators:如此少的文件提到名称,迭代器肯定不是Go的东西。

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

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

答案 3 :(得分:4)

通过为goroutines提供第二个控制消息通道,您可以在不泄漏的情况下突破。在最简单的情况下,它只是chan bool。当你想要停止goroutine时,你发送这个频道。在goroutine中,你将迭代器的通道发送,并在一个选择内的控制通道上监听。

Here's an example.

您可以通过允许不同的控制消息来进一步实现,例如“跳过”。

你的问题很抽象,所以说一个具体的例子会有所帮助。

答案 4 :(得分:1)

查看容器/列表包,看起来没有办法做到这一点。如果迭代对象,则应使用类似C的方式。

像这样。

type Foo struct {
...
}

func (f *Foo) Next() int {
...
}

foo := Foo(10)

for f := foo.Next(); f >= 0; f = foo.Next() {
...
}

答案 5 :(得分:1)

这是我想用渠道和goroutines做的一种方式:

mom

https://play.golang.org/p/M6NPT-hYPNd

我从Rob Pike的Go Concurrency Patterns谈话中得到了这个想法。

答案 6 :(得分:0)

事实上,这里有许多看似不同的解决方案,这实际上意味着这并不是一种惯用的方法。我正在开始使用Go语言,我认为有一种方法可以利用range的力量。可悲的是没有。

这是我想出的(与上面的某些解决方案类似)

// Node Basically, this is the iterator (or the head of it) 
// and the scaffolding for your itterable type
type Node struct {
    next *Node
}

func (node *Node) Next() (*Node, bool) {
    return node.next, node.next != nil
}

// Add add the next node
func (node *Node) Add(another *Node) {
    node.next = another
}

这是我的用法:

node := &Node{}
node.Add(&Node{})

for goOn := true; goOn; node, goOn = node.Next() {
    fmt.Println(node)
}

或者可能是一个更优雅的解决方案:

...
func (node *Node) Next() *Node {
    return node.next
}
...

for ; node != nil; node = node.Next() {
    fmt.Println(node)
}

答案 7 :(得分:0)

正如其他队友所说,您可以使用渠道来实现所需的Generator设计模式。

生成器功能

通道和goroutines为实现某种形式的 使用生成器功能的生产者/生产者模式。在这种方法中,goroutine是 包装在一个函数中,该函数生成的值是通过由 功能。消费者goroutine在生成这些值时会收到它们。

示例摘自 Go真实世界的设计模式

package main

import (
    "fmt"
    "strings"
    )

func main() {
    data := []string{"Sphinx of black quartz, judge my vow", 
             "The sky is blue and the water too", 
             "Cozy lummox gives smart squid who asks for job pen",
             "Jackdaws love my big sphinx of quartz",
             "The quick onyx goblin jumps over the lazy dwarf"}
    histogram := make(map[string]int)
    words := words(data) // returns handle to data channel
    for word := range words { // Reads each word from channel every time
        histogram[word]++
    }   
    fmt.Println(histogram)
}

// Generator function that produces data
func words(data []string) <-chan string {
    out := make(chan string)
    // Go Routine
    go func() {
        defer close(out) // closes channel upon fn return
        for _, line := range data {
            words := strings.Split(line, " ")
            for _, word := range words {
                word = strings.ToLower(word)
                out <- word // Send word to channel 
            }
        }
     }()
     return out
}

https://play.golang.org/p/f0nynFWbEam

在此示例中,生成器函数声明为 func words(data [] string)<- chan string ,返回字符串元素的仅接收通道。使用者函数(在本例中为 main())接收生成器函数发出的数据,该数据使用 for…range 循环进行处理。

此设计模式的改进版本:

https://play.golang.org/p/uyUfz3ALO6J

添加下一步错误之类的方法:

type iterator struct {
    valueChan   <-chan interface{}
    okChan      <-chan bool
    errChan     <-chan error
    err     error
}

func (i *iterator) next() (interface{}, bool) {
    var (
        value   interface{}
        ok  bool
    )
    value, ok, i.err = <-i.valueChan, <-i.okChan, <-i.errChan
    return value, ok
}

func (i *iterator) error() error {
    return i.err
}

// Generator function that produces data
func NewIterator(data []string) iterator {
    out := make(chan interface{})
    ok := make(chan bool)
    err := make(chan error)
    // Go Routine
    go func() {
        defer close(out) // closes channel upon fn return
        for _, line := range data {
            words := strings.Split(line, " ")
            for _, word := range words {
                word = strings.ToLower(word)
                out <- word // Send word to channel and waits for its reading
                ok <- true
                err <- nil // if there was any error, change its value
            }
        }
        out <- ""
        ok <- false
        err <- nil
     }()

     return iterator{ out, ok, err, nil }
}