本文介绍了在 Go 中处理集合的推荐库,以及如何基于 Go 标准库中的 slices 和 maps 包来操作集合,并进一步介绍了如何通过第三方库(如pie,lo)进行更复杂的集合操作。原文: Working with Collections in Go

集合是构建任何应用程序的重要组成部分,对于集合来说,常见的有以下几类操作:

  • 转换(transform) - 对集合中的每个元素应用某种函数以创建新类型集合的操作;
  • 过滤(filter) - 从集合中选择满足特定条件的元素的操作;
  • 聚合(aggregation) - 从集合中计算出单一结果的操作,通常用于汇总;
  • 分类(sorting)/排序(ordering) - 根据某些标准重新排列集合元素的操作;
  • 访问(access) - 根据元素属性或位置检索元素的操作;
  • 通用(utility) - 与集合有关的通用操作,但不一定能够完全归入某个类别。

尽管 Go 有很多优点,但它对高级集合操作的内置支持相对有限,因此在需要的时候有必要使用第三方软件包。本文将探讨几种流行的 Go 库的特性和功能,它们增强了 Go 高效处理集合的能力,从而帮助读者选择合适的工具,简化 Go 项目中的数据处理任务。

导言

我们回顾一下上述每个集合操作的常用方法。

转换(transform)
  • Map - 对集合中的每个元素应用一个函数,并返回结果集合;
  • FlatMap - 将每个元素处理为元素列表,然后将所有列表扁平化为单个列表。
过滤(filter)
  • Filter - 删除与过滤函数不匹配的元素;
  • Distinct - 删除集合中的重复元素;
  • TakeWhile - 从头开始返回满足给定条件的元素;
  • DropWhile - 从头开始删除符合给定条件的元素,返回剩余部分。
聚合(aggregation)
  • Reduce - 使用给定函数合并集合中的所有元素,并返回合并结果;
  • Count - 返回满足特定条件的元素数量;
  • Sum - 计算集合中每个元素的数值属性总和;
  • Max/Min - 确定元素属性的最大或最小值;
  • Average - 计算集合中元素数值属性的平均值。
分类(sorting)/排序(ordering)
  • Sort - 根据比较规则对集合中的元素进行排序;
  • Reverse - 逆序排列集合中的元素。
访问(access)
  • Find - 返回与查询函数匹配的第一个元素;
  • AtIndex - 检索特定索引位置的元素。
通用(utility)
  • GroupBy - 根据索引键生成器函数将元素分类;
  • Partition - 根据分类函数将一个集合分成两个集合:一个是满足分类函数的元素集合,另一个是不满足分类函数的元素集合;
  • Slice Operations - 像切片或分块这样的操作,可以自定义查看或分割集合的方法。

Go 内置功能

在 Go 中,有几种类型可以处理数据集合:

  • Arrays(数组) - 固定大小的元素集合。数组大小在声明时定义 var myArray [5]int
  • Slices(切片) - 动态大小的元素集合。切片建立在数组之上,但与数组不同的是,它们可以增大或缩小。声明:mySlice = []int{1, 2, 3}
  • Maps(映射) - 键值对的集合。map 可以动态增长,但不保证键的顺序。myMap := map[string]int{"first":1, "second":2} 创建了一个键为字符串、值为整数的 map。
  • Channels(通道) - 强类型通信原语,允许在程序之间共享数据。myChan := make(chan int) 创建了一个用于传输整数的通道。

Go 标准库提供了可充当或增强集合的其他数据结构及工具,例如:

  • Heap - container/heap包为任何sort.Interface接口提供堆操作。堆是一棵树,其属性是每个节点都是其子树中的最小值节点;
  • Listcontainer/list包实现了双向链表;
  • Ringcontainer/ring包实现了对循环列表的操作。

作为 Go 标准库的一部分,还有用于处理切片和映射的软件包:

  • slices - 该软件包定义了对任何类型的切片都可用的各种函数;
  • maps - 该软件包定义了对任何类型的映射都有用的各种函数。

通过内置功能,可以对集合进行如下操作:

  • 获取数组/切片/映射的长度;
  • 通过索引/键访问元素,"切片"一个片段;
  • 遍历元素。
