Включите исполнение JavaScript в браузере, чтобы запустить приложение.

Мапы в Go: Полное руководство для начинающих

В Go, как и во многих других языках, часто нужно хранить данные в формате ключ-значение. Для этого предназначен встроенный тип данных map. В статье объясняем, как создавать мапы, добавлять и изменять элементы.

Что такое мапы в Golang

map в Go — это неупорядоченная коллекция пар ключ-значение. Каждый ключ в мапе должен быть уникальным, и по такому ключу можно быстро получить или изменить связанное с ним значение.

Ключевые характеристики:

  1. Основная идея — хранение данных в виде ассоциаций.
  2. Элементы не хранятся в каком-либо определенном порядке. При переборе порядок элементов не гарантируется и может меняться от запуска к запуску.
  3. Тип ключа должен поддерживать операции сравнения (== и !=). Это могут быть строки, числа, булевы значения, указатели, структуры. Срезы, функции и сами мапы не могут быть ключами.
  4. Значения могут быть абсолютно любого типа, включая другие мапы или срезы.
  5. Мапы могут расти по мере добавления новых элементов.
  6. Мапы являются ссылочными типами. Это означает, что когда вы присваиваете мапу новую переменную или передаете ее в функцию, вы работаете с той же самой базовой структурой данных.

Нулевое значение для типа map — это nil. nil-мапа не содержит ключей, в нее нельзя добавлять элементы. Прежде чем использовать мапу, ее нужно инициализировать.

Создание map

Есть несколько способов:

С помощью встроенной функции make

Это наиболее распространенный способ создания пустой, готовой к использованию (не nil) мапы.

package main

import "fmt"

func main() {

// Создаем мапу, где ключ - строка (string), значение - целое число (int)

userAges := make(map[string]int)

// Можно указать начальную емкость (capacity) для оптимизации,

// если вы примерно знаете, сколько элементов будет в мапе.

// Это не ограничивает размер, мапа все равно будет расти при необходимости.

productPrices := make(map[int]float64, 10) // Емкость 10

fmt.Println(userAges)    // Вывод: map[]

fmt.Println(productPrices) // Вывод: map[]

// Важно: userAges и productPrices не nil, в них можно добавлять элементы.

userAges["Alice"] = 30

fmt.Println(userAges) // Вывод: map[Alice:30]

}
go

С помощью литерала map

Этот способ удобен, когда вы хотите сразу инициализировать мапу какими-то значениями.

package main

import "fmt"

func main() {

// Создаем и инициализируем мапу с парами ключ-значение

capitals := map[string]string{

"Россия": "Москва",

"США":    "Вашингтон", // Запятая в конце обязательна, если } на новой строке

"Япония": "Токио",

}

// Создание пустой, но инициализированной мапы с помощью литерала

emptyMap := map[string]int{} // Не nil, готова к использованию

fmt.Println(capitals)  // Вывод: map[Россия:Москва США:Вашингтон Япония:Токио] (порядок может отличаться!)

fmt.Println(emptyMap) // Вывод: map[]

emptyMap["test"] = 1

fmt.Println(emptyMap) // Вывод: map[test:1]

}
go

Объявление без инициализации

Если вы просто объявите переменную типа map без использования make или литерала, она будет равна nil.

package main

import "fmt"

func main() {

var nilMap map[string]int

fmt.Println(nilMap == nil) // Вывод: true

fmt.Println(len(nilMap))    // Вывод: 0

// Попытка добавить элемент в nil-мапу вызовет панику!

// nilMap["test"] = 1 // panic: assignment to entry in nil map

}
go

Всегда инициализируйте мапу с помощью make или литерала перед добавлением в нее элементов.

Работа с элементами

С элементами можно выполнять основные операции: добавление, изменение, получение, перебор и удаление.

Добавление и изменение

Добавление нового элемента или изменение значения существующего элемента выполняется с помощью одной и той же синтаксической конструкции:

mapName[key] = value
go

Go сам определяет, нужно ли добавить новую пару или обновить значение для уже существующего ключа.

package main

import "fmt"

