In this blog post we are going to explore the maps
and slices
packages
from the Go standard library. These two packages were introduced in Go 1.21
and got new methods in Go 1.23 with the introduction of Iterators.
The two packages provide useful and convenient methods to work with slices and maps.
Builtin ¶
Go provides us with three builtin types to work with collections of data:
array
is a fixed-size, sequentially stored data structure containing elements of the same type.slice
is a dynamically sized view of an underlying array.map
is a data structure that associates keys with values.
The language provides a set of builtin functions to work with these types:
append adds elements to the end of a slice, resizing the slice if necessary. This method either returns the original slice if it has enough capacity, or a new slice with the added elements.
cap returns the capacity of a variable depending on its type (array, slice, channel).
clear removes all elements from a map or sets all elements of a slice to their zero value.
copy copies elements from a source slice to a destination slice.
delete removes the element with the specified key from the map.
len returns the number of elements in various types like arrays, slices, maps, and strings.
make allocates and initializes an object of type slice, map, or channel.
comparable Type Constraint ¶
Before we dive into the slices
and maps
packages, let's talk about the comparable
type constraint.
Most methods in these two packages are generic, and quite a few of them use parameters constrained
to the comparable
type constraint.
The comparable
constraint specifies that a generic type parameter must support the ==
and !=
operators.
Most types in Go fulfill this constraint, including all basic types (e.g., int, string, bool), pointers,
arrays, structs, and channels composed solely of comparable types, and interface types.
However, there are some types that are not comparable:
Slices: Comparing two slices directly will result in a compile-time error.
Maps: Cannot be compared, except to nil.
Functions: Are not comparable, as they represent code references rather than concrete values.
Interfaces with non-comparable dynamic values
Interface types (e.g., any
) can be compared syntactically, but if their dynamic (underlying) values are non-comparable, a runtime panic occurs:
var x, y any = []int{1}, []int{1}
fmt.Println(x == y) // Runtime Panic
Structs/Arrays: Containing non-comparable types
Composite types like structs or arrays are only comparable if all their fields/elements are comparable.
This example does not compile because slices are not comparable:
type Person struct {
Hobbies []string
}
a := Person{Hobbies: []string{"Reading"}}
b := Person{Hobbies: []string{"Swimming"}}
fmt.Println(a == b) // Compile Time Error
But this example works because all fields in the struct are comparable and Go compares them field by field:
type Person struct {
Name string
Age int
}
a := Person{Name: "Alice", Age: 25}
b := Person{Name: "Alice", Age: 25}
fmt.Println(a == b) // true
Pointers are always comparable, even when they point to non-comparable types. The following example compiles and runs without errors. Important to note that comparing pointers compares the memory address, not the values they point to.
type Person struct {
Hobbies *[]string
}
a := Person{Hobbies: &[]string{"Reading"}}
b := Person{Hobbies: &[]string{"Reading"}}
fmt.Println(a == b) // false
hobbies := &[]string{"Reading"}
c := Person{Hobbies: hobbies}
d := Person{Hobbies: hobbies}
fmt.Println(c == d) // true
Assignments ¶
Some methods we will look at later internally use assignments to copy values from one slice or map to another.
In Go, variables hold values, and this value can be either a "value type" or a "reference type".
-
Value types (e.g., int, float64, bool, string, structs, arrays):
When you assign a value type variable to another, or pass it to a function, a copy of the actual value is made. Changes to the copy do not affect the original variable. -
Reference types (e.g., slices, maps, channels, pointers, functions):
When you assign a reference type variable to another or pass it to a function, the address structure is copied, not the full data itself. Both variables then reference the same underlying data. Changes made through one variable will affect the data seen by the other variable. Maps and slices technically contain a header that is a value type, but the header contains a pointer to the underlying data. So when you assign a slice or map to another variable, Go copies the header, but because the header contains a pointer, both variables point to the same underlying data structure.
Value types are copied, changing one does not affect the other:
type address struct {
city string
}
type person struct {
name string
age int
addr address
}
func main() {
pv1 := person{
name: "Alice",
age: 30,
addr: address{
city: "Wonderland",
},
}
pv2 := pv1
pv2.name = "Bob"
pv2.addr.city = "Builderland"
println(pv1.name, pv1.age, pv1.addr.city) // Alice 30 Wonderland
println(pv2.name, pv2.age, pv2.addr.city) // Bob 30 Builderland
v1 := 10
v2 := v1
v2 = 20
println(v1) // 10
println(v2) // 20
Reference types, in this example a pointer to a struct, point to some underlying data structure. When you assign a reference type variable to another, the code copies the reference (the address of the underlying data structure), not the data itself.
When you change the data through one variable, the other variable sees the change.
pp1 := &person{
name: "Charlie",
age: 25,
addr: address{
city: "Chocoland",
},
}
pp2 := pp1
pp2.name = "Dave"
pp2.addr.city = "Daveland"
println(pp1.name, pp1.age, pp1.addr.city) // Dave 25 Daveland
println(pp2.name, pp2.age, pp2.addr.city) // Dave 25 Daveland
Keep this in mind when you use methods like slices.Clone()
or maps.Clone()
.
Even though these methods create a new slice or map and copy the elements,
the elements in the new slice or map still reference the same underlying data if they are reference types.
Slices ¶
In this section we take a look at all the methods provided by the slices
package.
The following examples use this struct and example data
type Developer struct {
Name string
CoffeeLevel int
BugCount int
}
func main() {
devTeam := []Developer{
{Name: "Alice", CoffeeLevel: 8, BugCount: 2},
{Name: "Bob", CoffeeLevel: 3, BugCount: 5},
{Name: "Charlie", CoffeeLevel: 6, BugCount: 1},
{Name: "Diana", CoffeeLevel: 9, BugCount: 0},
{Name: "Eve", CoffeeLevel: 4, BugCount: 3},
}
Iterator methods ¶
Returns an iterator (iter.Seq2
) that yields both the index and value of each element in the slice. This is by itself not that useful, because in Go you can already use the for range
loop to iterate over a slice. However, it is useful when you have methods that expect an iterator as input.
pretty := prettyPrint(slices.All(devTeam))
fmt.Println(pretty) // 0: {Alice 8 2}, 1: {Bob 3 5}, 2: {Charlie 6 1}, 3: {Diana 9 0}, 4: {Eve 4 3}
Returns an iterator (iter.Seq
) that returns only the values of each element. Useful when you only need the elements and not their positions.
maxLevel := maxCoffeeLevel(slices.Values(devTeam))
fmt.Println(maxLevel) // 9
Similar to All
returns an iterator (iter.Seq2
) that returns both the index and value, but this one traverses the slice in reverse order. The indices are adjusted to reflect the reverse traversal order, so the first element in the slice will have index len(slice) - 1
.
pretty = prettyPrint(slices.Backward(devTeam))
fmt.Println(pretty) // 4: {Eve 4 3}, 3: {Diana 9 0}, 2: {Charlie 6 1}, 1: {Bob 3 5}, 0: {Alice 8 2}
Creating and populating ¶
Creates a new slice and appends all values from the passed in iterator (iter.Seq
). This method calls internally the slices.AppendSeq()
method.
collectedDevs := slices.Collect(slices.Values(devTeam))
fmt.Println(collectedDevs) // [{Alice 8 2} {Bob 3 5} {Charlie 6 1} {Diana 9 0} {Eve 4 3}]
Appends all values from an iterator (iter.Seq
) to an existing slice and returns the extended slice. This method internally uses append
so it either returns the
original slice if it has enough capacity, or a new extended slice with the original elements and the appended elements.
existingDevelopers := []Developer{
{Name: "Frank", CoffeeLevel: 7, BugCount: 2},
}
appendedDevs := slices.AppendSeq(existingDevelopers, slices.Values(devTeam))
fmt.Println(appendedDevs) // [{Frank 7 2} {Alice 8 2} {Bob 3 5} {Charlie 6 1} {Diana 9 0} {Eve 4 3}]
Returns a new slice containing the specified slice repeated count times.
defaultDev := Developer{Name: "NewHire", CoffeeLevel: 5, BugCount: 0}
newHires := slices.Repeat([]Developer{defaultDev}, 3)
fmt.Println(newHires) // [{NewHire 5 0} {NewHire 5 0} {NewHire 5 0}]
Copying and cloning ¶
Returns a new slice containing copies of all elements from the original slice.
teamCopy := slices.Clone(devTeam)
fmt.Println(teamCopy) // [{Alice 8 2} {Bob 3 5} {Charlie 6 1} {Diana 9 0} {Eve 4 3}]
Creates a new slice and then appends the passed in slices to this new slice in order they are passed in.
team1 := []Developer{
{Name: "Frank", CoffeeLevel: 7, BugCount: 2},
}
team2 := []Developer{
{Name: "Grace", CoffeeLevel: 10, BugCount: 0},
}
combinedTeam := slices.Concat(team1, team2)
fmt.Println(combinedTeam) // [{Frank 7 2} {Grace 10 0}]
Searching ¶
Returns true if the slice contains the specified value. The elements in the slice must be comparable, i.e., they must support the == operator.
alice := Developer{Name: "Alice", CoffeeLevel: 8, BugCount: 2}
isAliceInTeam := slices.Contains(devTeam, alice)
fmt.Println(isAliceInTeam) // true
Returns true if any element in the slice satisfies the provided predicate function. Useful if either the elements in the slice are not comparable or if you want to search for a specific condition.
hasDevsWithZeroBugs := slices.ContainsFunc(devTeam, func(dev Developer) bool {
return dev.BugCount == 0
})
fmt.Println(hasDevsWithZeroBugs) // true
Returns the index of the first element equal to the specified value, or -1 if not found. Elements must be comparable.
alicePos := slices.Index(devTeam, alice)
fmt.Println(alicePos) // 0
Returns the index of the first element that satisfies the predicate function, or -1 if no element matches.
highCoffeeIdx := slices.IndexFunc(devTeam, func(dev Developer) bool {
return dev.CoffeeLevel >= 9
})
fmt.Println(highCoffeeIdx) // 3
Searches for a value in a sorted slice using the binary search algorithm. Returns the index where the value is found and a boolean indicating if it was found. The slice must be sorted in ascending order.
coffeeLevels := []int{3, 4, 6, 8, 9}
index, found := slices.BinarySearch(coffeeLevels, 6)
fmt.Println(index, found) // 2 true
Performs a binary search on a sorted slice using a custom comparison function. The slice must be sorted according to the same comparison function.
comparisonFn := func(a, b Developer) int {
return a.BugCount - b.BugCount
}
sortedByBugs := slices.Clone(devTeam)
slices.SortFunc(sortedByBugs, comparisonFn)
targetBugs := 2
bugIndex, bugFound := slices.BinarySearchFunc(sortedByBugs, Developer{BugCount: targetBugs}, comparisonFn)
fmt.Println(bugIndex, bugFound) // 2 true
Comparing ¶
Returns true if both slices have the same length and all corresponding elements are equal using the == operator, therefore, all elements in the slice must be comparable.
anotherTeam := []Developer{
{Name: "Alice", CoffeeLevel: 8, BugCount: 2},
{Name: "Bob", CoffeeLevel: 3, BugCount: 5},
{Name: "Charlie", CoffeeLevel: 6, BugCount: 1},
{Name: "Diana", CoffeeLevel: 9, BugCount: 0},
{Name: "Eve", CoffeeLevel: 4, BugCount: 3},
}
teamsEqual := slices.Equal(devTeam, anotherTeam)
fmt.Println(teamsEqual) // true
Returns true if both slices have the same length and all corresponding elements are equal, according to the provided equality function. This allows for custom comparison logic, which is useful when the elements are not directly comparable or when you want to compare based on specific fields.
sameNames := slices.EqualFunc(devTeam, anotherTeam, func(a, b Developer) bool {
return a.Name == b.Name
})
fmt.Println(sameNames) // true
Compares the elements of two slices, using cmp.Compare
on each pair of elements. The elements are compared index by index, starting at index 0, until one element is not equal to the other. The result of the first non-matching element is returned. If both slices are equal until one of them ends, the shorter slice is considered less than the longer one. The method returns 0 if the slices are equal, -1 if the first slice is less than the second, and 1 if the first slice is greater than the second.
The elements in the slice must be of a type that supports the <, <=, >, and >= operators. This means they must implement the cmp.Ordered
type constraint.
numbers1 := []int{1, 2, 3}
numbers2 := []int{1, 2, 4}
comparison := slices.Compare(numbers1, numbers2)
fmt.Println(comparison) // -1 (since 3 < 4)
Compares the elements of two slices using a custom comparison function. The comparison function must return negative, zero, or positive values to indicate whether the first element is less than, equal to, or greater than the second element. This method is useful when the elements in the slice are not orderable or when you want to compare based on specific fields.
This examples compares slices containing structs. Structs in Go do not implement the cmp.Ordered
type constraint, so we can't use the slices.Compare
method. Instead, we have to use a custom comparison function.
compResult := slices.CompareFunc(devTeam, anotherTeam, func(a, b Developer) int {
if a.Name < b.Name {
return -1
} else if a.Name > b.Name {
return 1
}
return 0
})
fmt.Println(compResult) // 0
Sorting ¶
Sorts the slice in-place in ascending order. The elements in the slice must implement the cmp.Ordered
type constraint. The original slice is modified.
numbers := []int{9, 3, 6, 1, 8}
slices.Sort(numbers)
fmt.Println(numbers) // [1 3 6 8 9]
Sorts the slice in-place using a custom comparison function that must return negative, zero, or positive values. The original slice is modified.
coffeeTeam := slices.Clone(devTeam)
slices.SortFunc(coffeeTeam, func(a, b Developer) int {
return a.CoffeeLevel - b.CoffeeLevel
})
fmt.Println(coffeeTeam) // [{Bob 3 5} {Eve 4 3} {Charlie 6 1} {Alice 8 2} {Diana 9 0}]
Sorts the slice in-place using a stable sort algorithm with a custom comparison function. Equal elements maintain their relative order from the original slice.
This is different to slices.SortFunc
, where the order of equal elements is not defined. The original slice is modified.
stableTeam := slices.Clone(devTeam)
slices.SortStableFunc(stableTeam, func(a, b Developer) int {
return a.BugCount - b.BugCount
})
fmt.Println(stableTeam) // [{Diana 9 0} {Charlie 6 1} {Alice 8 2} {Eve 4 3} {Bob 3 5}]
Returns true if the slice is sorted in ascending order. Elements must implement the cmp.Ordered
type constraint.
isSliceSorted := slices.IsSorted([]int{1, 7, 3, 4, 5})
fmt.Println(isSliceSorted) // false
Returns true if the slice is sorted according to the provided comparison function.
isTeamSorted := slices.IsSortedFunc(coffeeTeam, func(a, b Developer) int {
return a.CoffeeLevel - b.CoffeeLevel
})
fmt.Println(isTeamSorted) // true
Mutation ¶
Reverses the order of elements in the slice in-place. The original slice is modified.
reverseNumbers := []int{1, 2, 3, 4, 5}
slices.Reverse(reverseNumbers)
fmt.Println(reverseNumbers) // [5 4 3 2 1]
Returns a modified slice after removing elements from the specified range. Lower bound is inclusive, upper bound is exclusive. The method panics if the indices are out of bounds.
withoutMiddle := slices.Delete(slices.Clone(devTeam), 1, 3)
fmt.Println(withoutMiddle) // [{Alice 8 2} {Diana 9 0} {Eve 4 3}]
Be aware that the modified slice is still linked to the original slice's underlying array. If the example code did not clone the slice, and instead would use the original slice, the end of the original slice would be zeroed out.
In this example, the Delete
method creates a copy of the original slice, removes the elements from index 1 and 2, moves elements at index 3 and 4 to index 1 and 2, respectively,
and finally zeros (with clear
) the elements at index 3 and 4. Because the copy of the slice points to the same underlying array as the original slice, the original slice is also modified.
To prevent any issues, best practice is to overwrite the variable holding the original slice with the result of the Delete
method, or to clone the original slice before calling Delete
.
Returns a modified slice with all elements removed that satisfy the provided predicate function. Same behavior as slices.Delete
, zeros out the elements at the end and
the original slice and the modified slice shares the same underlying array.
lowCoffeeTeam := slices.DeleteFunc(slices.Clone(devTeam), func(dev Developer) bool {
return dev.CoffeeLevel < 5
})
fmt.Println(lowCoffeeTeam) // [{Alice 8 2} {Charlie 6 1} {Diana 9 0}]
Returns a modified slice with consecutive duplicate elements removed, keeping only the first occurrence of each group.
Elements in the slice must be comparable. Same behavior as the slices.Delete
method concerning the underlying array.
duplicates := []int{1, 1, 2, 2, 2, 3, 1, 1}
compacted := slices.Compact(slices.Clone(duplicates))
fmt.Println(compacted) // [1 2 3 1]
Returns a modified slice with consecutive elements removed that are equal, according to the provided equality function. Only the first element of each equal group is kept. Same behavior as the slices.Delete
method concerning the underlying array.
devDuplicates := []Developer{
{Name: "Alice", CoffeeLevel: 8, BugCount: 2},
{Name: "Bob", CoffeeLevel: 8, BugCount: 3},
{Name: "Charlie", CoffeeLevel: 6, BugCount: 1},
}
compactedDevs := slices.CompactFunc(devDuplicates, func(a, b Developer) bool {
return a.CoffeeLevel == b.CoffeeLevel
})
fmt.Println(compactedDevs) // [{Alice 8 2} {Charlie 6 1}]
Returns a modified slice with elements in the specified range (lower bound inclusive, upper bound exclusive) replaced by the provided replacement elements. If the replacement slice is shorter than the range, it moves the remaining elements to the left and zeros the end of the slice, which also affects the original slice because they share the same underlying array. If you add more elements than the range length, the method might create a new slice if the original slice does not have enough capacity.
replacement := []Developer{{Name: "Grace", CoffeeLevel: 10, BugCount: 0}}
replaced := slices.Replace(slices.Clone(devTeam), 0, 1, replacement...)
fmt.Println(replaced) // [{Grace 10 0} {Bob 3 5} {Charlie 6 1} {Diana 9 0} {Eve 4 3}]
Returns a modified slice with the specified values inserted at the given index. All existing elements at and after the index are shifted to the right. The modified slice might share the underlying array with the original slice if there is enough capacity to add all new elements. If the original slice does not have enough capacity, a new slice is created with the original elements and the new elements inserted at the specified index.
newTeam := slices.Insert(devTeam, 2, Developer{Name: "Frank", CoffeeLevel: 7, BugCount: 2})
fmt.Println(newTeam) // [{Alice 8 2} {Bob 3 5} {Frank 7 2} {Charlie 6 1} {Diana 9 0} {Eve 4 3}]
Utility methods ¶
The following min and max methods expect the slice to be non-empty. If the slice is empty, they will panic. Always ensure your slice contains at least one element before calling these methods.
Returns the smallest element in the slice. The elements must implement the cmp.Ordered
type constraint.
testNumbers := []int{8, 3, 6, 9, 4}
minNum := slices.Min(testNumbers)
fmt.Println(minNum) // 3
Returns the smallest element in the slice according to the provided comparison function. Useful for slices with elements that
do not implement the cmp.Ordered
type constraint, such as structs.
minCoffeeDev := slices.MinFunc(devTeam, func(a, b Developer) int {
return a.CoffeeLevel - b.CoffeeLevel
})
fmt.Println(minCoffeeDev) // {Bob 3 5}
Returns the largest element in the slice. The elements must implement the cmp.Ordered
type constraint.
maxNum := slices.Max(testNumbers)
fmt.Println(maxNum) // 9
Returns the largest element in the slice according to the provided comparison function.
maxCoffeeDev := slices.MaxFunc(devTeam, func(a, b Developer) int {
return a.CoffeeLevel - b.CoffeeLevel
})
fmt.Println(maxCoffeeDev) // {Diana 9 0}
Returns a slice with unused capacity removed, setting the capacity equal to the length. This can help free memory when you are sure that the slice will not grow further.
Be aware that the returned slice always shares the same underlying array with the original slice. Clip
only changes the slice header's view of the underlying array (specifically, its capacity), but it does not allocate a new array or copy elements.
largeSlice := make([]int, 5, 20)
fmt.Println(len(largeSlice), cap(largeSlice)) // 5 20
clippedSlice := slices.Clip(largeSlice)
fmt.Println(len(clippedSlice), cap(clippedSlice)) // 5 5
Returns a slice with increased capacity to make sure there is enough space for adding at least n
more elements without requiring another allocation.
This can help improve performance when you know you will append a certain number of elements to the slice. This method returns the original slice if there is enough free capacity.
smallSlice := []int{1, 2, 3}
fmt.Println(len(smallSlice), cap(smallSlice)) // 3 3
grownSlice := slices.Grow(smallSlice, 10)
fmt.Println(len(grownSlice), cap(grownSlice)) // 3 14
Maps ¶
In this section we take a look at all the methods provided by the maps
package.
The following examples use this struct and example data.
type Developer struct {
CoffeeLevel int
BugCount int
}
func main() {
devTeam := map[string]Developer{
"Alice": {CoffeeLevel: 8, BugCount: 2},
"Bob": {CoffeeLevel: 3, BugCount: 5},
"Charlie": {CoffeeLevel: 6, BugCount: 1},
}
Iterator methods ¶
These methods by itself are not that useful, because in Go you can already use the for range
loop to iterate over a map. However, they are useful when you have methods that expect an iterator as input.
All three methods share the same behavior that the iteration order is not guaranteed and may vary between different iterations of the same map.
Returns an iterator (iter.Seq2
) that returns all key-value pairs from the map.
maxDevName, maxDev, found := maxCoffeeDeveloper(maps.All(devTeam))
fmt.Println(maxDevName, maxDev, found) // Alice {8 2} true
Returns an iterator (iter.Seq
) that returns only the keys from the map.
keys := prettyPrint(maps.Keys(devTeam))
fmt.Println(keys) // Bob, Charlie, Alice
Returns an iterator (iter.Seq
) that returns only the values from the map.
maxCoffee := maxCoffeeLevel(maps.Values(devTeam))
fmt.Println(maxCoffee) // 8
Collecting ¶
Creates a new map by collecting all key-value pairs from an iterator sequence (iter.Seq2
).
This allocates a new map and populates it with the yielded pairs.
highPerformers := maps.Collect(maps.All(devTeam))
fmt.Println(highPerformers) // map[Alice:{8 2} Bob:{3 5} Charlie:{6 1}]
Cloning ¶
Returns a copy of the passed in map. Be aware if your values are reference types, like slices or pointers, the values in the new map will still reference the same underlying data.
newTeam := maps.Clone(devTeam)
maps.Insert(newTeam, maps.All(newDevs))
fmt.Println(newTeam) // map[Alice:{9 0} Bob:{3 5} Charlie:{6 1} Eve:{7 3}]
Comparing ¶
Returns true if both maps have the same key/value pairs. Values must be comparable, i.e., they must support the == operator. Keys are also compared using the == operator.
areTeamsEqual := maps.Equal(devTeam, backupTeam)
fmt.Println(areTeamsEqual) // false
Returns true if both maps have the same key/value pairs. Values will be compared using the provided equality function. Keys are compared using the == operator. Go only supports map keys that are comparable.
sameCoffeeLevel := maps.EqualFunc(stagingTeam, backupTeam,
func(dev1, dev2 Developer) bool {
return dev1.CoffeeLevel == dev2.CoffeeLevel
})
fmt.Println(sameCoffeeLevel) // false
Mutation ¶
Inserts all key-value pairs from an iterator sequence (iter.Seq2
) into the destination map.
If a key already exists in the destination map, its value is overwritten with the value from the iterator.
newTeam := maps.Clone(devTeam)
maps.Insert(newTeam, maps.All(newDevs))
fmt.Println(newTeam) // map[Alice:{9 0} Bob:{3 5} Charlie:{6 1} Eve:{7 3}]
Copies all key/value pairs from the source map (second argument), to the destination map (first argument). If a key already exists in the destination map, the value will be overwritten with the value from the source map.
stagingTeam := make(map[string]Developer)
stagingTeam["Frank"] = Developer{CoffeeLevel: 10, BugCount: 4}
stagingTeam["Eve"] = Developer{CoffeeLevel: 3, BugCount: 9}
maps.Copy(stagingTeam, devTeam)
fmt.Println(stagingTeam) // map[Alice:{8 2} Bob:{3 5} Charlie:{6 1} Diana:{9 0} Eve:{7 3} Frank:{10 4}]
Removes all key-value pairs from the map where the predicate function returns true.
maps.DeleteFunc(devTeam, func(name string, dev Developer) bool {
return dev.CoffeeLevel < 5
})
fmt.Println(devTeam) // map[Alice:{8 2} Charlie:{6 1}]
Conclusion ¶
This concludes our exploration of the maps
and slices
packages in Go. These packages provide useful and convenient methods to work with slices and maps, making it easier to manipulate and query collections of data.