package main

import "fmt"

func main() {
 s := []int{1, 2, 3, 4, 5}
 m := map[int]string{1: "one", 2: "two", 3: "three"}

 fmt.Printf("len(s)=%d\n", len(s))
 fmt.Printf("len(m)=%d\n", len(m))
 fmt.Printf("cap(s)=%d\n", cap(s))
 // fmt.Printf("cap(m)=%d\n", cap(m)) // error: invalid argument m (type map[int]string) for cap

 // panic: runtime error: index out of range [5] with length 5
 // fmt.Printf("s[5]=%d\n", s[5])

 // panic: runtime error: index out of range [5] with length 5
 // s[5] = 6

 s = append(s, 6)
 fmt.Printf("s=%v\n", s)
 fmt.Printf("len(s)=%d\n", len(s))
 fmt.Printf("cap(s)=%d\n", cap(s))

 m[4] = "four"
 fmt.Printf("m=%v\n", m)

 fmt.Printf("s[2:4]=%v\n", s[2:4])
 fmt.Printf("s[2:]=%v\n", s[2:])
 fmt.Printf("s[:2]=%v\n", s[:2])
 fmt.Printf("s[:]=%v\n", s[:])
}

该代码将会打印如下内容:

len(s)=5
len(m)=3
cap(s)=5
s=[1 2 3 4 5 6]
len(s)=6
cap(s)=10
m=map[1:one 2:two 3:three 4:four]
s[2:4]=[3 4]
s[2:]=[3 4 5 6]
s[:2]=[1 2]
s[:]=[1 2 3 4 5 6]

让我们回顾一下内置库的功能!

切片

从 Go 1.21 开始,Go 标准库中出现了slices包,这是 Go 语言向前迈出的重要一步,但我仍然更喜欢使用外部库来处理集合(你很快就会明白为什么)。让我们回顾一下该库是如何支持所有集合操作类的。

支持的集合

切片

转换

本软件包不支持此类操作。

过滤

本软件包不支持此类操作。

聚合

slices可以查找切片中的最小/最大值:

package main

import (
 "fmt"
 "slices"
)

type Example struct {
 Name   string
 Number int
}

func main() {
 s := []int{1, 2, 3, 4, 5}

 fmt.Printf("Min: %d\n", slices.Min(s))
 fmt.Printf("Max: %d\n", slices.Max(s))

 e := []Example{
  {"A", 1},
  {"B", 2},
  {"C", 3},
  {"D", 4},
 }

 fmt.Printf("Min: %v\n", slices.MinFunc(
  e,
  func(i, j Example) int {
   return i.Number - j.Number
  }),
 )

 fmt.Printf("Max: %v\n", slices.MaxFunc(
  e,
  func(i, j Example) int {
   return i.Number - j.Number
  }),
 )
}

上述代码将打印:

Min: 1
Max: 5
Min: {A 1}
Max: {D 4}

不支持其他聚合。

分类/排序

slices可以使用比较函数对切片进行排序:

package main

import (
 "fmt"
 "slices"
)

type Example struct {
 Name   string
 Number int
}

func main() {
 s := []int{4, 2, 5, 1, 3}

 slices.Sort(s)
 fmt.Printf("Sorted: %v\n", s)

 slices.Reverse(s)
 fmt.Printf("Reversed: %v\n", s)

 e := []Example{
  {"C", 3},
  {"A", 1},
  {"D", 4},
  {"B", 2},
 }

 slices.SortFunc(e, func(a, b Example) int {
  return a.Number - b.Number
 })

 fmt.Printf("Sorted: %v\n", e)

 slices.Reverse(e)
 fmt.Printf("Reversed: %v\n", e)
}

上述代码将打印:

Sorted: [1 2 3 4 5]
Reversed: [5 4 3 2 1]
Sorted: [{A 1} {B 2} {C 3} {D 4}]
Reversed: [{D 4} {C 3} {B 2} {A 1}]

对我来说,最大的缺点是排序会修改原始切片。如果该方法能返回一个新的排序切片,从而保留原始数组,那就更好了。

访问

slices提供了几个方法,可以访问元素在切片中的位置:

package main

import (
 "fmt"
 "slices"
)