func main() {

userStatus := make(map[string]string)

// Добавление элементов

userStatus["Alice"] = "online"

userStatus["Bob"] = "offline"

userStatus["Charlie"] = "away"

fmt.Println("После добавления:", userStatus)

// Вывод: После добавления: map[Alice:online Bob:offline Charlie:away]

// Изменение значения для существующего ключа

userStatus["Bob"] = "online" // Боб вернулся

fmt.Println("После изменения:", userStatus)

// Вывод: После изменения: map[Alice:online Bob:online Charlie:away]

// Добавление еще одного элемента

userStatus["David"] = "offline"

fmt.Println("После добавления David:", userStatus)

// Вывод: После добавления David: map[Alice:online Bob:online Charlie:away David:offline]

}
go

Получение

Чтобы получить значение по ключу, используется синтаксис:

value := mapName[key]
go

Здесь есть важный нюанс. Если ключ key не существует в мапе, то операция не вызовет ошибки, а вернет нулевое значение. Это может привести к путанице: значение действительно равно нулю или пустой строке, или такого ключа просто нет?

Для решения этой проблемы используется специальная форма присваивания: она возвращает само значение и булев флаг, указывающий, был ли ключ найден в мапе.

value, ok := mapName[key]

// ok == true, если ключ key существует в мапе

// ok == false, если ключ key отсутствует
go

Это предпочтительный способ получения значений, когда вы не уверены в наличии ключа.

package main

import "fmt"

func main() {

capitals := map[string]string{

"Россия": "Москва",

"Франция": "Париж",

}

// Прямое получение (может быть неоднозначным)

japanCapital := capitals["Япония"]

fmt.Printf("Столица Японии (прямое получение): '%s'\n", japanCapital) // Вывод: Столица Японии (прямое получение): '' (пустая строка - нулевое значение для string)

// Получение с проверкой наличия ("comma ok") - ПРАВИЛЬНЫЙ СПОСОБ

capitalFrance, okFrance := capitals["Франция"]

if okFrance {

fmt.Printf("Столица Франции: %s (найдено: %t)\n", capitalFrance, okFrance)

} else {

fmt.Printf("Столица Франции не найдена (найдено: %t)\n", okFrance)

}

// Вывод: Столица Франции: Париж (найдено: true)

capitalGermany, okGermany := capitals["Германия"]

if okGermany {

fmt.Printf("Столица Германии: %s (найдено: %t)\n", capitalGermany, okGermany)

} else {

fmt.Printf("Столица Германии не найдена (найдено: %t)\n", okGermany)

}

// Вывод: Столица Германии не найдена (найдено: false)

fmt.Printf("Значение для Германии (если не найдено): '%s'\n", capitalGermany) // Вывод: Значение для Германии (если не найдено): ''

}
go

Перебор

Для перебора всех пар ключ-значение используется цикл for range.

package main

import "fmt"

func main() {

scores := map[string]int{

"Alice": 95,

"Bob":   80,

"Charlie": 75,

}

fmt.Println("Перебор ключ-значение:")

for name, score := range scores {

fmt.Printf("Игрок: %s, Очки: %d\n", name, score)

}

// Порядок вывода не гарантирован

fmt.Println("\nПеребор только ключей:")

for name := range scores {

fmt.Println("Игрок:", name) // Получаем только ключ

}

fmt.Println("\nПеребор только значений:")

for _, score := range scores {

fmt.Println("Очки:", score) // Игнорируем ключ с помощью _

}

    // Получение количества элементов

    fmt.Println("\nКоличество элементов в мапе:", len(scores)) // Вывод: 3

}
go

Удаление

Для удаления элемента по ключу используется встроенная функция delete. Если ключ key существует в мапе, элемент будет удален. Если ключ key не существует, вызов delete не вызовет ошибки и ничего не сделает.

package main

import "fmt"

func main() {

permissions := map[string]bool{

"read":  true,

"write": true,

"exec":  false,

}

fmt.Println("До удаления:", permissions) // Вывод: До удаления: map[exec:false read:true write:true]

// Удаляем ключ "exec"

delete(permissions, "exec")

fmt.Println("После удаления 'exec':", permissions) // Вывод: После удаления 'exec': map[read:true write:true]

// Пытаемся удалить несуществующий ключ "admin" - ошибки не будет

delete(permissions, "admin")

fmt.Println("После попытки удаления 'admin':", permissions) // Вывод: После попытки удаления 'admin': map[read:true write:true]

}
go

