Working with Collections in Go
Recommended Libraries for Working with Collections in Go
Working with collections is an essential part of building any application. Typically, you need the next classes of operations:
- transform — operations that apply some function to each element of the collection to create a new collection of a new type;
- filter — operations that select elements from the collection that satisfy a particular condition;
- aggregation — operations that compute a single result from a collection, typically used for summaries;
- sorting/ordering — operations that involve rearranging the elements of the collection according to some criteria;
- access — operations that retrieve elements based on their properties or position;
- utility — general purpose operations that work with collections but don’t necessarily fit neatly into one category.
Despite its many advantages, Go has relatively limited built-in support for advanced collection operations, so if you need it, you need to use a third-party package. In this post, I will explore several popular Go libraries that enhance the language’s capabilities to work with collections efficiently, covering their features and functionality. This review will help you choose the right tools to streamline your data handling tasks in Go projects.
Introduction
Let’s review popular methods from each collection operation class above.
Transform
- Map — applies a function to each element in a collection and returns a collection of the results;
- FlatMap —Processes each element into a list of elements, then flattens the lists into a single list.
Filter
- Filter — removes elements that do not match a predicate function;
- Distinct — removes duplicate elements from the collection;
- TakeWhile — returns elements from the beginning as long as they satisfy a given condition;
- DropWhile — removes elements from the beginning as long as they satisfy a given condition, then returns the remainder.
Aggregation
- Reduce — combines all elements of the collection using a given function and returns the combined result;
- Count — returns the number of elements that satisfy a particular condition;
- Sum — calculates the sum of a numeric property for each element in the collection;
- Max/Min — determines the maximum or minimum value among an attribute of the elements;
- Average — computes the average of a numeric property for the elements in the collection.
Sorting/Ordering
- Sort — orders the elements of the collection based on comparator rules;
- Reverse — reverses the order of the elements in the collection.
Access
- Find — returns the first element matching a predicate;
- AtIndex — retrieves the element at a specific index.
Utility
- GroupBy — categorizes elements into groups based on a key generator function;
- Partition — divides a collection into two collections based on a predicate: one for elements that satisfy the predicate and one for those that don’t;
- Slice Operations — actions like slicing or chunking that modify how a collection is viewed or divided.
Go Built-In Capabilities
In Go, there are several types to work with data collections:
- Arrays — a fixed-size collection of elements. Array size is defined during declaration
var myArray [5]int
; - Slices — a dynamic-size collection of elements. Slices are built on top of arrays, but unlike array they can grow or shrink. Declaration:
mySlice = []int{1, 2, 3}
; - Maps — a collection of key-value pairs. Maps can grow dynamically, and the order of keys is not guaranteed.
myMap := map[string]int{“first”: 1, “second”: 2}
creates a map with string keys and integer values. - Channels — typed communication primitive that allows sharing of data between goroutines.
myChan := make(chan int)
creates a channel for transmitting integers.
Go standard library provides additional structures and utilities that can act as or enhance collections, such as:
- Heap — The
container/heap
package provides heap operations for any sort.Interface. A heap is a tree with the property that each node is the minimum-valued node in its subtree; - List — The
container/list
package implements a doubly linked list; - Ring — The
container/ring
package implements operations on circular lists.
Also as part of Go standard library, there are packages to work with slices and maps:
slices
— package defines various functions useful with slices of any type;maps
— package defines various functions useful with maps of any type.
With built-in functionality, you’re able to do some manipulations with collections:
- get length of array/slice/map;
- access element by index/key, “slice” a slice;
- iterate through items.
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[:])
}
It will print next:
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]
Let’s review built-in library capabilities!
Slices
Package slices
appeared in Go standard library just recently starting with Go 1.21. Significant step forward in the language, but still, I prefer to use external libraries to work with collections (you’ll quickly understand why). Let’s review how this library supports all collection operation classes.
Supported collections
Slices.
Transform
This package does not provide support for this category of operations.
Filter
This package does not provide support for this category of operations.
Aggregation
slices
allows you to find min/max values in the slice:
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
}),
)
}
It will print next:
Min: 1
Max: 5
Min: {A 1}
Max: {D 4}
Other aggregations are not supported.
Sorting/Ordering
slices
allows you to sort slices with a comparer function:
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)
}
It will print next:
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}]
For me, the most significant disadvantage is that the sorting occurs in-place, modifying the original slice. It would be preferable if the method returned a new sorted slice, thereby preserving the original array.
Access
slices
exposes few method that allows you to find element position in the slice:
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)
}
It will print next:
Index of 3: 4
Index of 3: 0
If you’re working with sorted slices, you can use BinarySearch
or BinarySearchFunc
to search for a target in a sorted slice and return the position where the target is found or the position where the target would appear in the sort order; it also returns a bool saying whether the target is found in the slice. Slice must be sorted in increasing order.
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)
}
It will print next:
Position of 3: 2. Found: true
Position of 6: 5. Found: false
Utility
slices
provides a long range of utility functions:
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
}))
}
It will print next:
Compare: 2
Contains: true
Delete: [{C 3} {A 1} {B 2}]
Equal: false
Is Sorted: false
Documentation
Conclusions
The recent introduction of slices
package in Go 1.21 represents a significant enhancement in handling slices. This package, however, needs a lot of important functions. The omission of some higher-level collection operations means that third-party libraries remain necessary for more complex data manipulation needs.
Maps
Similar to slices
, maps
appeared in Go standard library starting with Go 1.21. As you might expect, it defines various methods to manipulate maps.
Supported collections
Maps.
Transform
This package does not provide support for this category of operations.
Filter
This package does not provide support for this category of operations.
Aggregation
This package does not provide support for this category of operations.
Sorting/Ordering
This package does not provide support for this category of operations.
Access
This package does not provide support for this category of operations.
Utility
The only group present in this package:
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 }))
}
It will print next:
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
Documentation
Conclusions
The functionality of the maps
package is even more limited than that of the slices
package. Therefore, if you need to perform more complex manipulations with maps, you will almost certainly need to rely on third-party libraries.
github.com/elliotchance/pie
My personal favorite package to manipulate slices and maps. It offers a unique syntax that enables you to chain operations seamlessly, enhancing readability and efficiency in your code.
There are four ways to use library methods:
- pure call — just call the library method providing the arguments it needs;
pie.Of
— chain multiple operation with any element type;pie.OfOrdered
— chain multiple operation with numbers and strings type;pie.OfNumeric
— chain multiple operation with numbers only.
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(),
)
}
It will print next:
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]}
Chaining operations are pretty limited with this library because functions like Map
should return a collection of the same type. So, I believe a pure methods call is the best way to use this library.
Supported collections
Slices, maps.
Transform
Library exposes method Map
that allows to transform each element from one type to another:
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
},
),
)
}
It will print next:
Map: [C A D B]
Also, you can find method Flat
that transforms two dimension slice into single dimension:
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
},
),
),
),
)
}
It will print next:
Unique Tags: [b c d e a]
It’s possible to get only keys/values of the map with method Keys
or Values
:
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))
}
It will print next:
Keys: [3 1 2]
Values: [one two three]
Filter
Library provides several method to filter original collection: Bottom
, DropTop
, DropWhile
, Filter
, FilterNot
, Unique
, etc.
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))
}
It will print next:
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]
Aggregation
There is a generic method to do any type of aggregation Reduce
. Let’s count standard deviation:
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)
}
It will print next:
Standard deviation: 1.555635
Reduce
method calls reducer
first time with first slice element as accumulated value and second element as value argument. That’s why the formula is so weird.
From the example below you can find another built-in aggregation method Average
. Also, you can find methods Min
, Max
, Product
, etc.:
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))
}
It will print next:
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
Sorting/Ordering
There are three different methods to sort slices with pie:
Sort
— works similar tosort.Slice
. However, unlikesort.Slice
the slice returned will be reallocated as to not modify the input slice;SortStableUsing
— works similar tosort.SliceStable
. However, unlikesort.SliceStable
the slice returned will be reallocated as to not modify the input slice.SortUsing
— works similar tosort.Slice
. However, unlikesort. Slice
the slice returned will be reallocated as to not modify the input 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)
}
It will print next:
Sort: [1 2 3 4 5]
SortStableUsing: [1 2 3 4 5]
SortUsing: [1 2 3 4 5]
Original: [3 5 1 4 2]
Access
pie
exposes method FindFirstUsing
to get index of first element matching predicate in the slice:
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
},
),
)
}
It will print next:
FindFirstUsing: 2
Utility
pie
contains a lot of utility methods to work with slices. To name few:
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()))))
}
It will print next:
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}]
Documentation
Conclusions
elliotchance/pie/v2
library offers an impressive suite of functionalities that significantly simplify working with slices in Go. Its robust methods for manipulating and querying slice data provide developers with a powerful tool that enhances code readability and efficiency. I highly recommend that any Go developer try this library for your next projects.
github.com/samber/lo
Another popular library to manipulate collections in Go. It may look like the popular JavaScript library Lodash in some aspects. It uses generics under the hood, not reflection.
Supported collections
Slices, maps, channels.
Transform
Library supports default methods Map
and FlatMap
for slices:
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
},
),
)
}
It will print next:
Map: [C A D B]
The next example shows how FlatMap
works:
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
},
),
),
)
}
It will print next:
Unique Tags: [a b c d e]
Also, you can get map keys, values or map pairs to some slice, etc.:
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) }))
}
It will print next:
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]
Filter
There are bunch of Drop
methods in lo
library:
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 }))
}
It will print next:
Drop: [3 4 5]
DropRight: [1 2 3]
DropWhile: [3 4 5]
DropRightWhile: [1 2 3]
Also, you can filter slices and maps by predicate:
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 }))
}
It will print next:
Filter: [3 4 5]
PickBy: map[c:3]
Aggregation
lo
exposes method Reduce
to aggregate slices:
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)
}
It will print next:
Standard deviation: 1.555635
Also, it supports some generic aggregation methods like Sum
, Min
, Max
:
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))
}
It will print next:
Sum: 16.5
Min: 1.1
Max: 5.5
There are some useful methods to work with channels: FanIn
and FanOut
:
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)
}
}
It will print next:
0
1
2
5
3
6
4
7
8
9
And another example:
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)
}
}
}
It will print next:
0
1
2
0
1
2
0
1
2
Sorting/Ordering
lo
support only Reverse
method:
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))
}
It will print next:
Reverse: [5 4 3 2 1]
Access
You can find several method to find elements in slices:
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)
}
It will print next:
Item: {Name:Charlie Age:35}, Found: true
FindDuplicatesBy: [{Alice 25}]
Item: {Name:Charlie Age:35}, Index: 2, Found: true
Also, you can find methods that supports maps:
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)
}
The result is unpredictable because the map is an unordered structure. So you might see one of the next:
Key: Charlie, Found: true
Key: Alice, Found: true
Key: Eve, Found: true
Utility
There are tons of methods you can find in lo
. Some examples:
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)
}
It will print next:
Chunk: [[1 2 3] [4 5]]
Intersect: [3 4 5]
Union: [1 2 3 4 5 6 7]
Difference: [1 2], [6 7]
Documentation
Conclusions
I have demonstrated only about 10% of the methods from github.com/samber/lo
library. There are a lot of utils that simplify work with functions. This library stands out as a comprehensive toolkit for Go developers. This library is an invaluable asset for optimizing their Go development workflows.
UPD1: Comparison of lo and pie
Thanks to Vincent Free for this question. When deliberating between github.com/elliotchance/pie/v2
and github.com/samber/lo
, it’s crucial to consider several key aspects:
Required Functionality:
- Identify the specific features you need. While both libraries excel in providing operations for slices,
lo
stands out with a broader range of functionalities. It supports not only slices but also maps, channels, and additional helper functions.
Maintenance and Updates:
- Ensure the library is actively supported, maintained, and updated. Both
pie
andlo
fall within this category, offering continuous maintenance and updates to meet evolving needs.
Usability:
- Assess the ease of use of each library. While both are user-friendly, personal preference might sway your decision.
pie
appears to offer a smoother user experience than lo.
To make the final decision, I conducted a benchmark test to compare the three most popular methods of working with collections: Map
, Filter
, and Reduce
.
package main
import (
"testing"
"github.com/elliotchance/pie/v2"
"github.com/samber/lo"
)
type Person struct {
Name string
Age int
Address Address
}
type Address struct {
Street string
City string
Country string
}
type PersonSummary struct {
Name string
City string
Age int
}
var persons5 = []Person{
{Name: "Alice", Age: 30, Address: Address{Street: "123 Main St", City: "New York", Country: "USA"}},
{Name: "Bob", Age: 35, Address: Address{Street: "456 Elm St", City: "Los Angeles", Country: "USA"}},
{Name: "Charlie", Age: 25, Address: Address{Street: "789 Oak St", City: "New York", Country: "USA"}},
{Name: "David", Age: 40, Address: Address{Street: "321 Pine St", City: "Chicago", Country: "USA"}},
{Name: "Eve", Age: 22, Address: Address{Street: "654 Birch St", City: "Los Angeles", Country: "USA"}},
}
var persons35 = []Person{
{Name: "Alice", Age: 30, Address: Address{Street: "123 Main St", City: "New York", Country: "USA"}},
{Name: "Bob", Age: 35, Address: Address{Street: "456 Elm St", City: "Los Angeles", Country: "USA"}},
{Name: "Charlie", Age: 25, Address: Address{Street: "789 Oak St", City: "New York", Country: "USA"}},
{Name: "David", Age: 40, Address: Address{Street: "321 Pine St", City: "Chicago", Country: "USA"}},
{Name: "Eve", Age: 22, Address: Address{Street: "654 Birch St", City: "Los Angeles", Country: "USA"}},
{Name: "Alice", Age: 30, Address: Address{Street: "123 Main St", City: "New York", Country: "USA"}},
{Name: "Bob", Age: 35, Address: Address{Street: "456 Elm St", City: "Los Angeles", Country: "USA"}},
{Name: "Charlie", Age: 25, Address: Address{Street: "789 Oak St", City: "New York", Country: "USA"}},
{Name: "David", Age: 40, Address: Address{Street: "321 Pine St", City: "Chicago", Country: "USA"}},
{Name: "Eve", Age: 22, Address: Address{Street: "654 Birch St", City: "Los Angeles", Country: "USA"}},
{Name: "Alice", Age: 30, Address: Address{Street: "123 Main St", City: "New York", Country: "USA"}},
{Name: "Bob", Age: 35, Address: Address{Street: "456 Elm St", City: "Los Angeles", Country: "USA"}},
{Name: "Charlie", Age: 25, Address: Address{Street: "789 Oak St", City: "New York", Country: "USA"}},
{Name: "David", Age: 40, Address: Address{Street: "321 Pine St", City: "Chicago", Country: "USA"}},
{Name: "Eve", Age: 22, Address: Address{Street: "654 Birch St", City: "Los Angeles", Country: "USA"}},
{Name: "Alice", Age: 30, Address: Address{Street: "123 Main St", City: "New York", Country: "USA"}},
{Name: "Bob", Age: 35, Address: Address{Street: "456 Elm St", City: "Los Angeles", Country: "USA"}},
{Name: "Charlie", Age: 25, Address: Address{Street: "789 Oak St", City: "New York", Country: "USA"}},
{Name: "David", Age: 40, Address: Address{Street: "321 Pine St", City: "Chicago", Country: "USA"}},
{Name: "Eve", Age: 22, Address: Address{Street: "654 Birch St", City: "Los Angeles", Country: "USA"}},
{Name: "Alice", Age: 30, Address: Address{Street: "123 Main St", City: "New York", Country: "USA"}},
{Name: "Bob", Age: 35, Address: Address{Street: "456 Elm St", City: "Los Angeles", Country: "USA"}},
{Name: "Charlie", Age: 25, Address: Address{Street: "789 Oak St", City: "New York", Country: "USA"}},
{Name: "David", Age: 40, Address: Address{Street: "321 Pine St", City: "Chicago", Country: "USA"}},
{Name: "Eve", Age: 22, Address: Address{Street: "654 Birch St", City: "Los Angeles", Country: "USA"}},
{Name: "Alice", Age: 30, Address: Address{Street: "123 Main St", City: "New York", Country: "USA"}},
{Name: "Bob", Age: 35, Address: Address{Street: "456 Elm St", City: "Los Angeles", Country: "USA"}},
{Name: "Charlie", Age: 25, Address: Address{Street: "789 Oak St", City: "New York", Country: "USA"}},
{Name: "David", Age: 40, Address: Address{Street: "321 Pine St", City: "Chicago", Country: "USA"}},
{Name: "Eve", Age: 22, Address: Address{Street: "654 Birch St", City: "Los Angeles", Country: "USA"}},
{Name: "Alice", Age: 30, Address: Address{Street: "123 Main St", City: "New York", Country: "USA"}},
{Name: "Bob", Age: 35, Address: Address{Street: "456 Elm St", City: "Los Angeles", Country: "USA"}},
{Name: "Charlie", Age: 25, Address: Address{Street: "789 Oak St", City: "New York", Country: "USA"}},
{Name: "David", Age: 40, Address: Address{Street: "321 Pine St", City: "Chicago", Country: "USA"}},
{Name: "Eve", Age: 22, Address: Address{Street: "654 Birch St", City: "Los Angeles", Country: "USA"}},
}
func BenchmarkPieMap5(b *testing.B) {
for i := 0; i < b.N; i++ {
result := mapPie(persons5)
if len(result) != 5 {
b.Fatal("unexpected length")
}
}
}
func BenchmarkLoMap5(b *testing.B) {
for i := 0; i < b.N; i++ {
result := mapLo(persons5)
if len(result) != 5 {
b.Fatal("unexpected length")
}
}
}
func BenchmarkPieMap35(b *testing.B) {
for i := 0; i < b.N; i++ {
result := mapPie(persons35)
if len(result) != 35 {
b.Fatal("unexpected length")
}
}
}
func BenchmarkLoMap35(b *testing.B) {
for i := 0; i < b.N; i++ {
result := mapLo(persons35)
if len(result) != 35 {
b.Fatal("unexpected length")
}
}
}
func BenchmarkPieFilter5(b *testing.B) {
for i := 0; i < b.N; i++ {
result := filterPie(persons5, func(p Person) bool {
return p.Age > 30
})
if len(result) != 2 {
b.Fatal("unexpected length")
}
}
}
func BenchmarkLoFilter5(b *testing.B) {
for i := 0; i < b.N; i++ {
result := filterLo(persons5, func(p Person, _ int) bool {
return p.Age > 30
})
if len(result) != 2 {
b.Fatal("unexpected length")
}
}
}
func BenchmarkPieFilter35(b *testing.B) {
for i := 0; i < b.N; i++ {
result := filterPie(persons35, func(p Person) bool {
return p.Age > 30
})
if len(result) != 14 {
b.Fatal("unexpected length")
}
}
}
func BenchmarkLoFilter35(b *testing.B) {
for i := 0; i < b.N; i++ {
result := filterLo(persons35, func(p Person, _ int) bool {
return p.Age > 30
})
if len(result) != 14 {
b.Fatal("unexpected length")
}
}
}
func BenchmarkPieReduce5(b *testing.B) {
for i := 0; i < b.N; i++ {
result := reducePie(persons5, func(acc Person, p Person) Person {
acc.Age += p.Age
return acc
})
if result.Age != 152 {
b.Fatal("unexpected result")
}
}
}
func BenchmarkLoReduce5(b *testing.B) {
for i := 0; i < b.N; i++ {
result := reduceLo(persons5, func(acc Person, p Person, _ int) Person {
acc.Age += p.Age
return acc
})
if result.Age != 152 {
b.Fatal("unexpected result")
}
}
}
func BenchmarkPieReduce35(b *testing.B) {
for i := 0; i < b.N; i++ {
result := reducePie(persons35, func(acc Person, p Person) Person {
acc.Age += p.Age
return acc
})
if result.Age != 1064 {
b.Fatal("unexpected result")
}
}
}
func BenchmarkLoReduce35(b *testing.B) {
for i := 0; i < b.N; i++ {
result := reduceLo(persons35, func(acc Person, p Person, _ int) Person {
acc.Age += p.Age
return acc
})
if result.Age != 1064 {
b.Fatal("unexpected result")
}
}
}
func mapPie(persons []Person) []PersonSummary {
return pie.Map(persons, func(p Person) PersonSummary {
return PersonSummary{
Name: p.Name,
City: p.Address.City,
Age: p.Age,
}
})
}
func mapLo(persons []Person) []PersonSummary {
return lo.Map(persons, func(p Person, _ int) PersonSummary {
return PersonSummary{
Name: p.Name,
City: p.Address.City,
Age: p.Age,
}
})
}
func filterPie(persons []Person, predicate func(Person) bool) []Person {
return pie.Filter(persons, predicate)
}
func filterLo(persons []Person, predicate func(Person, int) bool) []Person {
return lo.Filter(persons, predicate)
}
func reducePie(persons []Person, reducer func(acc Person, p Person) Person) Person {
return pie.Reduce(persons, reducer)
}
func reduceLo(persons []Person, reducer func(acc Person, p Person, _ int) Person) Person {
return lo.Reduce(persons, reducer, Person{})
}
I received the next results:
goos: linux
goarch: amd64
pkg: go-collections
cpu: AMD Ryzen 7 5800U with Radeon Graphics
BenchmarkPieMap5-16 7304163 159.5 ns/op
BenchmarkLoMap5-16 7416362 160.4 ns/op
BenchmarkPieMap35-16 1322214 909.1 ns/op
BenchmarkLoMap35-16 1323153 914.5 ns/op
BenchmarkPieFilter5-16 7790677 147.9 ns/op
BenchmarkLoFilter5-16 8953048 136.1 ns/op
BenchmarkPieFilter35-16 1502720 794.2 ns/op
BenchmarkLoFilter35-16 1647608 725.9 ns/op
BenchmarkPieReduce5-16 10105086 119.0 ns/op
BenchmarkLoReduce5-16 7784736 154.0 ns/op
BenchmarkPieReduce35-16 1331350 900.5 ns/op
BenchmarkLoReduce35-16 1880635 614.8 ns/op
PASS
ok go-collections 21.267s
In the majority of scenarios, lo
has demonstrated better performance compared to pie
.
Thus, if your application prioritizes performance and demands a wide array of operations, lo
emerges as the preferable choice. Conversely, if seamless integration and straightforward usage are your primary considerations, opting for pie
would be more advantageous.
Support Me
If you found joy or value in my article, and you’d like to show some appreciation, you can now donate with ‘Buy Me A Coffee’! Your support will go a long way in helping me continue to share more stories, insights, and content that resonates with you.
Conclusions
Go ecosystem is rich with tools that can simplify and extend the functionality of data manipulation. Libraries I’ve reviewed offer different methods to perform different operations with slices, maps, and channels. The library choice largely depends on the specific requirements of the project and the developer’s preference for certain programming styles.