type Example struct {
 Name   string
 Number int
}

func main() {
 s := []int{4, 2, 5, 1, 3}

 i := slices.Index(s, 3)
 fmt.Printf("Index of 3: %d\n", i)

 e := []Example{
  {"C", 3},
  {"A", 1},
  {"D", 4},
  {"B", 2},
 }

 i = slices.IndexFunc(e, func(a Example) bool {
  return a.Number == 3
 })

 fmt.Printf("Index of 3: %d\n", i)
}

上述代码将打印:

Index of 3: 4
Index of 3: 0

如果要处理已排序的切片,可以使用BinarySearchBinarySearchFunc在已排序切片中搜索目标,并返回找到目标的位置或目标在排序顺序中出现的位置。同时它还返回一个布尔值,表示是否在片段中找到目标。切片必须按递增顺序排序。

package main

import (
 "fmt"
 "slices"
)

func main() {
 s := []int{4, 2, 5, 1, 3}

 slices.Sort(s)

 i, found := slices.BinarySearch(s, 3)
 fmt.Printf("Position of 3: %d. Found: %t\n", i, found)

 i, found = slices.BinarySearch(s, 6)
 fmt.Printf("Position of 6: %d. Found: %t\n", i, found)
}

上述代码将打印:

Position of 3: 2. Found: true
Position of 6: 5. Found: false

通用

slices提供了多种通用函数:

package main

import (
 "fmt"
 "slices"
)

type Example struct {
 Name   string
 Number int
}

func main() {
 e1 := []Example{
  {"C", 3},
  {"A", 1},
  {"D", 4},
  {"B", 2},
 }

 e2 := []Example{
  {"A", 1},
  {"B", 2},
  {"C", 3},
  {"D", 4},
 }

 fmt.Printf("Compare: %v\n", slices.CompareFunc(e1, e2, func(a, b Example) int {
  return a.Number - b.Number
 }))

 fmt.Printf("Contains: %v\n", slices.ContainsFunc(e1, func(a Example) bool {
  return a.Number == 2
 }))

 fmt.Printf("Delete: %v\n", slices.Delete(e1, 2, 3))
 fmt.Printf("Equal: %v\n", slices.Equal(e1, e2))

 fmt.Printf("Is Sorted: %v\n", slices.IsSortedFunc(e1, func(a, b Example) int {
  return a.Number - b.Number
 }))
}

上述代码将打印:

Compare: 2
Contains: true
Delete: [{C 3} {A 1} {B 2}]
Equal: false
Is Sorted: false

文档

https://pkg.go.dev/slices

结论

在 Go 1.21 中引入的slices软件包代表了在处理切片方面的重大改进。然而,这个软件包还缺乏很多重要功能,由于省略了一些更高级的集合操作,因此仍需要第三方库来满足更复杂的数据处理需求。

映射

slices类似,maps从 Go 1.21 开始出现在 Go 标准库中。正如所期望的那样,它定义了各种操作映射的方法。

支持的集合

映射

转换

本软件包不支持此类操作。

过滤

本软件包不支持此类操作。

汇聚

本软件包不支持此类操作。

分类/排序

本软件包不支持此类操作。

访问

本软件包不支持此类操作。

通用

本软件包支持一组通用操作:

package main

import (
 "fmt"
 "maps"
)

func main() {
 m := map[int]string{1: "one", 2: "two", 3: "three"}
 c := maps.Clone(m)

 c[4] = "four"

 fmt.Printf("Original: %v\n", m)
 fmt.Printf("Clone: %v\n", c)

 maps.DeleteFunc(c, func(k int, v string) bool { return k%2 == 0 })
 fmt.Printf("DeleteFunc: %v\n", c)

 fmt.Printf("Equal: %v\n", maps.Equal(m, c))
 fmt.Printf("EqualFunc: %v\n", maps.EqualFunc(m, c, func(v1, v2 string) bool { return v1 == v2 }))
}

上述代码将打印:

Original: map[1:one 2:two 3:three]
Clone: map[1:one 2:two 3:three 4:four]
DeleteFunc: map[1:one 3:three]
Equal: false
EqualFunc: false

文档

https://pkg.go.dev/maps

结论

