Основы словарей (Dictionary) в Go

GolangBeginner
Практиковаться сейчас

Введение

Добро пожаловать, Гоферы (Gophers), в новую главу.

В этой главе мы узнаем о базовом использовании карт (maps) и проанализируем наличие nil в Go.

После завершения этой главы вы сможете применять карты в своих повседневных разработках.

Точки знания:

  • Объявление карты
  • Инициализация карты
  • Добавление, удаление, обновление и поиск элементов карты
  • Проверка существования элемента карты
  • Итерация по карте

Введение в словари (Dictionary)

Итак, что такое словарь?

В информатике словарь представляет собой специальную структуру данных, состоящую из пар ключ-значение (key-value pairs).

Пара ключ-значение: Пара элементов, где один элемент называется ключом, а другой - значением.

Так зачем же нам использовать словарь?

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

Описание изображения

Объявление словарей (Maps)

Для новой структуры данных первым шагом является изучение того, как ее объявить.

Для map способ ее определения выглядит так:

var variableName map[keyType]valueType

Например:

var a map[string]int

Однако, так как map является ссылочным типом, если мы объявим ее, как в коде выше, мы столкнемся с проблемой.

Создадим файл map.go в директории ~/project:

touch ~/project/map.go

Напишем следующий код в файле map.go:

package main

func main() {
    // Declare a map named m with key type as string and value type as int
    var m map[string]int
    // Add a data entry "labex"->1 to it
    m["labex"] = 1 // Program crashes
}

Запустим программу:

go run map.go

Мы обнаружили, что программа аварийно завершилась и выдала ошибку:

panic: assignment to entry in nil map

Это означает, что присвоение элемента nil map является ошибочным поведением.

Это происходит потому, что для таких структур данных, как срезы (slices), карты (maps), каналы (channels) и указатели (pointers), они должны быть инициализированы перед использованием.

А nil является начальным значением по умолчанию для карты, что означает, что при определении переменной не выделяется память.

Начальное значение nil

В предыдущем разделе мы упомянули, что присвоение элемента nil-карте (map) является ошибочным поведением.

Возьмем это в руки и исследуем истинный смысл nil в Go.

По сути, nil представляет собой предварительно объявленный идентификатор.

Для базовых типов данных их начальные значения различны:

  • Логические значения
  • Числовые значения
  • Строки

Однако для срезов (slices), словарей (dictionaries), указателей (pointers), каналов (channels) и функций их начальным значением является nil. Это не то начальное значение по умолчанию, с которым мы привыкли.

Это проявляется в том, что объекты, которым присвоено nil, можно напечатать, но нельзя использовать.

Кроме того, есть несколько моментов, которые нужно учитывать при работе с nil.

Несравнимость различных типов nil

package main

import "fmt"

func main() {
    var m map[int]string
    var p *int
    fmt.Printf("%v", m == p)
}

Вывод программы выглядит следующим образом:

invalid operation: m == p (mismatched types map[int]string and *int)

То есть мы не можем сравнить nil указателя на тип int с nil карты.

Они не сравнимы.

nil не является ключевым словом

package main

import "fmt"

func main() {
    var nilValue = "= =$"
    fmt.Println(nilValue)
}

Вывод программы выглядит следующим образом:

= =$

Мы можем определить переменную с именем nil, и программа скомпилируется без ошибок. Однако мы настоятельно рекомендуем вам не делать этого в реальной разработке.

Несравнимость nil

package main

import "fmt"

func main() {
    fmt.Println(nil == nil)
}

Вывод программы выглядит следующим образом:

invalid operation: nil == nil (operator == not defined on nil)

Объявление с использованием ключевого слова make

После понимания значения nil давайте решим проблему выделения памяти для словарей (dictionaries).

Здесь мы используем ключевое слово make для выделения памяти.

Функция make создает значение указанного типа, которое уже инициализировано, и возвращает его.

package main

import "fmt"

func main() {
    var m map[string]int     // Declare a dictionary
    m = make(map[string]int) // Allocate memory to the dictionary
    m["labex"] = 1       // Add data to the dictionary
    fmt.Println(m)
}

Вывод программы выглядит следующим образом:

map[labex:1]

В приведенном выше коде показано, как выделить память для уже объявленного словаря с использованием ключевого слова make.

Мы также можем упростить этот процесс:

Напишите следующий код в файле map.go:

package main

import "fmt"

func main() {
    m := make(map[string]int) // Declare and initialize a dictionary
    m["labex"] = 666      // Add data to the dictionary
    fmt.Println(m)
}

Вывод программы выглядит следующим образом:

map[labex:666]

В приведенном выше коде показано, как объявить словарь с использованием ключевого слова make.

Ручная инициализация пустого словаря (Map)

В предыдущем разделе мы показали, как использовать ключевое слово make для инициализации карты. Теперь давайте рассмотрим другой метод: использование литерального синтаксиса для ручной инициализации пустой карты.

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

