Преобразование коллекции
Преобразование берёт коллекцию, например slice или map, и строит из неё новую коллекцию. Исходная коллекция при этом не меняется.
Во многих языках у списков и словарей есть готовые методы вроде map, zip, associate и flatten. В Go таких встроенных методов нет. Вместо этого вы пишете небольшой цикл for или маленькую вспомогательную функцию, которую можно использовать снова и снова. Это и есть подход Go, и он получается коротким, как только вы запомните приём.
Начиная с версии 1.18 в Go есть дженерики. Обобщённая функция работает с любым типом элементов. Примеры на этой странице используют дженерики (Go 1.21 или новее), поэтому одна вспомогательная функция подходит и для чисел, и для строк, и для структур. Стандартные пакеты slices, maps и strings тоже помогают в частых задачах.
Синтаксис обобщённой вспомогательной функции Map
Так выглядит вспомогательная функция Map, которую можно использовать повторно. T — это тип входных элементов, а R — тип элементов результата. Функция f вызывается один раз для каждого элемента, и каждый ответ добавляется в новый 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
Операция map применяет одну и ту же функцию к каждому элементу и собирает ответы в новый slice той же длины. Например, она может превратить [1 2 3] в [3 6 9].
В Go у слайсов нет метода map. Вы пишете цикл, который вызывает функцию для каждого элемента и с помощью append добавляет результат в новый slice. Если вы делаете так часто, вынесите цикл в обобщённую вспомогательную функцию.
Не путайте это с типом map в Go, который хранит пары ключ:значение. Здесь «map» означает «применить функцию к каждому элементу».
Map через вспомогательную функцию
Вспомогательная функция Map принимает slice и функцию. Она вызывает функцию для каждого элемента и возвращает новый slice. Здесь она умножает каждое число на 3.
Вывод: программа на Go и захваченный вывод
package main
import ("fmt")
// Map строит новый slice, применяя f к каждому элементу 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 с индексом
Иногда важна и позиция элемента. В Go range даёт и индекс, и значение, поэтому достаточно обычного цикла. Здесь каждое значение умножается на свой индекс.
Вывод: программа на Go и захваченный вывод
package main
import ("fmt")
func main() {
numbers := []int{1, 2, 3}
result := make([]int, 0, len(numbers))
// range даёт индекс и значение каждого элемента
for idx, value := range numbers {
result = append(result, value * idx)
}
fmt.Println(result)
}
Пропустить часть результатов
Иногда часть результатов нужно отбросить. У чисел в Go нет значения null, поэтому функция возвращает две вещи: результат и bool, который говорит, оставлять его или нет. Когда bool равен false, элемент пропускается.
Вывод: программа на Go и захваченный вывод
package main
import ("fmt")
// MapNotNil оставляет только те результаты, для которых f вернула 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 // пропускаем этот элемент
}
return x * 3, true
})
fmt.Println(result)
}
Преобразование ключей map
Чтобы изменить ключи map, создайте новый map и скопируйте в него каждую запись с новым ключом. Здесь strings.ToUpper переводит каждый ключ в верхний регистр. fmt.Println печатает map с отсортированными ключами, поэтому порядок вывода всегда одинаковый.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"strings"
)
func main() {
numbersMap := map[string]int{"key1": 1, "key2": 2}
upperKeys := make(map[string]int, len(numbersMap))
// копируем каждую запись, но переводим ключ в верхний регистр
for key, value := range numbersMap {
upperKeys[strings.ToUpper(key)] = value
}
fmt.Println(upperKeys)
}
Преобразование значений map
Чтобы изменить значения, сделайте то же самое, но оставьте ключ и вычислите новое значение. Здесь каждое значение увеличивается на длину своего ключа: 1 + len("key1") равно 5.
Вывод: программа на Go и захваченный вывод
package main
import ("fmt")
func main() {
numbersMap := map[string]int{"key1": 1, "key2": 2}
newValues := make(map[string]int, len(numbersMap))
// ключ оставляем, значение меняем
for key, value := range numbersMap {
newValues[key] = value + len(key)
}
fmt.Println(newValues)
}
Zip
Zip берёт два слайса и соединяет их элементы по позициям: первый с первым, второй со вторым и так далее. В результате получается столько пар, сколько элементов в более коротком слайсе.
В Go нет функции zip. Вы объявляете маленькую структуру для одной пары, находите меньшую длину встроенной функцией min и в цикле по индексу строите пары.
Zip двух слайсов
Структура Pair хранит один цвет и одно животное. Цикл идёт до меньшей длины и создаёт по одной Pair на каждый индекс. При печати слайса структур каждая пара показана в фигурных скобках.
Вывод: программа на Go и захваченный вывод
package main
import ("fmt")
// Pair хранит по одному элементу из каждого слайса.
type Pair struct {
Color string
Animal string
}
func main() {
colors := []string{"красный", "коричневый", "серый"}
animals := []string{"лиса", "медведь", "волк"}
// останавливаемся на более коротком слайсе
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 с преобразованием
Сами пары нужны не всегда. Здесь цикл сразу использует оба элемента и строит из них строку. Слово превращается в []rune, чтобы работать с буквами, а не с байтами: runes[:1] — это первая буква, которую strings.ToUpper переводит в верхний регистр, а runes[1:] — остаток слова.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"strings"
)
func main() {
colors := []string{"красная", "коричневый", "серый"}
animals := []string{"лиса", "медведь", "волк"}
n := min(len(colors), len(animals))
for i := 0; i < n; i++ {
// переводим в верхний регистр только первую букву названия животного
runes := []rune(animals[i])
name := strings.ToUpper(string(runes[:1])) + string(runes[1:])
fmt.Println(name + " — " + colors[i])
}
}
Unzip слайса пар
Unzip — обратная операция: один slice пар превращается в два слайса. Цикл читает каждую пару и добавляет её части в два отдельных слайса.
Вывод: программа на Go и захваченный вывод
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))
// разбиваем каждую пару на две части
for _, p := range numberPairs {
words = append(words, p.Word)
numbers = append(numbers, p.Number)
}
fmt.Println(words)
fmt.Println(numbers)
}
Associate
Associate строит map из слайса. Каждый элемент даёт один ключ и одно значение. Map не может хранить один и тот же ключ дважды, поэтому если два элемента дают одинаковый ключ, побеждает более поздний.
В Go нет функций associate. Создайте пустой map с помощью make, затем пройдите по слайсу и присвойте result[key] = value для каждого элемента.
Элементы как ключи
Сам элемент становится ключом, а значение вычисляется из него. Здесь слова — это ключи, а значения — их длина в байтах, которую возвращает len: у one — 3, у three — 5.
Вывод: программа на Go и захваченный вывод
package main
import ("fmt")
func main() {
numbers := []string{"one", "two", "three"}
lengths := make(map[string]int, len(numbers))
// каждое слово становится ключом, его длина — значением
for _, word := range numbers {
lengths[word] = len(word)
}
fmt.Println(lengths)
}
Элементы как значения
Элемент становится значением, а ключ вычисляется из него. Здесь ключ — это первая буква в верхнем регистре. И two, и three начинаются с T, поэтому three заменяет two, и в map остаётся только две записи.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"strings"
)
func main() {
numbers := []string{"one", "two", "three"}
byFirstLetter := make(map[string]string, len(numbers))
// ключ — первая буква; более позднее слово заменяет более раннее с тем же ключом
for _, word := range numbers {
byFirstLetter[strings.ToUpper(word[:1])] = word
}
fmt.Println(byFirstLetter)
}
Вычислить и ключ, и значение
Из элемента можно вычислить и ключ, и значение. Здесь ключ — это первая буква, а значение — длина слова. Снова побеждает более поздняя запись с ключом T.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"strings"
)
func main() {
numbers := []string{"one", "two", "three"}
lengthByLetter := make(map[string]int, len(numbers))
// ключ — первая буква, значение — длина слова
for _, word := range numbers {
lengthByLetter[strings.ToUpper(word[:1])] = len(word)
}
fmt.Println(lengthByLetter)
}
Ключ и значение из одного элемента
Иногда один элемент несёт обе части. strings.Fields разбивает полное имя по пробелам в slice слов. Фамилия становится ключом, а имя — значением.
Вывод: программа на Go и захваченный вывод
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) // разбиваем по пробелам
byLastName[parts[1]] = parts[0] // фамилия -> имя
}
fmt.Println(byLastName)
}
Flatten
Flatten превращает вложенную коллекцию, например slice слайсов, в один плоский slice со всеми элементами по порядку.
В Go нет метода flatten, но append умеет добавить сразу целый slice: append(result, inner...). Три точки раскрывают все элементы inner в этом вызове.
Flatten слайса слайсов
Цикл проходит по каждому вложенному слайсу и добавляет все его элементы в result. Два вложенных слайса по три числа превращаются в один slice из шести.
Вывод: программа на Go и захваченный вывод
package main
import ("fmt")
func main() {
numberSets := [][]int{{1, 2, 3}, {4, 5, 6}}
result := []int{}
// три точки раскрывают все элементы вложенного слайса в append
for _, inner := range numberSets {
result = append(result, inner...)
}
fmt.Println(result)
}
Flatten значений из структур
Операция flat map сначала берёт slice из каждого элемента, а затем соединяет все эти слайсы вместе. В Go вы читаете нужное поле, c.Values, и добавляете его с тремя точками. Все значения из трёх контейнеров попадают в один slice.
Вывод: программа на Go и захваченный вывод
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", "семь", "восемь"}},
}
result := []string{}
// берём Values каждого контейнера и добавляем их все в один slice
for _, c := range containers {
result = append(result, c.Values...)
}
fmt.Println(result)
}
Строковое представление
Часто коллекцию нужно показать как читаемый текст, например с запятыми между элементами.
В Go strings.Join принимает slice строк и разделитель и возвращает одну строку. Дополнения — префикс, постфикс, ограничение количества или преобразование — делаются обычной склейкой строк или коротким циклом перед объединением. Числа сначала нужно превратить в строки, например с помощью strconv.Itoa.
Объединение с разделителем
strings.Join ставит ", " между словами и возвращает одну строку.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"strings"
)
func main() {
numbers := []string{"one", "two", "three"}
fmt.Println(strings.Join(numbers, ", "))
}
Добавить префикс и постфикс
Префикс и постфикс — это просто текст вокруг объединённого результата. В Go он добавляется с помощью +.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"strings"
)
func main() {
numbers := []string{"one", "two", "three"}
// добавляем префикс и постфикс вокруг объединённого текста
text := "начало: " + strings.Join(numbers, " | ") + ": конец"
fmt.Println(text)
}
Ограничить количество элементов
Для длинного слайса иногда нужно показать только первые несколько элементов. Возьмите первые 10 элементов через numbers[:10], превратите каждый в строку с помощью strconv.Itoa, а затем добавьте в конец "...>", чтобы показать, что остальные элементы отброшены.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
numbers := make([]int, 0, 100)
for i := 1; i <= 100; i++ {
numbers = append(numbers, i)
}
// берём только первые 10 чисел, как строки
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, ", "))
}
Преобразовать каждый элемент перед объединением
Чтобы изменить каждый элемент перед объединением, соберите в цикле новый slice строк, а затем вызовите на нём strings.Join. Здесь каждое слово превращается в "Элемент: " плюс само слово в верхнем регистре.
Вывод: программа на Go и захваченный вывод
package main
import (
"fmt"
"strings"
)
func main() {
numbers := []string{"one", "two", "three"}
parts := make([]string, 0, len(numbers))
// сначала меняем каждое слово, потом объединяем результаты
for _, word := range numbers {
parts = append(parts, "Элемент: " + strings.ToUpper(word))
}
fmt.Println(strings.Join(parts, ", "))
}