maps包的功能比slices包更加有限。因此,如果需要对映射进行更复杂的操作,几乎肯定需要依赖第三方库。

github.com/elliotchance/pie

这是我个人最喜欢的处理切片和映射的软件包。它提供了独特的语法,能让我们无缝进行链式操作,提高代码的可读性和效率。

有四种方式使用该库:

  • 纯调用 - 只需调用提供所需参数的库方法;
  • pie.Of - 对任何元素类型进行链式多重操作;
  • pie.OfOrdered - 对数字和字符串类型进行链式多重操作;
  • pie.OfNumeric - 仅对数字进行链式多重操作。
package main

import (
 "fmt"
 "strings"

 "github.com/elliotchance/pie/v2"
)

type Example struct {
 Name   string
 Number int
}

func main() {
 e := []Example{
  {"C", 3},
  {"A", 1},
  {"D", 4},
  {"B", 2},
 }

 fmt.Printf(
  "Map 1: %v\n",
  pie.Sort(
   pie.Map(
    e,
    func(e Example) string {
     return e.Name
    },
   ),
  ),
 )

 fmt.Printf(
  "Map 2: %v\n",
  pie.Of(e).
   Map(func(e Example) Example {
    return Example{
     Name:   e.Name,
     Number: e.Number * 2,
    }
   }).
   SortUsing(func(a, b Example) bool {
    return a.Number < b.Number
   }),
 )

 fmt.Printf(
  "Map 3: %v\n",
  pie.OfOrdered([]string{"A", "C", "B", "A"}).
   Map(func(e string) string {
    return strings.ToLower(e)
   }).
   Sort(),
 )

 fmt.Printf(
  "Map 4: %v\n",
  pie.OfNumeric([]int{4, 1, 3, 2}).
   Map(func(e int) int {
    return e * 2
   }).
   Sort(),
 )
}

上述代码将打印:

Map 1: [A B C D]
Map 2: {[{A 2} {B 4} {C 6} {D 8}]}
Map 3: {[a a b c]}
Map 4: {[2 4 6 8]}

该库的链式操作非常有限,因为Map等函数应返回相同类型的集合,因此我认为纯方法调用是使用该库的最佳方式。

支持的集合

切片、映射

转换

库中的Map方法允许将每个元素从一种类型转换为另一种类型:

package main

import (
 "fmt"

 "github.com/elliotchance/pie/v2"
)

type Example struct {
 Name   string
 Number int
}

func main() {
 e := []Example{
  {"C", 3},
  {"A", 1},
  {"D", 4},
  {"B", 2},
 }

 fmt.Printf(
  "Map: %v\n",
  pie.Map(
   e,
   func(e Example) string {
    return e.Name
   },
  ),
 )
}

上述代码将打印:

Map: [C A D B]

此外,还可以找到将二维切片转换为单维的方法Flat

package main

import (
 "fmt"

 "github.com/elliotchance/pie/v2"
)

type Person struct {
 Name string
 Tags []string
}

func main() {
 p := []Person{
  {"Alice", []string{"a", "b", "c"}},
  {"Bob", []string{"b", "c", "d"}},
  {"Charlie", []string{"c", "d", "e"}},
 }

 fmt.Printf(
  "Unique Tags: %v\n",
  pie.Unique(
   pie.Flat(
    pie.Map(
     p,
     func(e Person) []string {
      return e.Tags
     },
    ),
   ),
  ),
 )
}

上述代码将打印:

Unique Tags: [b c d e a]

使用KeysValues方法可以只获取映射的键或值:

package main

import (
 "fmt"

 "github.com/elliotchance/pie/v2"
)

func main() {
 m := map[int]string{
  1: "one",
  2: "two",
  3: "three",
 }

 fmt.Printf("Keys: %v\n", pie.Keys(m))
 fmt.Printf("Values: %v\n", pie.Values(m))
}

上述代码将打印:

Keys: [3 1 2]
Values: [one two three]

过滤

库提供了多种过滤原始集合的方法:BottomDropTopDropWhileFilterFilterNotUnique等。

package main

import (
 "fmt"

 "github.com/elliotchance/pie/v2"
)

