Transforming a Collection
A transformation takes a collection, such as a slice or a map, and builds a new collection from it. The original is not changed.
In many languages, lists and maps come with ready-made methods like map, zip, associate and flatten. Go does not have these methods built in. Instead you write a small for loop, or a small helper function that you can reuse. That is the Go way, and it is short once you know the pattern.
Go has generics since version 1.18. A generic function works with any element type. The lessons on this page use generics (Go 1.21 or newer) so one helper works for numbers, strings and structs alike. The standard packages slices, maps and strings also help with common jobs.
Generic Map helper syntax
This is the shape of a reusable Map helper. T is the type of the input elements and R is the type of the result elements. The function f is called once for every element, and each answer is added to the new slice.
func Map[T, R any](items []T, f func(T) R) []R {
result := make([]R, 0, len(items))
for _, v := range items {
result = append(result, f(v))
}
return result
}
Map
The map operation applies the same function to every element and collects the answers in a new slice of the same length. For example, it can turn [1 2 3] into [3 6 9].
Go has no map method on slices. You write a loop that calls the function for each element and uses append to add the result to a new slice. If you do this often, put the loop in a generic helper function.
Do not confuse this with Go's map type, which stores key:value pairs. Here "map" means "apply a function to every element".
Map with a helper function
The Map helper takes a slice and a function. It calls the function on every element and returns a new slice. Here it multiplies each number by 3.
Output: Go program and captured output
package main
import ("fmt")
// Map builds a new slice by applying f to every element of items.
func Map[T, R any](items []T, f func(T) R) []R {
result := make([]R, 0, len(items))
for _, v := range items {
result = append(result, f(v))
}
return result
}
func main() {
numbers := []int{1, 2, 3}
tripled := Map(numbers, func(x int) int { return x * 3 })
fmt.Println(tripled)
}
Map with the index
Sometimes the position of an element matters too. In Go, range gives you both the index and the value, so a plain loop is enough. Here each value is multiplied by its own index.
Output: Go program and captured output
package main
import ("fmt")
func main() {
numbers := []int{1, 2, 3}
result := make([]int, 0, len(numbers))
// range gives the index and the value of each element
for idx, value := range numbers {
result = append(result, value * idx)
}
fmt.Println(result)
}
Skip some results
Sometimes some results should be dropped. Go has no null values for numbers, so the helper's function returns two things: the result and a bool that says whether to keep it. When the bool is false, the element is skipped.
Output: Go program and captured output
package main
import ("fmt")
// MapNotNil keeps only the results where f returns ok == true.
func MapNotNil[T, R any](items []T, f func(T) (R, bool)) []R {
result := make([]R, 0, len(items))
for _, v := range items {
if r, ok := f(v); ok {
result = append(result, r)
}
}
return result
}
func main() {
numbers := []int{1, 2, 3}
result := MapNotNil(numbers, func(x int) (int, bool) {
if x == 2 {
return 0, false // skip this element
}
return x * 3, true
})
fmt.Println(result)
}
Transform map keys
To change the keys of a map, make a new map and copy every entry with the new key. Here strings.ToUpper turns each key into uppercase. fmt.Println prints a map with its keys sorted, so the output is always in the same order.
Output: Go program and captured output
package main
import (
"fmt"
"strings"
)
func main() {
numbersMap := map[string]int{"key1": 1, "key2": 2}
upperKeys := make(map[string]int, len(numbersMap))
// copy every entry, but change the key to uppercase
for key, value := range numbersMap {
upperKeys[strings.ToUpper(key)] = value
}
fmt.Println(upperKeys)
}
Transform map values
To change the values, do the same but keep the key and compute a new value. Here each value grows by the length of its key: 1 + len("key1") is 5.
Output: Go program and captured output
package main
import ("fmt")
func main() {
numbersMap := map[string]int{"key1": 1, "key2": 2}
newValues := make(map[string]int, len(numbersMap))
// keep the key, change the value
for key, value := range numbersMap {
newValues[key] = value + len(key)
}
fmt.Println(newValues)
}
Zip
Zipping takes two slices and pairs up their elements by position: the first with the first, the second with the second, and so on. The result has as many pairs as the shorter slice.
Go has no zip function. You define a small struct to hold one pair, find the shorter length with the built-in min, and loop with an index to build the pairs.
Zip two slices
The Pair struct holds one color and one animal. The loop runs to the shorter length and builds one Pair per index. Printing a slice of structs shows each pair in curly braces.
Output: Go program and captured output
package main
import ("fmt")
// Pair holds one element from each slice.
type Pair struct {
Color string
Animal string
}
func main() {
colors := []string{"red", "brown", "grey"}
animals := []string{"fox", "bear", "wolf"}
// stop at the shorter slice
n := min(len(colors), len(animals))
pairs := make([]Pair, 0, n)
for i := 0; i < n; i++ {
pairs = append(pairs, Pair{colors[i], animals[i]})
}
fmt.Println(pairs)
}
Zip with a transformation
You do not always need the pairs themselves. Here the loop uses both elements straight away to build a sentence. animals[i][:1] is the first letter, made uppercase with strings.ToUpper, and animals[i][1:] is the rest of the word.
Output: Go program and captured output
package main
import (
"fmt"
"strings"
)
func main() {
colors := []string{"red", "brown", "grey"}
animals := []string{"fox", "bear", "wolf"}
n := min(len(colors), len(animals))
for i := 0; i < n; i++ {
// uppercase only the first letter of the animal name
name := strings.ToUpper(animals[i][:1]) + animals[i][1:]
fmt.Println("The " + name + " is " + colors[i])
}
}
Unzip a slice of pairs
Unzipping is the reverse: one slice of pairs becomes two slices. The loop reads each pair and appends its parts to two separate slices.
Output: Go program and captured output
package main
import ("fmt")
type Pair struct {
Word string
Number int
}
func main() {
numberPairs := []Pair{{"one", 1}, {"two", 2}, {"three", 3}}
words := make([]string, 0, len(numberPairs))
numbers := make([]int, 0, len(numberPairs))
// split each pair into its two parts
for _, p := range numberPairs {
words = append(words, p.Word)
numbers = append(numbers, p.Number)
}
fmt.Println(words)
fmt.Println(numbers)
}
Associate
Associating builds a map from a slice. Each element gives you one key and one value. A map cannot hold the same key twice, so if two elements produce the same key, the later one wins.
Go has no associate functions. Make an empty map with make, then loop over the slice and assign result[key] = value for each element.
Elements as keys
The element itself becomes the key, and a value is computed from it. Here the words are keys and their lengths are values.
Output: Go program and captured output
package main
import ("fmt")
func main() {
numbers := []string{"one", "two", "three"}
lengths := make(map[string]int, len(numbers))
// each word becomes a key, its length becomes the value
for _, word := range numbers {
lengths[word] = len(word)
}
fmt.Println(lengths)
}
Elements as values
The element becomes the value, and the key is computed from it. Here the key is the uppercase first letter. Both two and three start with T, so three replaces two and the map has only two entries.
Output: Go program and captured output
package main
import (
"fmt"
"strings"
)
func main() {
numbers := []string{"one", "two", "three"}
byFirstLetter := make(map[string]string, len(numbers))
// the key is the first letter; a later word replaces an earlier one with the same key
for _, word := range numbers {
byFirstLetter[strings.ToUpper(word[:1])] = word
}
fmt.Println(byFirstLetter)
}
Compute both key and value
You can compute both the key and the value from the element. Here the key is the first letter and the value is the word length. Again the later T entry wins.
Output: Go program and captured output
package main
import (
"fmt"
"strings"
)
func main() {
numbers := []string{"one", "two", "three"}
lengthByLetter := make(map[string]int, len(numbers))
// the key is the first letter, the value is the word length
for _, word := range numbers {
lengthByLetter[strings.ToUpper(word[:1])] = len(word)
}
fmt.Println(lengthByLetter)
}
Key and value from one element
Sometimes one element carries both parts. strings.Fields splits a full name on spaces into a slice of words. The last name becomes the key and the first name becomes the value.
Output: Go program and captured output
package main
import (
"fmt"
"strings"
)
func main() {
names := []string{"Alice Adams", "Brian Brown"}
byLastName := make(map[string]string, len(names))
for _, fullName := range names {
parts := strings.Fields(fullName) // split on spaces
byLastName[parts[1]] = parts[0] // last name -> first name
}
fmt.Println(byLastName)
}
Flatten
Flattening turns a nested collection, such as a slice of slices, into one plain slice with all the elements in order.
Go has no flatten method, but append can add a whole slice at once: append(result, inner...). The three dots spread all elements of inner into the call.
Flatten a slice of slices
The loop visits each inner slice and appends all of its elements to result. Two inner slices of three numbers become one slice of six.
Output: Go program and captured output
package main
import ("fmt")
func main() {
numberSets := [][]int{{1, 2, 3}, {4, 5, 6}}
result := []int{}
// the ... spreads all elements of the inner slice into append
for _, inner := range numberSets {
result = append(result, inner...)
}
fmt.Println(result)
}
Flatten values from structs
A flat map first takes a slice out of every element, then joins all those slices together. In Go you read the field you want, c.Values, and append it with the three dots. All values from the three containers end up in one slice.
Output: Go program and captured output
package main
import ("fmt")
type Container struct {
Values []string
}
func main() {
containers := []Container{
{Values: []string{"one", "two", "three"}},
{Values: []string{"four", "five"}},
{Values: []string{"six", "seven", "eight"}},
}
result := []string{}
// take the Values of each container and add them all to one slice
for _, c := range containers {
result = append(result, c.Values...)
}
fmt.Println(result)
}
String Representation
Often you want to show a collection as readable text, for example with commas between the elements.
In Go, strings.Join takes a slice of strings and a separator and returns one string. The extras — a prefix, a postfix, a limit, or a transformation — are done with plain string concatenation or a short loop before joining. Numbers must be turned into strings first, for example with strconv.Itoa.
Join with a separator
strings.Join puts ", " between the words and returns one string.
Output: Go program and captured output
package main
import (
"fmt"
"strings"
)
func main() {
numbers := []string{"one", "two", "three"}
fmt.Println(strings.Join(numbers, ", "))
}
Add a prefix and a postfix
A prefix and a postfix are just text around the joined result. In Go you add them with +.
Output: Go program and captured output
package main
import (
"fmt"
"strings"
)
func main() {
numbers := []string{"one", "two", "three"}
// add the prefix and postfix around the joined text
text := "start: " + strings.Join(numbers, " | ") + ": end"
fmt.Println(text)
}
Limit the number of elements
For a long slice you may want to show only the first few elements. Take a slice of the first 10 with numbers[:10], turn each into a string with strconv.Itoa, then add "...>" at the end to show that more elements were cut off.
Output: Go program and captured output
package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
numbers := make([]int, 0, 100)
for i := 1; i <= 100; i++ {
numbers = append(numbers, i)
}
// take only the first 10 numbers, as strings
limit := 10
parts := make([]string, 0, limit)
for _, n := range numbers[:limit] {
parts = append(parts, strconv.Itoa(n))
}
if len(numbers) > limit {
parts = append(parts, "<...>")
}
fmt.Println(strings.Join(parts, ", "))
}
Transform each element before joining
To change every element before joining, build a new slice of strings in a loop, then call strings.Join on it. Here each word becomes "Element: " plus the word in uppercase.
Output: Go program and captured output
package main
import (
"fmt"
"strings"
)
func main() {
numbers := []string{"one", "two", "three"}
parts := make([]string, 0, len(numbers))
// change each word first, then join the results
for _, word := range numbers {
parts = append(parts, "Element: " + strings.ToUpper(word))
}
fmt.Println(strings.Join(parts, ", "))
}