一种选择是使用渠道。通道在某种程度上类似于迭代器,您可以使用 range 关键字对其进行迭代。但是当你发现你不能在不泄漏 goroutine 的情况下跳出这个循环时,使用就会受到限制。
在 Go 中创建迭代器模式的惯用方法是什么?
编辑:
渠道的根本问题是它们是推送模型。迭代器是一个拉模型。您不必告诉迭代器停止。我正在寻找一种以一种很好的表达方式迭代集合的方法。我还想链接迭代器(地图、过滤器、折叠替代品)。
通道很有用,但闭包通常更合适。
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
}
TL;DR:忘记关闭和通道,太慢了。如果您的集合中的各个元素可以通过索引访问,请在类数组类型上进行经典的 C 迭代。如果没有,请实现一个有状态的迭代器。
我需要遍历一些集合类型,但它们的确切存储实现尚未一成不变。这一点,加上从客户端抽象实现细节的无数其他原因,导致我使用各种迭代方法进行一些测试。完整的代码在这里,包括一些使用错误作为值的实现。以下是基准测试结果:
对类数组结构的经典 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 更喜欢“惯用的做法”这个词是有充分理由的。也 grep 迭代器的源代码树:由于很少有文件提到这个名字,迭代器绝对不是 Go 的东西。
另请查看此页面:http ://ewencp.org/blog/golang-iterators/
无论如何,接口在这里没有任何帮助,除非你想定义一些 Iterable 接口,但这是一个完全不同的主题。
TL;DR:迭代器在 Go 中不是惯用的。将它们留给其他语言。
深入然后,维基百科条目“迭代器模式”开始,“在面向对象编程中,迭代器模式是一种设计模式......”那里有两个危险信号:首先,面向对象编程概念通常不能很好地转化为 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 代码。您的代码保持简洁明了。如果事情变得复杂,请考虑您真正需要的功能。您是否需要在某些函数中从随机位置发出结果?通道提供了一种类似产量的功能,允许这样做。您需要无限列表还是惰性求值?闭包效果很好。您是否有不同的数据类型并且需要它们透明地支持相同的操作?接口交付。通道、函数和接口都是一流的对象,这些技术都很容易组合。那么什么是最惯用的方法?它是尝试不同的技术,熟悉它们,并以最简单的方式使用任何满足您需求的方法。无论如何,在面向对象的意义上,迭代器几乎从来都不是最简单的。
你可以通过为你的 goroutines 提供第二个控制消息的通道来突破而不会泄漏。在最简单的情况下,它只是一个chan bool
. 当你想让 goroutine 停止时,你在这个频道上发送。在 goroutine 中,您将迭代器的通道发送和监听放在一个选择中的控制通道上。
您可以通过允许不同的控制消息(例如“跳过”)来进一步实现这一点。
你的问题很抽象,所以说更多,一个具体的例子会有所帮助。
查看 container/list 包,似乎没有办法做到这一点。如果您迭代对象,则应使用类似 C 的方式。
像这样的东西。
type Foo struct {
...
}
func (f *Foo) Next() int {
...
}
foo := Foo(10)
for f := foo.Next(); f >= 0; f = foo.Next() {
...
}
这是我想到的一种使用通道和 goroutine 的方法:
package main
import (
"fmt"
)
func main() {
c := nameIterator(3)
for batch := range c {
fmt.Println(batch)
}
}
func nameIterator(batchSize int) <-chan []string {
names := []string{"Cherry", "Cami", "Tildy", "Cory", "Ronnie", "Aleksandr", "Billie", "Reine", "Gilbertina", "Dotti"}
c := make(chan []string)
go func() {
for i := 0; i < len(names); i++ {
startIdx := i * batchSize
endIdx := startIdx + batchSize
if startIdx > len(names) {
continue
}
if endIdx > len(names) {
c <- names[startIdx:]
} else {
c <- names[startIdx:endIdx]
}
}
close(c)
}()
return c
}
https://play.golang.org/p/M6NPT-hYPNd
我从 Rob Pike 的Go Concurrency Patterns talk 中得到了这个想法。
这里有这么多看似不同的解决方案这一事实意味着这似乎不是一种惯用的方式。我开始使用围棋,我认为会有一种方法可以利用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)
}
我发表了一篇关于这个主题的文章:
有一个相关的 Git 存储库:https ://github.com/serge-hulne/iter/tree/main/iterate
主要思想是:
func Fib(n int) chan int {
out := make(chan int)
go func() {
defer close(out)
for i, j := 0, 1; i < n; i, j = i+j, i {
out <- i
}
}()
return out
}
用作:
fibs = Fib(100)
for i := range Map(fibs) {
fmt.Printf("i = %6v\n", i)
}
正如其他伙伴所说,您可以使用通道来实现您正在寻找的生成器设计模式。
生成器函数
通道和 goroutine 为使用生成器函数实现某种形式的生产者/生产者模式提供了天然的基础。在这种方法中,goroutine 被包装在一个函数中,该函数生成通过函数返回的通道发送的值。消费者 goroutine 在生成这些值时接收它们。
从Go Design Patterns For Real-World中提取的示例
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
添加Next和Error等方法:
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 }
}