Структуры данных — это способы организации данных в программе. В Go встроены мощные и простые структуры: массивы, срезы (slices), карты (maps) и структуры (structs). Они позволяют хранить и обрабатывать данные, от списков чисел до сложных объектов, таких как профили пользователей.

💬 Зачем это нужно?
Представь, что ты хранишь информацию о книгах: название, автор, количество страниц. Вместо множества переменных лучше использовать структуры данных, чтобы всё было организовано и удобно.


🗃 Массивы

Массивы — это наборы элементов одного типа с фиксированным размером, заданным при создании.

Объявление массива

package main

import "fmt"

func main() {
    var numbers [4]int // Массив из 4 целых чисел
    numbers[0] = 10
    numbers[1] = 20
    fmt.Println(numbers) // [10 20 0 0]
}

Короткий синтаксис

numbers := [4]int{10, 20, 30, 40}
fmt.Println(numbers) // [10 20 30 40]

Особенности

  • Размер массива — часть его типа ([4]int[5]int).
  • Элементы, не инициализированные явно, будут иметь стандартное значение (например, 0 для int, "" для string).

⚠️ Массивы редко используются напрямую из-за фиксированного размера. Чаще применяются срезы.


🔪 Срезы (Slices)

Срезы — это динамические массивы, которые могут расти или уменьшаться. Они создаются на основе массива, но Go управляет их размером автоматически.

Создание среза

package main

import "fmt"

func main() {
    slice := []int{1, 2, 3} // Срез без фиксированного размера
    fmt.Println(slice)      // [1 2 3]
}

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

Функция append добавляет элементы в срез:

slice = append(slice, 4, 5)
fmt.Println(slice) // [1 2 3 4 5]

Длина и ёмкость

  • len(slice) — возвращает текущую длину (число элементов).
  • cap(slice) — возвращает ёмкость (сколько элементов срез может вместить без перевыделения памяти).
fmt.Println(len(slice), cap(slice)) // 5 6

Как увеличивается ёмкость?

Когда срез заполняет свою ёмкость, Go выделяет новый массив с большей ёмкостью. Обычно ёмкость удваивается до определённого порога (примерно 1024 элемента), а затем растёт линейно (примерно на 25%). Это снижает количество перевыделений памяти.

Пример:

package main

import "fmt"

func main() {
    slice := make([]int, 0, 2) // Длина 0, ёмкость 2
    fmt.Printf("Начало: len=%d, cap=%d, %v\n", len(slice), cap(slice), slice)
    slice = append(slice, 1) // len=1, cap=2
    fmt.Printf("После 1: len=%d, cap=%d, %v\n", len(slice), cap(slice), slice)
    slice = append(slice, 2, 3) // len=3, cap=4 (ёмкость удвоилась)
    fmt.Printf("После 2,3: len=%d, cap=%d, %v\n", len(slice), cap(slice), slice)
}

Вывод:

Начало: len=0, cap=2, []
После 1: len=1, cap=2, [1]
После 2,3: len=3, cap=4, [1 2 3]

💡 Используйте make с начальной ёмкостью (make([]int, 0, n)), чтобы минимизировать перевыделение памяти.


🗺 Карты (Maps)

Карты — это словари, хранящие пары “ключ-значение”. Ключи уникальны, а типы ключей и значений задаются при создании.

Создание и работа с картой

package main

import "fmt"

func main() {
    scores := map[string]int{
        "Алиса": 90,
        "Боб":   85,
    }
    scores["Катя"] = 95 // Добавление
    delete(scores, "Боб") // Удаление
    fmt.Println(scores) // map[Алиса:90 Катя:95]
}

Проверка ключа

value, exists := scores["Алиса"]
if exists {
    fmt.Println("Оценка Алисы:", value) // Оценка Алисы: 90
} else {
    fmt.Println("Ключ не найден")
}

Итерация по карте

Используйте for ... range для перебора ключей и значений:

for name, score := range scores {
    fmt.Printf("%s: %d\n", name, score)
}

Вывод:

Алиса: 90
Катя: 95

⚠️ Порядок итерации по карте не гарантирован — Go специально рандомизирует его. Карта должна быть инициализирована (make или {}), иначе возникнет ошибка nil map.

Пример: Подсчёт частоты

