Перейти к содержанию

Лекция 4. Композитные типы: массивы, срезы, карты, структуры

Массивы

Массив в Go имеет фиксированную длину, заданную на этапе компиляции, и эта длина — часть типа.

var a [3]int                  // [0, 0, 0]
b := [3]int{10, 20, 30}
c := [...]int{1, 2, 3, 4}     // длина выводится автоматически (4)

fmt.Println(len(a), len(b))   // 3 3

Массивы передаются по значению — копируются целиком при присваивании и при передаче в функцию:

a := [3]int{1, 2, 3}
b := a       // полная копия
b[0] = 100
fmt.Println(a[0])  // 1 (не изменилось)

На практике массивы используются редко — почти всегда нужны срезы. Прямое применение массивов: фиксированные буферы, ключи карт (срез не может быть ключом, а массив — может), хеши и подобные структуры.

Срезы (slices) — главная коллекция Go

Срез — это окно поверх массива. Внутри он представлен тремя полями:

  • указатель на начало;
  • длина (len);
  • ёмкость (cap) — сколько элементов в подлежащем массиве с этой позиции.
nums := []int{10, 20, 30}        // литерал среза
empty := []int{}                  // пустой срез (len=0, cap=0)
var nilSlice []int                // nil-срез (len=0, cap=0, но == nil)

make — создание среза с резервом

s := make([]int, 5)        // [0 0 0 0 0], len=5, cap=5
s = make([]int, 5, 10)     // len=5, cap=10 (под капотом массив на 10)

make с дополнительным cap важен, когда вы заранее знаете размер: без него append будет периодически переаллоцировать массив, копируя данные.

append — добавление элементов

s := []int{1, 2, 3}
s = append(s, 4)
s = append(s, 5, 6, 7)
other := []int{100, 200}
s = append(s, other...)   // распаковка среза в variadic-параметры

append возвращает (возможно, новый) срез. Старый срез нужно перезаписать. Если len + N <= cap, элементы дописываются в существующий массив. Если нет — выделяется новый массив (обычно удвоенной ёмкости), данные копируются.

Слайсинг

s := []int{10, 20, 30, 40, 50}
fmt.Println(s[1:3])   // [20 30]   индексы 1..2
fmt.Println(s[:2])    // [10 20]
fmt.Println(s[2:])    // [30 40 50]
fmt.Println(s[:])     // полная копия по «окну», но не копия данных

Полная форма s[low:high:max] управляет ёмкостью результата:

s := []int{1, 2, 3, 4, 5}
t := s[1:3:3]   // len=2, cap=2 — будущие append'ы не затронут s

Главная ловушка: общий массив

Все срезы, происходящие от одного массива, разделяют память:

a := []int{1, 2, 3, 4, 5}
b := a[1:4]
b[0] = 999
fmt.Println(a)   // [1 999 3 4 5]

Это полезно (zero-copy подсрезы), но опасно: модификация b может неожиданно изменить a. Если нужна независимая копия:

b := make([]int, len(a))
copy(b, a)
// или
b := append([]int{}, a...)
// или (Go 1.21+)
b := slices.Clone(a)

Удаление элемента по индексу

В Go нет встроенного метода, но есть идиома:

s := []int{10, 20, 30, 40, 50}
i := 2
s = append(s[:i], s[i+1:]...)   // [10 20 40 50]

Или, начиная с Go 1.21:

import "slices"
s = slices.Delete(s, i, i+1)

Полезные функции из slices (Go 1.21+)

slices.Contains(s, 42)
slices.Index(s, 42)
slices.Sort(s)
slices.SortFunc(s, func(a, b Item) int { return cmp.Compare(a.Name, b.Name) })
slices.Reverse(s)
slices.Equal(a, b)

Раньше всё это писали вручную или через sort.Slice.

Карты (maps)

Хеш-таблица. Аналог Python dict.

m := map[string]int{
    "apple":  1,
    "banana": 2,
}

m["cherry"] = 3
delete(m, "apple")

fmt.Println(m["banana"])  // 2
fmt.Println(m["xxxxx"])   // 0 — zero value типа значения, без ошибки!

Идиома comma-ok

Обращение к несуществующему ключу не падает, но и не отличает «ключа нет» от «значение равно zero». Для различения:

v, ok := m["xxxxx"]
if !ok {
    fmt.Println("ключа нет")
} else {
    fmt.Println("значение:", v)
}

Запомните этот шаблон — встретится в Go буквально везде (карты, type assertions, чтение из канала).

make и nil карта

var m map[string]int     // nil-карта — читать можно, ПИСАТЬ нельзя (panic)
m["x"] = 1               // panic: assignment to entry in nil map

m = make(map[string]int)
m["x"] = 1               // ОК

Карту всегда создавайте через make или литерал. Сравнивать карту с nil — можно (if m == nil), но эта проверка чаще не нужна.

Итерация

for k, v := range m {
    fmt.Println(k, v)
}