Какие еще возможны операции

Кроме базовых операций создания, чтения, обновления, удаления и перебора, с мапами в Go можно выполнять и другие полезные действия. Нужно знать и о некоторых особенностях их поведения.

Получение размера мапы

Как и для срезов, вы можете легко узнать текущее количество пар ключ-значение с помощью встроенной функции len().

package main

import "fmt"

func main() {

params := map[string]string{

"user":  "guest",

"scope": "read-only",

}

fmt.Println("Начальный размер:", len(params)) // Вывод: Начальный размер: 2

params["token"] = "xyz123"

fmt.Println("Размер после добавления:", len(params)) // Вывод: Размер после добавления: 3

delete(params, "user")

fmt.Println("Размер после удаления:", len(params)) // Вывод: Размер после удаления: 2

emptyMap := make(map[int]int)

fmt.Println("Размер пустой мапы:", len(emptyMap)) // Вывод: Размер пустой мапы: 0

var nilMap map[string]int

fmt.Println("Размер nil мапы:", len(nilMap)) // Вывод: Размер nil мапы: 0

}
go

Передача мап в функции

Когда вы передаете мапу в функцию, функция получает копию ссылки на ту же самую базовую структуру данных. Это значит, что любые изменения, которые функция вносит в мапу, будут видны и вне этой функции, в исходной мапе.

package main

import "fmt"

// Функция добавляет или обновляет статус пользователя

func updateUserStatus(statuses map[string]string, user string, status string) {

fmt.Printf("  (Внутри функции) Мапа до изменения: %v\n", statuses)

statuses[user] = status // Изменяем мапу, переданную по ссылке

fmt.Printf("  (Внутри функции) Мапа после изменения: %v\n", statuses)

// Возвращать мапу не нужно, изменения уже внесены в оригинал

}

func main() {

userActivity := make(map[string]string)

userActivity["Alice"] = "typing..."

fmt.Println("Мапа до вызова функции:", userActivity)

updateUserStatus(userActivity, "Bob", "online") // Передаем мапу в функцию

fmt.Println("Мапа после вызова функции:", userActivity) // Изменения видны!

// Вывод:

// Мапа до вызова функции: map[Alice:typing...]

//   (Внутри функции) Мапа до изменения: map[Alice:typing...]

//   (Внутри функции) Мапа после изменения: map[Alice:typing... Bob:online]

// Мапа после вызова функции: map[Alice:typing... Bob:online]

}
go

Сравнение

Мапы в Go можно сравнивать только с nil. Попытка сравнить две не-nil мапы с помощью оператора == приведет к ошибке компиляции.

map1 := make(map[string]int)

map2 := make(map[string]int)

var nilMap map[string]int

fmt.Println(map1 == nil) // false

fmt.Println(nilMap == nil) // true

// fmt.Println(map1 == map2) // Ошибка компиляции: invalid operation: map1 == map2 (map can only be compared to nil)
go

Почему так? Потому что мапы — сложные структуры, и определение их равенства неочевидно. Если вам нужно сравнить содержимое двух таких структур, это нужно сделать вручную.

Заключение

Итак, мы подробно рассмотрели один из самых универсальных и часто используемых типов данных в Go — map. Это ваш основной инструмент для создания ассоциативных коллекций, где данные хранятся и извлекаются по уникальному ключу.

Давайте кратко подытожим ключевые моменты:

  • Всегда создавайте мапы с помощью make() или литерала ({}), чтобы избежать паники при работе с nil-мапами.
  • Используйте синтаксис map[key] для получения значения и map[key] = value для добавления/обновления. Применяйте идиому value, ok := map[key] для безопасной проверки наличия ключа.
  • Цикл for range позволяет перебирать ключи и значения, но помните, что порядок не гарантирован.
  • Функция delete(map, key) безопасно удаляет элемент по ключу.
  • Передача мапы в функцию позволяет изменять оригинал. Мапы нельзя сравнивать напрямую (кроме как с nil).

Мапы находят применение в самых разных задачах: от подсчета частоты слов и хранения конфигураций до кэширования данных и представления JSON-объектов. Понимание того, как они работают и каковы их особенности, критически важно для написания надежного и эффективного Go-кода.