Карты отлично подходят для подсчёта данных, например, частоты символов в строке:

package main

import "fmt"

func main() {
    text := "hello"
    charCount := make(map[rune]int)
    for _, char := range text {
        charCount[char]++
    }
    fmt.Println(charCount) // map[e:1 h:1 l:2 o:1]
}

🏗 Структуры (Structs)

Структуры — это пользовательские типы, объединяющие поля для моделирования сложных объектов.

Объявление и использование

package main

import "fmt"

type Person struct {
    Name string
    Age  int
    Address struct { // Вложенная структура
        City  string
        Zip   string
    }
}

func main() {
    p := Person{
        Name: "Алиса",
        Age:  25,
        Address: struct {
            City string
            Zip  string
        }{City: "Москва", Zip: "101000"},
    }
    fmt.Println(p.Address.City) // Москва
}

Методы для структур

Структуры могут иметь методы — функции, привязанные к типу:

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func (p Person) Greet() string {
    return "Привет, я " + p.Name + "!"
}

func main() {
    p := Person{Name: "Боб", Age: 30}
    fmt.Println(p.Greet()) // Привет, я Боб!
}

Анонимные структуры

Для временных данных можно использовать анонимные структуры:

package main

import "fmt"

func main() {
    point := struct {
        X, Y int
    }{X: 10, Y: 20}
    fmt.Println(point) // {10 20}
}

💡 Структуры подходят для моделирования сложных сущностей (например, пользователей, заказов). Методы добавляют поведение, делая структуры похожими на объекты.


📚 Полезные советы

  • Массивы: Используйте для фиксированных данных (например, координаты).
  • Срезы: Задавайте начальную ёмкость через make для больших данных.
  • Карты: Проверяйте наличие ключа и используйте for ... range для итерации. Инициализируйте карту перед использованием.
  • Структуры: Добавляйте методы для поведения и используйте вложенные структуры для сложных данных, но избегайте избыточной вложенности.
  • Оптимизация: Следите за cap срезов и очищайте карты с помощью delete для экономии памяти.

🧠 Правильный выбор структуры данных упрощает код и повышает производительность.


🧪 Пример программы

package main

import "fmt"

func main() {
    // Массив
    arr := [3]int{10, 20, 30}
    fmt.Println("Массив:", arr)

    // Срез
    slice := make([]int, 0, 2)
    slice = append(slice, 1, 2, 3)
    fmt.Printf("Срез: %v, len=%d, cap=%d\n", slice, len(slice), cap(slice))

    // Карта
    grades := map[string]int{"Математика": 5, "Физика": 4}
    for subject, grade := range grades {
        fmt.Printf("Предмет: %s, оценка: %d\n", subject, grade)
    }

    // Структура
    type Student struct {
        Name   string
        Grades map[string]int
    }
    s := Student{Name: "Алиса", Grades: grades}
    fmt.Printf("Студент: %s, оценки: %v\n", s.Name, s.Grades)
}

🔍 Вопросы для самопроверки

  1. Чем отличается массив от среза в Go?
  2. Как работает увеличение ёмкости среза при использовании append?
  3. Как перебрать все элементы карты?
  4. Как добавить метод к структуре?
  5. Что выведет cap([]int{1, 2, 3})?
  6. Как создать анонимную структуру и зачем она нужна?
  7. Как удалить ключ из карты?

📌 Главное из главы

  • Массивы — фиксированные наборы элементов одного типа.
  • Срезы — динамические массивы, ёмкость которых удваивается при переполнении (до ~1024 элементов).
  • Карты — словари для пар “ключ-значение”, поддерживающие итерацию и удаление ключей.
  • Структуры — пользовательские типы с полями и методами, включая вложенные и анонимные структуры.
  • Правильный выбор структуры данных улучшает читаемость и производительность.

🛠 Упражнение

Напишите программу, которая:

  1. Создаёт срез чисел {1, 2, 3} с начальной ёмкостью 2 и добавляет 4, выводя len и cap.
  2. Создаёт карту для подсчёта частоты букв в строке "hello".
  3. Создаёт структуру Book с полями Title (строка), Pages (число) и методом Info, возвращающим строку вида "Книга: <Title>, страниц: <Pages>".
  4. Выводит результаты.