原文链接 https://blog.vertigrated.com/iterseq-in-practice

FirstN

返回序列中的前 N 个项。

// FirstN takes an iter.Seq[int] and returns a new iter.Seq[int] that
// yields only the first 'limit' items without creating intermediate slices.
func FirstN[T any](original iter.Seq[T], limit int) iter.Seq[T] {
    return iter.Seq[T](func(yield func(T) bool) {
        count := 0
        for item := range original {
            if count < limit {
                if !yield(item) {
                    return
                }
                count++
            } else {
                return
            }
        }
    })
}

SkipFirstN

跳过前 N 个元素并返回剩余的序列。

func SkipFirstN[T any](seq iter.Seq[T], skip int) iter.Seq[T] {
    return iter.Seq[T](func(yield func(T) bool) {
       next, stop := iter.Pull[T](seq)
       defer stop()

       for i := 0; i <= skip; i++ {
          _, ok := next()
          if !ok {
             break
          }
       }
       for {
          v, ok := next()
          if !ok {
             break
          }
          if !yield(v) {
             return
          }
       }
    })
}

SkipLimit(SubSeq)

示例内联组合 FirstNSkipFirstN

// I wrapped this in a function as straw man example just to get the highlighting to work
func SkipLimit[V any](it iter.Seq[V],, skip int, limit int) iter.Seq[V] {
    return FirstN[V](SkipFirstN[V](it, skip), limit)
}

// normally youo would just inline the composition ike this
subSeq := FirstN[string](SkipFirstN[string](it, skip, limit)

Chunk

从原始序列中创建一系列固定大小的序列,而不创建任何中间切片或数组。

当你向只接受 N 个项目的 API 提供数据时,这很实用。通过一些编码的改造,可以和 goroutines 和 channels 一起使用来处理通过并行处理生成的数据块。

// Chunk returns an iterator over consecutive sub-slices of up to n elements of s.
// All but the last iter.Seq chunk will have size n.
// Chunk panics if n is less than 1.
func Chunk[T any](sq iter.Seq[T], size int) iter.Seq[iter.Seq[T]] {
    if size < 0 {
       panic(errs.MinSizeExceededError.New("size %d must be >= 0", size))
    }

    return func(yield func(s iter.Seq[T]) bool) {
       next, stop := iter.Pull[T](sq)
       defer stop()
       endOfSeq := false
       for !endOfSeq {
          // get the first item for the chunk
          v, ok := next()
          // there are no more items !ok then exit loop
          // this prevents returning an extra empty iter.Seq at end of Seq
          if !ok {
             break
          }
          // create the next sequence chunk
          iterSeqChunk := func(yield func(T) bool) {
             i := 0
             for ; i < size-1; i++ {
                if ok {
                   if !ok {
                      // end of original sequence
                      // this sequence may be <= size
                      endOfSeq = true
                      break
                   }

                   if !yield(v) {
                      return
                   }
                   v, ok = next()
                }
             }
          }
          if !yield(iterSeqChunk) {
             return
          }
       }
    }
}

SeqToSeq2

此函数接受一个序列和一个键生成函数,并返回一个 iter.Seq2 ,而不创建任何中间切片或数组。

func SeqToSeq2[K any, V any](is iter.Seq[V], keyFunc func(v V) K) iter.Seq2[K, V] {
    return iter.Seq2[K, V](func(yield func(K, V) bool) {
       for v := range is {
          k := keyFunc(v)
          if !yield(k, v) {
             return
          }
       }
    })
}

Map

你们中大多数人可能认为我应该把它放在第一位。它与 Monad 在 bindmap 行为上最为相似,之所以命名为 Map 是有原因的。

但是这完全忽略了 iter 包的意义。这不仅仅关乎功能性编程,它是一种创建自定义可迭代类型的惯用方法。

这是按照定义最接近 Monad 行为的,跳过了 bind 语义,直接将 map 函数应用于值。如果你的值对象有一个名为 ID 的字段作为键,那么 keyFunc 可以是简单地返回 v.ID ,也可以是复杂地计算对象的 SHA256 哈希值作为键。或者,甚至不是映射,比如从 Person 结构中返回 FirstNameLastName

事实是 K 的值被定义为 any类型, 而不是 comparable 。这意味着它不只适用于来自Map对象的键值(Key)。

func SeqToSeq2[K any, V any](is iter.Seq[V], keyFunc func(v V) K) iter.Seq2[K, V] {
    return iter.Seq2[K, V](func(yield func(K, V) bool) {
       for v := range is {
          k := keyFunc(v)
          if !yield(k, v) {
             return
          }
       }
    })
}

DocumentIteratorToSeq

Firestore 文档迭代器被包裹在 iter.Seq 中,没有生成任何中间切片或数组。

// DocumentIteratorToSeq converts a firestore.Iterator to an iter.Seq.
// value is a pointer to the type V
func DocumentIteratorToSeq[V any](dsi firestore.DocumentIterator) iter.Seq[V] {
    return func(yield func(*V) bool) {
       defer dsi.Stop()
       for {
          doc, err := dsi.Next()
          if errors.Is(err, iterator.Done) {
             return
          }
          if err != nil {
             log.Error().Err(err).Msg("error iterating through Firestore documents")
             return
          }

          var b V
          err = doc.DataTo(&b)
          if err != nil {
             log.Error().Err(err).Msgf("error unmarshalling Firestore document with ID %s", doc.Ref.ID)
             return
          }

          if !yield(&b) {
             return
          }
       }
    }
}

DocumentIteratorToSeq2

与上面相同,但文档密钥用作返回的 iter.Seq2 中的 K 值作为 string

我总是使用 string 类型作为密钥,因此没有必要参数化密钥类型,那样会使它更复杂。

// DocumentIteratorToSeq2 converts a firestore.Iterator to an iter.Seq2.
// doc.Ref.ID is used as the "key" or first value, second value is a pointer to the type V
func DocumentIteratorToSeq2[V any](dsi *firestore.DocumentIterator) iter.Seq2[string, *V] {
    return func(yield func(string, *V) bool) {
        defer dsi.Stop()
        for {
            doc, err := dsi.Next()
            if errors.Is(err, iterator.Done) {
                return
            }
            if err != nil {
                log.Error().Err(err).Msg("error iterating through Firestore documents")
                return
            }

            var b V
            err = doc.DataTo(&b)
            if err != nil {
                log.Error().Err(err).Msgf("error unmarshalling Firestore document with ID %s", doc.Ref.ID)
                continue
            }

            if !yield(doc.Ref.ID, &b) {
                return
            }
        }
    }
}

引入的包

以下是上述代码示例中使用的包列表。

import (
    "iter"
    "slices"
    "strings"

    "cloud.google.com/go/firestore"
    "google.golang.org/api/iterator"
    "github.com/rs/zerolog/log"
    "github.com/joomcode/errorx"
    errs "github.com/jarrodhroberson/ossgo/errors"
)