func main() {
 v := []int{1, 2, 2, 3, 3, 3, 4, 4, 4, 4}

 fmt.Printf("Bottom 3: %v\n", pie.Bottom(v, 3))
 fmt.Printf("Drop top 3: %v\n", pie.DropTop(v, 3))
 fmt.Printf("Drop while 3: %v\n", pie.DropWhile(v, func(value int) bool { return value < 3 }))
 fmt.Printf("Filter even: %v\n", pie.Filter(v, func(value int) bool { return value%2 == 0 }))
 fmt.Printf("Filter not even: %v\n", pie.FilterNot(v, func(value int) bool { return value%2 == 0 }))
 fmt.Printf("Unique values: %v\n", pie.Unique(v))
}

上述代码将打印:

Bottom 3: [4 4 4]
Drop top 3: [3 3 3 4 4 4 4]
Drop while 3: [3 3 3 4 4 4 4]
Filter even: [2 2 4 4 4 4]
Filter not even: [1 3 3 3]
Unique values: [1 2 3 4]

聚合

有一种通用方法可以进行任何类型的聚合Reduce。我们来计算标准差:

package main

import (
 "fmt"
 "math"

 "github.com/elliotchance/pie/v2"
)

func main() {
 v := []float64{1.1, 2.2, 3.3, 4.4, 5.5}

 avg := pie.Average(v)
 count := len(v)

 sum2 := pie.Reduce(
  v,
  func(acc, value float64) float64 {
   return acc + (value-avg)*(value-avg)
  },
 ) - v[0] + (v[0]-avg)*(v[0]-avg)

 d := math.Sqrt(sum2 / float64(count))

 fmt.Printf("Standard deviation: %f\n", d)
}

上述代码将打印:

Standard deviation: 1.555635

Reduce方法第一次调用reducer时,第一个切片元素是累积值,第二个元素是值参数。这就是公式如此奇怪的原因。

从下面的示例中,可以找到另一种内置聚合方法Average。此外,还可以找到MinMaxProduct等方法:

package main

import (
 "fmt"

 "github.com/elliotchance/pie/v2"
)

func main() {
 v := []float64{1.1, 2.2, 3.3, 4.4, 5.5}

 fmt.Printf("Average: %f\n", pie.Average(v))
 fmt.Printf("Stddev: %f\n", pie.Stddev(v))
 fmt.Printf("Max: %f\n", pie.Max(v))
 fmt.Printf("Min: %f\n", pie.Min(v))
 fmt.Printf("Sum: %f\n", pie.Sum(v))
 fmt.Printf("Product: %f\n", pie.Product(v))

 fmt.Printf("All >0: %t\n", pie.Of(v).All(func(value float64) bool { return value > 0 }))
 fmt.Printf("Any >5: %t\n", pie.Of(v).Any(func(value float64) bool { return value > 5 }))

 fmt.Printf("First: %f\n", pie.First(v))
 fmt.Printf("Last: %f\n", pie.Last(v))

 fmt.Printf("Are Unique: %t\n", pie.AreUnique(v))
 fmt.Printf("Are Sorted: %t\n", pie.AreSorted(v))
 fmt.Printf("Contains 3.3: %t\n", pie.Contains(v, 3.3))
}

上述代码将打印:

Average: 3.300000
Stddev: 1.555635
Max: 5.500000
Min: 1.100000
Sum: 16.500000
Product: 193.261200
All >0: true
Any >5: true
First: 1.100000
Last: 5.500000
Are Unique: true
Are Sorted: true
Contains 3.3: true

分类/排序

pie提供了三种不同的方法来对切片进行分类:

  • Sort - 工作原理类似于sort.Slice,但与sort.Slice不同的是,返回的切片会重新分配内存,以避免修改输入切片;
  • SortStableUsing - 工作原理类似于sort.SliceStable。不过,与sort.SliceStable不同的是,返回的切片会重新分配内存,以避免修改输入切片。
  • SortUsing - 工作原理类似于sort.Slice。但是,与sort.Slice不同的是,返回的切片会重新分配内存,以避免修改输入切片。
package main

import (
 "fmt"

 "github.com/elliotchance/pie/v2"
)

