{"id":18655437,"url":"https://github.com/kevwan/stream","last_synced_at":"2025-06-14T12:05:13.987Z","repository":{"id":40457048,"uuid":"444706127","full_name":"kevwan/stream","owner":"kevwan","description":"Stream API for Go.","archived":false,"fork":false,"pushed_at":"2022-05-07T04:58:57.000Z","size":65,"stargazers_count":92,"open_issues_count":6,"forks_count":14,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-06-14T12:03:07.165Z","etag":null,"topics":["go","golang","stream","stream-api"],"latest_commit_sha":null,"homepage":"https://go-zero.dev","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kevwan.png","metadata":{"files":{"readme":"readme-cn.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-01-05T07:29:08.000Z","updated_at":"2024-09-18T21:21:46.000Z","dependencies_parsed_at":"2022-08-18T17:43:10.377Z","dependency_job_id":null,"html_url":"https://github.com/kevwan/stream","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/kevwan/stream","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kevwan%2Fstream","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kevwan%2Fstream/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kevwan%2Fstream/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kevwan%2Fstream/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kevwan","download_url":"https://codeload.github.com/kevwan/stream/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kevwan%2Fstream/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259812991,"owners_count":22915196,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","golang","stream","stream-api"],"created_at":"2024-11-07T07:18:58.053Z","updated_at":"2025-06-14T12:05:13.947Z","avatar_url":"https://github.com/kevwan.png","language":"Go","readme":"# stream\n\n[English](readme.md) | 简体中文\n\n[![Go](https://github.com/kevwan/stream/workflows/Go/badge.svg?branch=main)](https://github.com/kevwan/stream/actions)\n[![codecov](https://codecov.io/gh/kevwan/stream/branch/main/graph/badge.svg)](https://codecov.io/gh/kevwan/stream)\n[![Go Report Card](https://goreportcard.com/badge/github.com/kevwan/stream)](https://goreportcard.com/report/github.com/kevwan/stream)\n[![Release](https://img.shields.io/github/v/release/kevwan/stream.svg?style=flat-square)](https://github.com/kevwan/stream)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n## 为什么会有这个项目\n\n`stream` 其实是 [go-zero](https://github.com/zeromicro/go-zero) 的一部分，但是一些用户问我是不是可以单独使用 `fx`（go-zero里叫 fx）而不用引入 `go-zero` 的依赖，所以我考虑再三，还是单独提供一个吧。但是，我强烈推荐你使用 `go-zero`，因为 `go-zero` 真的提供了很多很好的功能。\n\n\u003cimg src=\"https://oscimg.oschina.net/oscnet/up-db431abd40479b215575831d598d68907ac.png\" width=\"700\"\u003e\n\n## 什么是流处理\n\n如果有 java 使用经验的同学一定会对 java8 的 Stream 赞不绝口，极大的提高了们对于集合类型数据的处理能力。\n\n```java\nint sum = widgets.stream()\n              .filter(w -\u003e w.getColor() == RED)\n              .mapToInt(w -\u003e w.getWeight())\n              .sum();\n```\n\nStream 能让我们支持链式调用和函数编程的风格来实现数据的处理，看起来数据像是在流水线一样不断的实时流转加工，最终被汇总。Stream 的实现思想就是将数据处理流程抽象成了一个数据流，每次加工后返回一个新的流供使用。\n\n## Stream 功能定义\n\n动手写代码之前，先想清楚，把需求理清楚是最重要的一步，我们尝试代入作者的视角来思考整个组件的实现流程。首先把底层实现的逻辑放一下 ,先尝试从零开始进行功能定义 stream 功能。\n\nStream 的工作流程其实也属于生产消费者模型，整个流程跟工厂中的生产流程非常相似，尝试先定义一下 Stream 的生命周期：\n\n1. 创建阶段/数据获取（原料）\n2. 加工阶段/中间处理（流水线加工）\n3. 汇总阶段/终结操作（最终产品）\n\n下面围绕 stream 的三个生命周期开始定义 API：\n\n#### 创建阶段\n\n为了创建出数据流 stream 这一抽象对象，可以理解为构造器。\n\n我们支持三种方式构造 stream，分别是：切片转换，channel 转换，函数式转换。\n\n注意这个阶段的方法都是普通的公开方法，并不绑定 Stream 对象。\n\n```Go\n// 通过可变参数模式创建 stream\nfunc Just(items ...interface{}) Stream\n\n// 通过 channel 创建 stream\nfunc Range(source \u003c-chan interface{}) Stream\n\n// 通过函数创建 stream\nfunc From(generate GenerateFunc) Stream\n\n// 拼接 stream\nfunc Concat(s Stream, others ...Stream) Stream\n```\n\n#### 加工阶段\n\n加工阶段需要进行的操作往往对应了我们的业务逻辑，比如：转换，过滤，去重，排序等等。\n\n这个阶段的 API 属于 method 需要绑定到 Stream 对象上。\n\n结合常用的业务场景进行如下定义：\n\n```Go\n// 去除重复item\nDistinct(keyFunc KeyFunc) Stream\n// 按条件过滤item\nFilter(filterFunc FilterFunc, opts ...Option) Stream\n// 分组\nGroup(fn KeyFunc) Stream\n// 返回前n个元素\nHead(n int64) Stream\n// 返回后n个元素\nTail(n int64) Stream\n// 转换对象\nMap(fn MapFunc, opts ...Option) Stream\n// 合并item到slice生成新的stream\nMerge() Stream\n// 反转\nReverse() Stream\n// 排序\nSort(fn LessFunc) Stream\n// 作用在每个item上\nWalk(fn WalkFunc, opts ...Option) Stream\n// 聚合其他Stream\nConcat(streams ...Stream) Stream\n```\n\n加工阶段的处理逻辑都会返回一个新的 Stream 对象，这里有个基本的实现范式\n\n\u003cimg src=\"https://oscimg.oschina.net/oscnet/up-96d31b1c7e14d75d8a7a25a98de676ae085.png\" width=\"700\"\u003e\n\n#### 汇总阶段\n\n汇总阶段其实就是我们想要的处理结果，比如：是否匹配，统计数量，遍历等等。\n\n```Go\n// 检查是否全部匹配\nAllMatch(fn PredicateFunc) bool\n// 检查是否存在至少一项匹配\nAnyMatch(fn PredicateFunc) bool\n// 检查全部不匹配\nNoneMatch(fn PredicateFunc) bool\n// 统计数量\nCount() int\n// 清空stream\nDone()\n// 对所有元素执行操作\nForAll(fn ForAllFunc)\n// 对每个元素执行操作\nForEach(fn ForEachFunc)\n```\n\n梳理完组件的需求边界后，我们对于即将要实现的 Stream 有了更清晰的认识。在我的认知里面真正的架构师对于需求的把握以及后续演化能达到及其精准的地步，做到这一点离不开对需求的深入思考以及洞穿需求背后的本质。通过代入作者的视角来模拟复盘整个项目的构建流程，学习作者的思维方法论这正是我们学习开源项目最大的价值所在。\n\n好了，我们尝试定义出完整的 Stream 接口全貌以及函数。\n\n\u003e 接口的作用不仅仅是模版作用，还在于利用其抽象能力搭建项目整体的框架而不至于一开始就陷入细节，能快速的将我们的思考过程通过接口简洁的表达出来，学会养成自顶向下的思维方法从宏观的角度来观察整个系统，一开始就陷入细节则很容易拔剑四顾心茫然。。。\n\n```Go\nrxOptions struct {\n  unlimitedWorkers bool\n  workers          int\n}\nOption func(opts *rxOptions)\n// key生成器\n//item - stream中的元素\nKeyFunc func(item interface{}) interface{}\n// 过滤函数\nFilterFunc func(item interface{}) bool\n// 对象转换函数\nMapFunc func(intem interface{}) interface{}\n// 对象比较\nLessFunc func(a, b interface{}) bool\n// 遍历函数\nWalkFunc func(item interface{}, pip chan\u003c- interface{})\n// 匹配函数\nPredicateFunc func(item interface{}) bool\n// 对所有元素执行操作\nForAllFunc func(pip \u003c-chan interface{})\n// 对每个item执行操作\nForEachFunc func(item interface{})\n// 对每个元素并发执行操作\nParallelFunc func(item interface{})\n// 对所有元素执行聚合操作\nReduceFunc func(pip \u003c-chan interface{}) (interface{}, error)\n// item生成函数\nGenerateFunc func(source \u003c-chan interface{})\n\nStream interface {\n  // 去除重复item\n  Distinct(keyFunc KeyFunc) Stream\n  // 按条件过滤item\n  Filter(filterFunc FilterFunc, opts ...Option) Stream\n  // 分组\n  Group(fn KeyFunc) Stream\n  // 返回前n个元素\n  Head(n int64) Stream\n  // 返回后n个元素\n  Tail(n int64) Stream\n  // 获取第一个元素\n  First() interface{}\n  // 获取最后一个元素\n  Last() interface{}\n  // 转换对象\n  Map(fn MapFunc, opts ...Option) Stream\n  // 合并item到slice生成新的stream\n  Merge() Stream\n  // 反转\n  Reverse() Stream\n  // 排序\n  Sort(fn LessFunc) Stream\n  // 作用在每个item上\n  Walk(fn WalkFunc, opts ...Option) Stream\n  // 聚合其他Stream\n  Concat(streams ...Stream) Stream\n  // 检查是否全部匹配\n  AllMatch(fn PredicateFunc) bool\n  // 检查是否存在至少一项匹配\n  AnyMatch(fn PredicateFunc) bool\n  // 检查全部不匹配\n  NoneMatch(fn PredicateFunc) bool\n  // 统计数量\n  Count() int\n  // 清空stream\n  Done()\n  // 对所有元素执行操作\n  ForAll(fn ForAllFunc)\n  // 对每个元素执行操作\n  ForEach(fn ForEachFunc)\n}\n```\n\nchannel() 方法用于获取 Stream 管道属性，因为在具体实现时我们面向的是接口对象所以暴露一个私有方法 read 出来。\n\n```Go\n// 获取内部的数据容器channel,内部方法\nchannel() chan interface{}\n```\n\n## 实现思路\n\n功能定义梳理清楚了，接下来考虑几个工程实现的问题。\n\n### 如何实现链式调用\n\n链式调用，创建对象用到的 builder 模式可以达到链式调用效果。实际上 Stream 实现类似链式的效果原理也是一样的，每次调用完后都创建一个新的 Stream 返回给用户。\n\n```Go\n// 去除重复item\nDistinct(keyFunc KeyFunc) Stream\n// 按条件过滤item\nFilter(filterFunc FilterFunc, opts ...Option) Stream\n```\n\n### 如何实现流水线的处理效果\n\n所谓的流水线可以理解为数据在 Stream 中的存储容器，在 go 中我们可以使用 channel 作为数据的管道，达到 Stream 链式调用执行多个操作时**异步非阻塞**效果。\n\n### 如何支持并行处理\n\n数据加工本质上是在处理 channel 中的数据，那么要实现并行处理无非是并行消费 channel 而已，利用 goroutine 协程、WaitGroup 机制可以非常方便的实现并行处理。\n\n## go-zero 实现\n\n`core/fx/stream.go`\n\ngo-zero 中关于 Stream 的实现并没有定义接口，不过没关系底层实现时逻辑是一样的。\n\n为了实现 Stream 接口我们定义一个内部的实现类，其中 source 为 channel 类型，模拟流水线功能。\n\n```Go\nStream struct {\n  source \u003c-chan interface{}\n}\n```\n\n### 创建 API\n\n#### channel 创建 Range \n\n通过 channel 创建 stream\n\n```Go\nfunc Range(source \u003c-chan interface{}) Stream {  \n  return Stream{  \n    source: source,  \n  }  \n}\n```\n\n#### 可变参数模式创建 Just\n\n通过可变参数模式创建 stream，channel 写完后及时 close 是个好习惯。\n\n```Go\nfunc Just(items ...interface{}) Stream {\n  source := make(chan interface{}, len(items))\n  for _, item := range items {\n    source \u003c- item\n  }\n  close(source)\n  return Range(source)\n}\n```\n\n#### 函数创建 From\n\n通过函数创建 Stream\n\n```Go\nfunc From(generate GenerateFunc) Stream {\n  source := make(chan interface{})\n  threading.GoSafe(func() {\n    defer close(source)\n    generate(source)\n  })\n  return Range(source)\n}\n```\n\n因为涉及外部传入的函数参数调用，执行过程并不可用因此需要捕捉运行时异常防止 panic 错误传导到上层导致应用崩溃。\n\n```Go\nfunc Recover(cleanups ...func()) {\n  for _, cleanup := range cleanups {\n    cleanup()\n  }\n  if r := recover(); r != nil {\n    logx.ErrorStack(r)\n  }\n}\n\nfunc RunSafe(fn func()) {\n  defer rescue.Recover()\n  fn()\n}\n\nfunc GoSafe(fn func()) {\n  go RunSafe(fn)\n}\n```\n\n#### 拼接 Concat\n\n拼接其他 Stream 创建一个新的 Stream，调用内部 Concat method 方法，后文将会分析 Concat 的源码实现。\n\n```Go\nfunc Concat(s Stream, others ...Stream) Stream {\n  return s.Concat(others...)\n}\n```\n\n### 加工 API\n\n#### 去重 Distinct\n\n因为传入的是函数参数`KeyFunc func(item interface{}) interface{}`意味着也同时支持按照业务场景自定义去重，本质上是利用 KeyFunc 返回的结果基于 map 实现去重。\n\n函数参数非常强大，能极大的提升灵活性。\n\n```Go\nfunc (s Stream) Distinct(keyFunc KeyFunc) Stream {\n  source := make(chan interface{})\n  threading.GoSafe(func() {\n    // channel记得关闭是个好习惯\n    defer close(source)\n    keys := make(map[interface{}]lang.PlaceholderType)\n    for item := range s.source {\n      // 自定义去重逻辑\n      key := keyFunc(item)\n      // 如果key不存在,则将数据写入新的channel\n      if _, ok := keys[key]; !ok {\n        source \u003c- item\n        keys[key] = lang.Placeholder\n      }\n    }\n  })\n  return Range(source)\n}\n```\n\n使用案例：\n\n```Go\n// 1 2 3 4 5\nJust(1, 2, 3, 3, 4, 5, 5).Distinct(func(item interface{}) interface{} {\n  return item\n}).ForEach(func(item interface{}) {\n  t.Log(item)\n})\n\n// 1 2 3 4\nJust(1, 2, 3, 3, 4, 5, 5).Distinct(func(item interface{}) interface{} {\n  uid := item.(int)\n  // 对大于4的item进行特殊去重逻辑,最终只保留一个\u003e3的item\n  if uid \u003e 3 {\n    return 4\n  }\n  return item\n}).ForEach(func(item interface{}) {\n  t.Log(item)\n})\n```\n\n#### 过滤 Filter\n\n通过将过滤逻辑抽象成 FilterFunc，然后分别作用在 item 上根据 FilterFunc 返回的布尔值决定是否写回新的 channel 中实现过滤功能，实际的过滤逻辑委托给了 Walk method。\n\nOption 参数包含两个选项：\n\n1. unlimitedWorkers 不限制协程数量\n2. workers 限制协程数量\n\n```Go\nFilterFunc func(item interface{}) bool\n\nfunc (s Stream) Filter(filterFunc FilterFunc, opts ...Option) Stream {\n  return s.Walk(func(item interface{}, pip chan\u003c- interface{}) {\n    if filterFunc(item) {\n      pip \u003c- item\n    }\n  }, opts...)\n}\n```\n\n使用示例：\n\n```Go\nfunc TestInternalStream_Filter(t *testing.T) {\n  // 保留偶数 2,4\n  channel := Just(1, 2, 3, 4, 5).Filter(func(item interface{}) bool {\n    return item.(int)%2 == 0\n  }).channel()\n  for item := range channel {\n    t.Log(item)\n  }\n}\n```\n\n#### 遍历执行 Walk\n\nwalk 英文意思是步行，这里的意思是对每个 item 都执行一次 WalkFunc 操作并将结果写入到新的 Stream 中。\n\n这里注意一下因为内部采用了协程机制异步执行读取和写入数据所以新的 Stream 中 channel 里面的数据顺序是随机的。\n\n```Go\n// item-stream中的item元素\n// pipe-item符合条件则写入pipe\nWalkFunc func(item interface{}, pipe chan\u003c- interface{})\n\nfunc (s Stream) Walk(fn WalkFunc, opts ...Option) Stream {\n  option := buildOptions(opts...)\n  if option.unlimitedWorkers {\n    return s.walkUnLimited(fn, option)\n  }\n  return s.walkLimited(fn, option)\n}\n\nfunc (s Stream) walkUnLimited(fn WalkFunc, option *rxOptions) Stream {\n  // 创建带缓冲区的channel\n  // 默认为16,channel中元素超过16将会被阻塞\n  pipe := make(chan interface{}, defaultWorkers)\n  go func() {\n    var wg sync.WaitGroup\n\n    for item := range s.source {\n      // 需要读取s.source的所有元素\n      // 这里也说明了为什么channel最后写完记得完毕\n      // 如果不关闭可能导致协程一直阻塞导致泄漏\n      // 重要, 不赋值给val是个典型的并发陷阱，后面在另一个goroutine里使用了\n      val := item\n      wg.Add(1)\n      // 安全模式下执行函数\n      threading.GoSafe(func() {\n        defer wg.Done()\n        fn(item, pipe)\n      })\n    }\n    wg.Wait()\n    close(pipe)\n  }()\n\n  // 返回新的Stream\n  return Range(pipe)\n}\n\nfunc (s Stream) walkLimited(fn WalkFunc, option *rxOptions) Stream {\n  pipe := make(chan interface{}, option.workers)\n  go func() {\n    var wg sync.WaitGroup\n    // 控制协程数量\n    pool := make(chan lang.PlaceholderType, option.workers)\n\n    for item := range s.source {\n      // 重要, 不赋值给val是个典型的并发陷阱，后面在另一个goroutine里使用了\n      val := item\n      // 超过协程限制时将会被阻塞\n      pool \u003c- lang.Placeholder\n      // 这里也说明了为什么channel最后写完记得完毕\n      // 如果不关闭可能导致协程一直阻塞导致泄漏\n      wg.Add(1)\n\n      // 安全模式下执行函数\n      threading.GoSafe(func() {\n        defer func() {\n          wg.Done()\n          //执行完成后读取一次pool释放一个协程位置\n          \u003c-pool\n        }()\n        fn(item, pipe)\n      })\n    }\n    wg.Wait()\n    close(pipe)\n  }()\n  return Range(pipe)\n}\n```\n\n使用案例：\n\n返回的顺序是随机的。\n\n```Go\nfunc Test_Stream_Walk(t *testing.T) {\n  // 返回 300,100,200\n  Just(1, 2, 3).Walk(func(item interface{}, pip chan\u003c- interface{}) {\n    pip \u003c- item.(int) * 100\n  }, WithWorkers(3)).ForEach(func(item interface{}) {\n    t.Log(item)\n  })\n}\n```\n\n#### 分组 Group\n\n通过对 item 匹配放入 map 中。\n\n```Go\nKeyFunc func(item interface{}) interface{}\n\nfunc (s Stream) Group(fn KeyFunc) Stream {\n  groups := make(map[interface{}][]interface{})\n  for item := range s.source {\n    key := fn(item)\n    groups[key] = append(groups[key], item)\n  }\n  source := make(chan interface{})\n  go func() {\n    for _, group := range groups {\n      source \u003c- group\n    }\n    close(source)\n  }()\n  return Range(source)\n}\n```\n\n#### 获取前 n 个元素 Head\n\nn 大于实际数据集长度的话将会返回全部元素\n\n```Go\nfunc (s Stream) Head(n int64) Stream {\n  if n \u003c 1 {\n    panic(\"n must be greather than 1\")\n  }\n  source := make(chan interface{})\n  go func() {\n    for item := range s.source {\n      n--\n      // n值可能大于s.source长度,需要判断是否\u003e=0\n      if n \u003e= 0 {\n        source \u003c- item\n      }\n      // let successive method go ASAP even we have more items to skip\n      // why we don't just break the loop, because if break,\n      // this former goroutine will block forever, which will cause goroutine leak.\n      // n==0说明source已经写满可以进行关闭了\n      // 既然source已经满足条件了为什么不直接进行break跳出循环呢?\n      // 作者提到了防止协程泄漏\n      // 因为每次操作最终都会产生一个新的Stream,旧的Stream永远也不会被调用了\n      if n == 0 {\n        close(source)\n        break\n      }\n    }\n    // 上面的循环跳出来了说明n大于s.source实际长度\n    // 依旧需要显示关闭新的source\n    if n \u003e 0 {\n      close(source)\n    }\n  }()\n  return Range(source)\n}\n```\n\n使用示例：\n\n```Go\n// 返回1,2\nfunc TestInternalStream_Head(t *testing.T) {\n  channel := Just(1, 2, 3, 4, 5).Head(2).channel()\n  for item := range channel {\n    t.Log(item)\n  }\n}\n```\n\n#### 获取后 n 个元素 Tail\n\n这里很有意思，为了确保拿到最后 n 个元素使用环形切片 Ring 这个数据结构，先了解一下 Ring 的实现。\n\n```Go\n// 环形切片\ntype Ring struct {\n  elements []interface{}\n  index    int\n  lock     sync.Mutex\n}\n\nfunc NewRing(n int) *Ring {\n  if n \u003c 1 {\n    panic(\"n should be greather than 0\")\n  }\n  return \u0026Ring{\n    elements: make([]interface{}, n),\n  }\n}\n\n// 添加元素\nfunc (r *Ring) Add(v interface{}) {\n  r.lock.Lock()\n  defer r.lock.Unlock()\n  // 将元素写入切片指定位置\n  // 这里的取余实现了循环写效果\n  r.elements[r.index%len(r.elements)] = v\n  // 更新下次写入位置\n  r.index++\n}\n\n// 获取全部元素\n// 读取顺序保持与写入顺序一致\nfunc (r *Ring) Take() []interface{} {\n  r.lock.Lock()\n  defer r.lock.Unlock()\n\n  var size int\n  var start int\n  // 当出现循环写的情况时\n  // 开始读取位置需要通过去余实现,因为我们希望读取出来的顺序与写入顺序一致\n  if r.index \u003e len(r.elements) {\n    size = len(r.elements)\n    // 因为出现循环写情况,当前写入位置index开始为最旧的数据\n    start = r.index % len(r.elements)\n  } else {\n    size = r.index\n  }\n  elements := make([]interface{}, size)\n  for i := 0; i \u003c size; i++ {\n    // 取余实现环形读取,读取顺序保持与写入顺序一致\n    elements[i] = r.elements[(start+i)%len(r.elements)]\n  }\n\n  return elements\n}\n```\n\n总结一下环形切片的优点：\n\n- 支持自动滚动更新\n- 节省内存\n\n环形切片能实现固定容量满的情况下旧数据不断被新数据覆盖，由于这个特性可以用于读取 channel 后 n 个元素。\n\n```Go\nfunc (s Stream) Tail(n int64) Stream {\n  if n \u003c 1 {\n    panic(\"n must be greather than 1\")\n  }\n  source := make(chan interface{})\n  go func() {\n    ring := collection.NewRing(int(n))\n    // 读取全部元素，如果数量\u003en环形切片能实现新数据覆盖旧数据\n    // 保证获取到的一定最后n个元素\n    for item := range s.source {\n      ring.Add(item)\n    }\n    for _, item := range ring.Take() {\n      source \u003c- item\n    }\n    close(source)\n  }()\n  return Range(source)\n}\n```\n\n那么为什么不直接使用 len(source) 长度的切片呢?\n\n答案是节省内存。凡是涉及到环形类型的数据结构时都具备一个优点那就省内存，能做到按需分配资源。\n\n使用示例：\n\n```Go\nfunc TestInternalStream_Tail(t *testing.T) {\n  // 4,5\n  channel := Just(1, 2, 3, 4, 5).Tail(2).channel()\n  for item := range channel {\n    t.Log(item)\n  }\n  // 1,2,3,4,5\n  channel2 := Just(1, 2, 3, 4, 5).Tail(6).channel()\n  for item := range channel2 {\n    t.Log(item)\n  }\n}\n```\n\n#### 元素转换Map\n\n元素转换，内部由协程完成转换操作，注意输出channel并不保证按原序输出。\n\n```Go\nMapFunc func(intem interface{}) interface{}\nfunc (s Stream) Map(fn MapFunc, opts ...Option) Stream {\n  return s.Walk(func(item interface{}, pip chan\u003c- interface{}) {\n    pip \u003c- fn(item)\n  }, opts...)\n}\n```\n\n使用示例：\n\n```Go\nfunc TestInternalStream_Map(t *testing.T) {\n  channel := Just(1, 2, 3, 4, 5, 2, 2, 2, 2, 2, 2).Map(func(item interface{}) interface{} {\n    return item.(int) * 10\n  }).channel()\n  for item := range channel {\n    t.Log(item)\n  }\n}\n```\n\n#### 合并 Merge\n\n实现比较简单\n\n```Go\nfunc (s Stream) Merge() Stream {\n  var items []interface{}\n  for item := range s.source {\n    items = append(items, item)\n  }\n  source := make(chan interface{}, 1)\n  source \u003c- items\n  close(source)\n  return Range(source)\n}\n```\n\n#### 反转 Reverse\n\n反转 channel 中的元素。反转算法流程是：\n\n- 找到中间节点\n\n- 节点两边开始两两交换\n\n注意一下为什么获取 s.source 时用切片来接收呢? 切片会自动扩容，用数组不是更好吗? \n\n其实这里是不能用数组的，因为不知道 Stream 写入 source 的操作往往是在协程异步写入的，每个 Stream 中的 channel 都可能在动态变化，用流水线来比喻 Stream 工作流程的确非常形象。\n\n```Go\nfunc (s Stream) Reverse() Stream {\n  var items []interface{}\n  for item := range s.source {\n    items = append(items, item)\n  }\n  for i := len(items)/2 - 1; i \u003e= 0; i-- {\n    opp := len(items) - 1 - i\n    items[i], items[opp] = items[opp], items[i]\n  }\n  return Just(items...)\n}\n```\n\n使用示例：\n\n```Go\nfunc TestInternalStream_Reverse(t *testing.T) {\n  channel := Just(1, 2, 3, 4, 5).Reverse().channel()\n  for item := range channel {\n    t.Log(item)\n  }\n}\n```\n\n#### 排序 Sort\n\n内网调用 slice 官方包的排序方案，传入比较函数实现比较逻辑即可。\n\n```Go\nfunc (s Stream) Sort(less LessFunc) Stream {\n  var items []interface{}\n  for item := range s.source {\n    items = append(items, item)\n  }\n\n  sort.Slice(items, func(i, j int) bool {\n    return less(items[i], items[j])\n  })\n  return Just(items...)\n}\n```\n\n使用示例：\n\n```Go\n// 5,4,3,2,1\nfunc TestInternalStream_Sort(t *testing.T) {\n  channel := Just(1, 2, 3, 4, 5).Sort(func(a, b interface{}) bool {\n    return a.(int) \u003e b.(int)\n  }).channel()\n  for item := range channel {\n    t.Log(item)\n  }\n}\n```\n\n#### 拼接 Concat\n\n```Go\nfunc (s Stream) Concat(steams ...Stream) Stream {\n  // 创建新的无缓冲channel\n  source := make(chan interface{})\n  go func() {\n    // 创建一个waiGroup对象\n    group := threading.NewRoutineGroup()\n    // 异步从原channel读取数据\n    group.Run(func() {\n      for item := range s.source {\n        source \u003c- item\n      }\n    })\n    // 异步读取待拼接Stream的channel数据\n    for _, stream := range steams {\n      // 每个Stream开启一个协程\n      group.Run(func() {\n        for item := range stream.channel() {\n          source \u003c- item\n        }\n      })\n    }\n    // 阻塞等待读取完成\n    group.Wait()\n    close(source)\n  }()\n  // 返回新的Stream\n  return Range(source)\n}\n```\n\n### 汇总 API\n\n#### 全部匹配 AllMatch\n\n```Go\nfunc (s Stream) AllMatch(fn PredicateFunc) bool {\n  for item := range s.source {\n    if !fn(item) {\n      // 需要排空 s.source，否则前面的goroutine可能阻塞\n      go drain(s.source)\n      return false\n    }\n  }\n\n  return true\n}\n```\n\n#### 任意匹配 AnyMatch\n\n```Go\nfunc (s Stream) AnyMatch(fn PredicateFunc) bool {\n  for item := range s.source {\n    if fn(item) {\n      // 需要排空 s.source，否则前面的goroutine可能阻塞\n      go drain(s.source)\n      return true\n    }\n  }\n\n  return false\n}\n```\n\n#### 一个也不匹配 NoneMatch\n\n```Go\nfunc (s Stream) NoneMatch(fn func(item interface{}) bool) bool {\n  for item := range s.source {\n    if fn(item) {\n      // 需要排空 s.source，否则前面的goroutine可能阻塞\n      go drain(s.source)\n      return false\n    }\n  }\n\n  return true\n}\n```\n\n#### 数量统计 Count\n\n```Go\nfunc (s Stream) Count() int {\n  var count int\n  for range s.source {\n    count++\n  }\n  return count\n}\n```\n\n#### 清空 Done\n\n```Go\nfunc (s Stream) Done() {\n  // 排空 channel，防止 goroutine 阻塞泄露\n  drain(s.source)\n}\n```\n\n#### 迭代全部元素 ForAll\n\n```Go\nfunc (s Stream) ForAll(fn ForAllFunc) {\n  fn(s.source)\n}\n```\n\n#### 迭代每个元素 ForEach\n\n```Go\nfunc (s Stream) ForEach(fn ForEachFunc) {\n  for item := range s.source {\n    fn(item)\n  }\n}\n```\n\n## 小结\n\n至此 Stream 组件就全部实现完了，核心逻辑是利用 channel 当做管道，数据当做水流，不断的用协程接收/写入数据到 channel 中达到异步非阻塞的效果。\n\n实现高效的基础来源三个语言特性：\n\n- channel\n- 协程\n- 函数式编程\n\n## 强烈推荐！\n\ngo-zero: [https://github.com/zeromicro/go-zero](https://github.com/zeromicro/go-zero)\n\n## 欢迎 star！⭐\n\n如果你正在使用或者觉得这个项目对你有帮助，请 **star** 支持，感谢！\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkevwan%2Fstream","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkevwan%2Fstream","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkevwan%2Fstream/lists"}