Напишите следующий код в файле map.go:

package main

import "fmt"

func main() {
    // Using literal syntax to initialize an empty map
    m := map[string]int{}

    // Adding elements to the map
    m["labex"] = 777
    m["labs"] = 11

    fmt.Println(m) // Output the map with added elements
}

Синтаксис map[keyType]valueType{} создает пустую карту, готовую к использованию. После инициализации вы можете добавлять элементы в карту с помощью синтаксиса map[key] = value.

При запуске вышеуказанного кода программа выведет:

map[labex:777 labs:11]

Преимущества ручной инициализации:

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

Фактическая инициализация словаря

Поскольку мы можем инициализировать пустой словарь, мы также можем задать ему некоторые начальные значения.

Напишите следующий код в файле map.go:

package main

import "fmt"

func main() {
    m := map[string]int{
        "labex": 777,
        "labs": 11,
    }
    m["labby"] = 666
    fmt.Println(m)
}

Вывод программы выглядит следующим образом:

map[labby:666 labs:11 labex:777]

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

Добавление и обновление элементов словаря

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

Синтаксис выглядит следующим образом:

dictionaryInstance[keyToAdd] = valueToAdd

Например:

m := map[string]int{}
m["labby"] = 666

Примечание: В Go каждый key в map должен быть уникальным.

Напишите следующий код в файле map.go:

package main

import "fmt"

func main() {
    m := map[string]int{}
    m["labby"] = 666
    m["labby"] = 777
    fmt.Println(m)
}

Вывод программы выглядит следующим образом:

map[labby:777]

Мы заметили, что значение в выводе изменилось на 777. То есть, если мы записываем разные значения для одного и того же key, соответствующее value будет обновлено до нового значения.

Вот как мы обновляем или изменяем словарь.

Удаление элементов словаря

Как можно удалить элемент из словаря?

Нам нужно использовать встроенную функцию delete.

Напишите следующий код в файле map.go:

package main

import "fmt"

func main() {
    m := map[string]int{
        "labex": 777,
        "labs": 11,
        "labby": 666,
    }
    fmt.Println(m)
    delete(m, "labs")
    fmt.Println(m)
}

Вывод программы выглядит следующим образом:

map[labby:666 labs:11 labex:777]
map[labby:666 labex:777]

Функция delete принимает два аргумента. Первый аргумент - это словарь, над которым будет выполняться операция, а второй аргумент - это ключ, который нужно удалить.

Конечно, функция delete имеет и другие применения, но в этом практическом занятии мы рассмотрим только ее использование в контексте словарей.

Поиск элементов словаря

Что происходит, когда мы ищем в словаре элемент, который не существует?

package main

import "fmt"

func main() {
    m := map[string]int{
        "labex": 0,
    }
    fmt.Print("The value of labs is: ")
    fmt.Println(m["labs"])
}

Вывод программы выглядит следующим образом:

The value of labs is: 0

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

А что насчет ключа в словаре, значение которого равно 0?

package main

import "fmt"

func main() {
    m := map[string]int{
        "labex": 0,
    }
    fmt.Print("The value of labs is: ")
    fmt.Println(m["labs"])
    fmt.Print("The value of labex is: ")
    fmt.Println(m["labex"])
}

Вывод программы выглядит следующим образом:

The value of labs is: 0
The value of labex is: 0

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

Это создает нам множество путаницы. Как же это решить?

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

То есть:

labs, ok := m["labs"]

Измените файл map.go:

package main

import "fmt"

func main() {
    m := map[string]int{
        "labex": 0,
    }
    labs, ok := m["labs"]
    fmt.Print("The value of labs is: ")
    fmt.Print(labs)
    fmt.Print(" Does it exist? ")
    fmt.Println(ok)
    labex, ok2 := m["labex"]
    fmt.Print("The value of labex is: ")
    fmt.Print(labex)
    fmt.Print(" Does it exist? ")
    fmt.Println(ok2)
}

Вывод программы выглядит следующим образом:

The value of labs is: 0 Does it exist? false
The value of labex is: 0 Does it exist? true

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

Перебор элементов словаря

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

package main

import (
    "fmt"
)

func main() {
    m := map[string]int{
        "labex": 777,
        "labs":   11,
        "labby":     666,
    }
    for key, value := range m {
        fmt.Println(key, value)
    }
}

Вывод программы выглядит следующим образом:

labby 666
labs 11
labex 777

Из вывода можно видеть, что способ итерации по словарю очень похож на итерацию по срезам (slices) или массивам (arrays).

Резюме

В этом практическом занятии мы изучили базовое использование словарей (Dictionary), в том числе:

  • Объявление словарей и способы решения проблем при объявлении
  • Характеристики начального значения nil
  • Методы инициализации словарей
  • Добавление, удаление, обновление и поиск элементов в словарях
  • Проверка существования элементов в словарях
  • Итерация по словарям