func main() {
 v := []int{3, 5, 1, 4, 2}

 less := func(a, b int) bool {
  return a < b
 }

 fmt.Printf("Sort: %v\n", pie.Sort(v))
 fmt.Printf("SortStableUsing: %v\n", pie.SortStableUsing(v, less))
 fmt.Printf("SortUsing: %v\n", pie.SortUsing(v, less))
 fmt.Printf("Original: %v\n", v)
}

上述代码将会打印:

Sort: [1 2 3 4 5]
SortStableUsing: [1 2 3 4 5]
SortUsing: [1 2 3 4 5]
Original: [3 5 1 4 2]

访问

pie公开了FindFirstUsing方法,用于获取切片中第一个匹配元素的索引:

package main

import (
 "fmt"

 "github.com/elliotchance/pie/v2"
)

type Person struct {
 Name string
 Age  int
}

func main() {
 p := []Person{
  {"Alice", 25},
  {"Bob", 30},
  {"Charlie", 35},
 }

 fmt.Printf(
  "FindFirstUsing: %v\n",
  pie.FindFirstUsing(
   p,
   func(p Person) bool {
    return p.Age >= 30
   },
  ),
 )

}

上述代码将打印:

FindFirstUsing: 2

通用

pie包含大量用于处理切片的实用方法。仅举几例:

package main

import (
 "fmt"
 "math/rand"
 "time"

 "github.com/elliotchance/pie/v2"
)

type Person struct {
 Name string
 Age  int
}

func main() {
 p := []Person{
  {"Alice", 25},
  {"Bob", 30},
  {"Charlie", 35},
  {"David", 25},
  {"Eve", 40},
  {"Frank", 35},
 }

 fmt.Printf("Chunk: %v\n", pie.Chunk(p, 2))
 fmt.Printf("GroupBy: %v\n", pie.GroupBy(p, func(p Person) int { return p.Age }))
 fmt.Printf("Shuffle: %v\n", pie.Shuffle(p, rand.New(rand.NewSource(time.Now().UnixNano()))))
}

上述代码将打印:

Chunk: [[{Alice 25} {Bob 30}] [{Charlie 35} {David 25}] [{Eve 40} {Frank 35}]]
GroupBy: map[25:[{Alice 25} {David 25}] 30:[{Bob 30}] 35:[{Charlie 35} {Frank 35}] 40:[{Eve 40}]]
Shuffle: [{Frank 35} {Bob 30} {David 25} {Eve 40} {Alice 25} {Charlie 35}]

文档

https://github.com/elliotchance/pie
https://pkg.go.dev/github.com/elliotchance/pie/v2

结论

elliotchance/pie/v2库提供了一套令人印象深刻的功能,极大简化了 Go 语言中的切片处理,其用于操作和查询切片数据的强大方法为开发人员提供了强大工具,提高了代码的可读性和效率。强烈建议所有 Go 开发人员在下一个项目中尝试使用该库。

github.com/samber/lo

这是另一个在 Go 中处理集合的流行库。它在某些方面很像流行的 JavaScript 库Lodash。该库基于泛型实现,而不是反射。

支持的集合

切片、映射、通道

转换

该库支持用于切片的MapFlatMap默认方法:

package main

import (
 "fmt"

 "github.com/samber/lo"
)

type Example struct {
 Name   string
 Number int
}

func main() {
 e := []Example{
  {"C", 3},
  {"A", 1},
  {"D", 4},
  {"B", 2},
 }

 fmt.Printf(
  "Map: %v\n",
  lo.Map(
   e,
   func(e Example, index int) string {
    return e.Name
   },
  ),
 )
}

上述代码将打印:

Map: [C A D B]

下一个示例展示了FlatMap的使用:

下一个示例展示了 FlatMap 如何工作:

package main

import (
 "fmt"

 "github.com/samber/lo"
)

type Person struct {
 Name string
 Tags []string
}

func main() {
 p := []Person{
  {"Alice", []string{"a", "b", "c"}},
  {"Bob", []string{"b", "c", "d"}},
  {"Charlie", []string{"c", "d", "e"}},
 }

 fmt.Printf(
  "Unique Tags: %v\n",
  lo.Uniq(
   lo.FlatMap(
    p,
    func(e Person, index int) []string {
     return e.Tags
    },
   ),
  ),
 )
}