Порядок итерации не определён и намеренно рандомизирован (чтобы код не зависел от случайного порядка). Хотите упорядоченный обход — сначала соберите ключи в срез и отсортируйте:

keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
slices.Sort(keys)
for _, k := range keys {
    fmt.Println(k, m[k])
}

Set через карту

В Go нет встроенного множества. Идиома — map[T]struct{}:

seen := make(map[string]struct{})
seen["apple"] = struct{}{}

if _, ok := seen["apple"]; ok {
    fmt.Println("есть")
}

struct{} — пустая структура, занимает 0 байт. Альтернативно используют map[T]bool, это чуть удобнее по синтаксису, но тратит лишний байт на значение.

Структуры

type User struct {
    ID       int
    Name     string
    Email    string
    Active   bool
}

u := User{ID: 1, Name: "Alice", Email: "alice@example.com", Active: true}
// или позиционно (хрупко — лучше так не делать)
u := User{1, "Alice", "alice@example.com", true}

fmt.Println(u.Name)
u.Email = "new@example.com"

Структуры передаются по значению — копируются при присваивании и передаче в функцию. Чтобы менять оригинал, нужны указатели:

func deactivate(u *User) {
    u.Active = false   // Go сам разыменует — писать (*u).Active не нужно
}

deactivate(&u)

Создание через new

u := new(User)   // эквивалент &User{}, поля имеют zero values
u.Name = "Bob"

На практике почти всегда пишут &User{Name: "Bob"} — нагляднее.

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

Можно создать структурный тип «на лету», без type:

point := struct {
    X, Y int
}{X: 10, Y: 20}

Удобно для возврата одноразового результата или конфигурации в тестах.

Сравнение

Две структуры одного типа сравниваются ==, если все их поля сравнимы (срезы, карты, функции — несравнимы). Это, кстати, ещё одна причина, почему массив может быть ключом карты, а срез — нет.

Теги полей

Магические строки в обратных кавычках после типа поля — это теги. Они доступны через рефлексию и используются библиотеками (де)сериализации:

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email,omitempty"`
    Hash  string `json:"-"`              // не сериализовать
}

Один из самых частых тегов — json:"...". Также: yaml:"...", db:"...", form:"...", validate:"...". Полнее про JSON — в лекции 4 темы 14.

Анализ тегов вручную

import "reflect"

t := reflect.TypeOf(User{})
f, _ := t.FieldByName("Email")
fmt.Println(f.Tag.Get("json"))  // "email,omitempty"

Обычно это не нужно — за вас работает библиотека (encoding/json и т. п.).

Embedding (встраивание)

В Go нет наследования, но есть встраивание структур, которое даёт похожий эффект:

type Animal struct {
    Name string
}

func (a *Animal) Greet() {
    fmt.Println("Привет, я", a.Name)
}

type Dog struct {
    Animal     // embedded — поле без имени
    Breed string
}

d := Dog{Animal: Animal{Name: "Рекс"}, Breed: "лабрадор"}
d.Greet()      // Привет, я Рекс — метод проброшен
fmt.Println(d.Name)  // Рекс — поле тоже доступно напрямую

Это композиция, а не наследование: Dog не «является» Animal, у него просто есть встроенное поле Animal, и Go разрешает обращаться к его полям и методам через точку без явного .Animal.. Если в Dog объявить свой метод Greet, он перекроет проброшенный (но к старому всё ещё можно обратиться через d.Animal.Greet()).

Embed интерфейсов

Можно встроить интерфейс — это даёт автоматическую реализацию через делегирование (см. лекцию 5).

iota + const для перечислений

Уже обсуждали в лекции 2, но напомним применение со структурами:

type Status int

const (
    StatusPending Status = iota
    StatusActive
    StatusBanned
)

func (s Status) String() string {
    return [...]string{"pending", "active", "banned"}[s]
}

fmt.Println(StatusActive)  // "active"

Реализация метода String() (из интерфейса fmt.Stringer) автоматически подхватывается fmt.Println.

Параллель с Python

Python Go
list (динамический) срез []T
tuple (неизменяемый) массив [N]T или структура
dict карта map[K]V
set map[T]struct{}
dataclass, NamedTuple struct { ... }
obj.field — атрибут obj.Field — поле
list[1:3] — копия s[1:3] — окно поверх того же массива
list.append(x) s = append(s, x) (нужно перезаписать!)
del d[k] delete(m, k)
исключение KeyError возвращает zero value (v, ok := m[k] для проверки)
наследование классов embedding (композиция)

Итог

Массив — фиксированный размер, копия по значению. Срез — окно поверх массива, передаётся «дёшево», но требует осторожности при разделении памяти. Карта — хеш-таблица с zero value для отсутствующих ключей и идиомой comma-ok. Структуры — простая композиция полей, без наследования; embedding даёт делегирование. Теги — машинно-читаемые метаданные для (де)сериализации. В следующей лекции — методы и интерфейсы.