Карты и Go
Карты в Go — это встроенный тип данных, предоставляющий возможность хранения пар ключ-значение. Они похожи на словари или ассоциативные массивы в других языках программирования.
Особенности карт в Go:
Динамический размер: Карты могут расти и уменьшаться по мере добавления и удаления элементов, что делает их гибкими для использования в ситуациях, когда количество элементов заранее неизвестно.
Типизированные ключи и значения: В картах Go и ключи, и значения могут быть почти любого типа, за исключением тех, которые содержат срезы, карты или другие несравнимые типы. Тип ключа и тип значения для карты указываются при её объявлении.
Быстрый доступ: Карты предоставляют быстрый доступ к значениям по ключам. Время доступа к элементу карты не зависит от размера карты, что делает их эффективным выбором для поиска данных.
Работа с картами:
package main
import (
"fmt"
)
func main() {
// Объявление карты
var myMap map[int]string
fmt.Println(myMap) // map[]
// Инициализация карты
myMap = make(map[int]string)
fmt.Println(myMap) // map[]
// Или можно объявить и инициализировать карту одновременно
myMap = make(map[int]string)
fmt.Println(myMap) // map[]
/*
Или можно объявить и инициализировать карту одновременно с емкостью
В отличие от слайсов, у карт нет понятия внешней емкости, доступной для проверки с помощью функции cap().
Это означает, что вызов cap(myMap) будет ошибочным, так как функция cap() не применима к картам.
Начальная емкость карты в Go служит лишь подсказкой для реализации о том, сколько элементов ожидается в карте.
Это может помочь оптимизировать ее внутреннюю структуру и уменьшить количество аллокаций памяти в
начальной фазе использования карты.
*/
myMap = make(map[int]string, 5)
fmt.Println(myMap, len(myMap)) // map[] 0
// Или можно объявить и инициализировать карту одновременно значениями
myMap = map[int]string{1: "a", 2: "b", 3: "c"}
fmt.Println(myMap) // map[1:a 2:b 3:c]
key := 5
// Добавить или изменить значение для ключа "apple"
myMap[key] = "apple"
fmt.Println(myMap) // map[1:a 2:b 3:c 5:apple]
// Получить значение. Если ключ существует, `ok` будет true, иначе false.
if _, ok := myMap[key]; !ok {
panic(fmt.Sprintf("myMap: key %d not found", key)) // panic: myMap: key 6 not found, если ключа 6 не окажется в карте
}
// Удалить элемент с ключом "apple"
// Ну и ни чего не происходит если такого ключа нет
delete(myMap, 6)
fmt.Println(myMap) // map[1:a 2:b 3:c 5:apple]
// Итерация по карте
for k, v := range myMap {
fmt.Println(k, v)
}
}
Подводные камни
1. Изменение карты во время итерации
Модификация карты во время итерации по ней может привести к непредсказуемому поведению. Например, вы можете пропустить некоторые элементы или столкнуться с паникой в рантайме.
Это допустимо в Go и не приведет к непосредственной ошибке или панике.
В случае с Go, поведение при удалении элементов из карты во время итерации по ней довольно определенное и безопасное. Итератор карты в Go спроектирован так, чтобы обрабатывать изменения карты во время итерации, но это может привести к тому, что некоторые элементы будут пропущены в процессе итерации, если вы удаляете элементы впереди текущего элемента итерации.
package main
func main() {
m := map[int]string{1: "a", 2: "b", 3: "c"}
for k := range m {
if k == 2 {
delete(m, 3) // Попытка удалить ключ во время итерации
}
}
// Поведение не определено. Возможно пропуск элементов или другие непредвиденные результаты.
}
Решение.
Для безопасного удаления элементов из карты во время итераций: сначала соберите ключи элементов, которые вы хотите удалить, в отдельный слайс, а затем удалите их в отдельном цикле после завершения итерации по карте. Это гарантирует, что итерация по карте не будет нарушена изменениями карты.
package main
import "fmt"
func main() {
m := map[int]string{1: "a", 2: "b", 3: "c"}
var keysToDelete []int
for k := range m {
if k == 2 {
// добавляем ключи для удаления
keysToDelete = append(keysToDelete, 3)
}
}
// Удаляем элементы после итерации
for _, k := range keysToDelete {
delete(m, k)
}
fmt.Println(m) // Вывод: map[1:a 2:b], элемент с ключом 3 удален
}
2. Неинициализированная карта
Попытка использования неинициализированной карты вызовет панику во время выполнения.
package main
func main() {
// Попытка использования неинициализированной карты вызовет панику во время выполнения.
var m map[int]string
m[1] = "apple" // panic: assignment to entry in nil map
// Чтобы избежать этого, карта должна быть инициализирована перед использованием
m = make(map[int]string)
m[1] = "apple" // OK
}
3. Проверка наличия ключа
При получении значения из карты, если ключ не существует, возвращаемое значение будет нулевым для типа значения карты. Это может ввести в заблуждение, если не проверять, действительно ли ключ существует.
package main
import (
"fmt"
)
func main() {
m := make(map[int]int)
k := 123
val := m[1] // val == 0, ключ 123 не существует, но 0 также может быть допустимым значением
fmt.Println(val)
// Лучше использовать двухзначную форму получения элемента:
if _, ok := m[k]; !ok {
fmt.Printf("map key: %d, not found\n", k)
}
}
4. Сортировка
В Go карты не имеют внутреннего порядка, и их элементы хранятся в случайном порядке. Однако вы можете отсортировать ключи (или значения) карты, используя дополнительные шаги. Вот как можно отсортировать карту по ключам:
package main
import (
"fmt"
"sort"
)
func main() {
m := map[string]int{
"banana": 3,
"apple": 5,
"pear": 2,
"orange": 4,
}
// Создаем слайс для хранения ключей
var keys []string
// Добавляем ключи в слайс
for k := range m {
keys = append(keys, k)
}
// Сортируем ключи
sort.Strings(keys)
// Выводим отсортированную карту
for _, k := range keys {
fmt.Println(k, m[k])
}
}
5. Конкурентная модификация
Карты в Go не являются потокобезопасными, и одновременная модификация карты из разных горутин может привести к состоянию гонки и панике.
package main
import "time"
func main() {
m := make(map[int]int)
// Имитация конкурентной модификации
go func() {
for i := 0; i < 10000; i++ {
m[i] = i
}
}()
go func() {
for i := 0; i < 10000; i++ {
delete(m, i)
}
}()
go func() {
for i := 0; i < 10000; i++ {
delete(m, i)
}
}()
// Возможна паника или другое непредсказуемое поведение
time.Sleep(time.Second * 100)
}
Решение.
Для предотвращения конкурентной модификации карты и обеспечения потокобезопасности, можно использовать мьютекс из пакета sync. Мьютекс позволяет блокировать доступ к ресурсу (в данном случае к карте) для всех горутин, кроме одной, которая в данный момент владеет мьютексом. Это гарантирует, что только одна горутина может изменять карту в любой момент времени.
Пример решения с использованием мьютекса:
package main
import (
"sync"
"time"
)
func main() {
var m = make(map[int]int) // Создаем карту
var mutex sync.Mutex // Создаем мьютекс
// Горутина для добавления элементов в карту
go func() {
for i := 0; i < 10000; i++ {
mutex.Lock() // Блокируем мьютекс перед модификацией карты
m[i] = i
mutex.Unlock() // Разблокируем мьютекс после модификации карты
}
}()
// Горутина для удаления элементов из карты
go func() {
for i := 0; i < 10000; i++ {
mutex.Lock() // Блокируем мьютекс перед модификацией карты
delete(m, i)
mutex.Unlock() // Разблокируем мьютекс после модификации карты
}
}()
// Даем горутинам время на выполнение
time.Sleep(time.Second)
}
В данном примере перед каждым изменением карты мьютекс блокируется вызовом mutex.Lock(), и разблокируется вызовом mutex.Unlock() после изменения. Это гарантирует, что в любой момент времени карта может быть изменена только одной горутиной, предотвращая конкурентную модификацию и потенциальные ошибки в программе.
Использование каналов: Каналы в Go могут использоваться не только для обмена данными между горутинами, но и как средство синхронизации. Вы можете создать канал, через который будут передаваться операции чтения и записи карты, а одна горутина будет отвечать за выполнение этих операций. Это гарантирует, что все операции с картой выполняются последовательно.
package main
import (
"sync"
)
type command struct {
key int
value int
op string // "set", "delete", "get"
}
func main() {
var opChan = make(chan command)
var wg sync.WaitGroup
// Горутина для управления картой
go func() {
m := make(map[int]int)
for op := range opChan {
switch op.op {
case "set":
m[op.key] = op.value
case "delete":
delete(m, op.key)
}
}
}()
// Горутина для добавления элементов
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
opChan <- command{key: i, value: i, op: "set"}
}
}()
// Горутина для удаления элементов
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
opChan <- command{key: i, op: "delete"}
}
}()
wg.Wait()
close(opChan)
}
sync.Map: В пакете sync есть специальная структура Map, предназначенная для использования в многопоточных средах без явного использования мьютексов для синхронизации. sync.Map имеет встроенные методы для безопасной работы с данными в конкурентных ситуациях.
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// Установка значения
m.Store(1, "a")
// Получение значения
if val, ok := m.Load(1); ok {
fmt.Println("Value:", val)
}
// Удаление значения
m.Delete(1)
}
Оба подхода имеют свои преимущества и недостатки. Использование каналов может быть более наглядным и понятным, но потребует больше кода для управления операциями. sync.Map более прост в использовании для простых случаев, но может быть менее гибким, чем полное управление синхронизацией через мьютексы или каналы.