上述代码将打印:

Unique Tags: [a b c d e]

此外还可以获取映射键、值或将映射转换到切片等:

package main

import (
 "fmt"
 "strings"

 "github.com/samber/lo"
)

func main() {
 m := map[int]string{
  1: "one",
  2: "two",
  3: "three",
 }

 fmt.Printf("Keys: %v\n", lo.Keys(m))
 fmt.Printf("Values: %v\n", lo.Values(m))
 fmt.Printf("MapKeys: %v\n", lo.MapKeys(m, func(value string, num int) int { return num * 2 }))
 fmt.Printf("MapValues: %v\n", lo.MapValues(m, func(value string, num int) string { return strings.ToUpper(value) }))
 fmt.Printf("MapToSlice: %v\n", lo.MapToSlice(m, func(num int, value string) string { return value + ":" + fmt.Sprint(num) }))
}

上述代码将打印:

Keys: [2 3 1]
Values: [one two three]
MapKeys: map[2:one 4:two 6:three]
MapValues: map[1:ONE 2:TWO 3:THREE]
MapToSlice: [three:3 one:1 two:2]

过滤

lo库中有许多Drop方法:

package main

import (
 "fmt"

 "github.com/samber/lo"
)

func main() {
 v := []int{1, 2, 3, 4, 5}

 fmt.Printf("Drop: %v\n", lo.Drop(v, 2))
 fmt.Printf("DropRight: %v\n", lo.DropRight(v, 2))
 fmt.Printf("DropWhile: %v\n", lo.DropWhile(v, func(i int) bool { return i < 3 }))
 fmt.Printf("DropRightWhile: %v\n", lo.DropRightWhile(v, func(i int) bool { return i > 3 }))
}

上述代码将打印:

Drop: [3 4 5]
DropRight: [1 2 3]
DropWhile: [3 4 5]
DropRightWhile: [1 2 3]

此外,还可以通过匿名函数过滤切片和映射:

package main

import (
 "fmt"

 "github.com/samber/lo"
)

func main() {
 v := []int{1, 2, 3, 4, 5}
 m := map[string]int{"a": 1, "b": 2, "c": 3}

 fmt.Printf("Filter: %v\n", lo.Filter(v, func(i int, index int) bool { return i > 2 }))
 fmt.Printf("PickBy: %v\n", lo.PickBy(m, func(key string, value int) bool { return value > 2 }))
}

上述代码将打印:

Filter: [3 4 5]
PickBy: map[c:3]

聚合

loReduce方法暴露给聚合切片:

package main

import (
 "fmt"
 "math"

 "github.com/samber/lo"
)

func main() {
 v := []float64{1.1, 2.2, 3.3, 4.4, 5.5}

 count := len(v)

 avg := lo.Reduce(v, func(acc, val float64, index int) float64 {
  return acc + val
 }, 0.0) / float64(count)

 sum2 := lo.Reduce(v, func(acc, val float64, index int) float64 {
  return acc + (val-avg)*(val-avg)
 }, 0.0)

 d := math.Sqrt(sum2 / float64(count))

 fmt.Printf("Standard deviation: %f\n", d)
}

上述代码将打印:

Standard deviation: 1.555635

此外,该库还支持一些通用聚合方法,如SumMinMax

package main

import (
 "fmt"

 "github.com/samber/lo"
)

func main() {
 v := []float64{1.1, 2.2, 3.3, 4.4, 5.5}

 fmt.Printf("Sum: %v\n", lo.Sum(v))
 fmt.Printf("Min: %v\n", lo.Min(v))
 fmt.Printf("Max: %v\n", lo.Max(v))
}

上述代码将打印:

Standard deviation: 1.555635

有一些有用的方法可以使用通道:FanInFanOut

package main

import (
 "fmt"

 "github.com/samber/lo"
)

func main() {
 ch1 := make(chan int)
 ch2 := make(chan int)
 ch3 := make(chan int)

 ch := lo.FanIn(10, ch1, ch2, ch3)

 for i := 0; i < 10; i++ {
  if i%3 == 0 {
   ch1 <- i
  } else if i%3 == 1 {
   ch2 <- i
  } else {
   ch3 <- i
  }
 }

 close(ch1)
 close(ch2)
 close(ch3)

 for v := range ch {
  fmt.Println(v)
 }
}

上述代码将打印:

0
1
2
5
3
6
4
7
8
9

再举一个例子:

package main

import (
 "fmt"

 "github.com/samber/lo"
)

func main() {
 ch := make(chan int)
 chs := lo.FanOut(3, 10, ch)

 for i := 0; i < 3; i++ {
  ch <- i
 }

 close(ch)

 for _, ch := range chs {
  for v := range ch {
   fmt.Println(v)
  }
 }
}

上述代码将打印:

0
1
2
0
1
2
0
1
2

分类/排序

lo仅支持Reverse方法:

package main

import (
 "fmt"

 "github.com/samber/lo"
)

func main() {
 v := []int{1, 2, 3, 4, 5}

 fmt.Printf("Reverse: %v\n", lo.Reverse(v))
}

上述代码将打印:

Reverse: [5 4 3 2 1]

访问

可以找到几种在切片中查找元素的方法:

package main

import (
 "fmt"

 "github.com/samber/lo"
)

type Person struct {
 Name string
 Age  int
}

func main() {
 p := []Person{
  {"Alice", 25},
  {"Bob", 30},
  {"Charlie", 35},
  {"David", 25},
  {"Edward", 40},
 }

 item, found := lo.Find(p, func(p Person) bool {
  return p.Name == "Charlie"
 })

 fmt.Printf("Item: %+v, Found: %v\n", item, found)

 fmt.Printf("FindDuplicatesBy: %v\n", lo.FindDuplicatesBy(p, func(p Person) int {
  return p.Age
 }))

 item, index, found := lo.FindIndexOf(p, func(p Person) bool {
  return p.Name == "Charlie"
 })

 fmt.Printf("Item: %+v, Index: %v, Found: %v\n", item, index, found)
}

上述代码将打印:

Item: {Name:Charlie Age:35}, Found: true
FindDuplicatesBy: [{Alice 25}]
Item: {Name:Charlie Age:35}, Index: 2, Found: true

此外,还可以找到支持映射的方法:

package main

import (
 "fmt"

 "github.com/samber/lo"
)

func main() {
 p := map[string]int{
  "Alice":   34,
  "Bob":     24,
  "Charlie": 34,
  "David":   29,
  "Eve":     34,
 }

 key, found := lo.FindKey(p, 34)
 fmt.Printf("Key: %v, Found: %v\n", key, found)
}

由于映射是无序结构,因此结果无法预测,可能会看到如下输出:

Key: Charlie, Found: true
Key: Alice, Found: true
Key: Eve, Found: true

通用

可以在lo中找到大量方法。举几个例子:

package main

import (
 "fmt"

 "github.com/samber/lo"
)

func main() {
 v1 := []int{1, 2, 3, 4, 5}
 v2 := []int{3, 4, 5, 6, 7}

 fmt.Printf("Chunk: %v\n", lo.Chunk(v1, 3))
 fmt.Printf("Intersect: %v\n", lo.Intersect(v1, v2))
 fmt.Printf("Union: %v\n", lo.Union(v1, v2))

 diff1, diff2 := lo.Difference(v1, v2)
 fmt.Printf("Difference: %v, %v\n", diff1, diff2)
}

上述代码将打印:

Chunk: [[1 2 3] [4 5]]
Intersect: [3 4 5]
Union: [1 2 3 4 5 6 7]
Difference: [1 2], [6 7]

文档

https://github.com/samber/lo
https://pkg.go.dev/github.com/samber/lo

结论

上面只演示了github.com/samber/lo库中约 10% 的方法,还有很多其他工具可以简化函数的使用,该库是 Go 开发人员的综合工具包,是优化 Go 开发工作流程的宝贵财富。

结论

Go 生态系统拥有丰富的工具,可以简化和扩展数据操作的功能。本文介绍了不同的方法来对切片、映射和通道执行不同的操作。库的选择主要取决于项目的具体要求和开发人员对某些编程风格的偏好。


你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

俞凡
13 声望11 粉丝

你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起...