Лекция 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
Массивы передаются по значению — копируются целиком при присваивании и при передаче в функцию:
На практике массивы используются редко — почти всегда нужны срезы. Прямое применение массивов: фиксированные буферы, ключи карт (срез не может быть ключом, а массив — может), хеши и подобные структуры.
Срезы (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] управляет ёмкостью результата:
Главная ловушка: общий массив¶
Все срезы, происходящие от одного массива, разделяют память:
Это полезно (zero-copy подсрезы), но опасно: модификация b может неожиданно изменить a. Если нужна независимая копия:
b := make([]int, len(a))
copy(b, a)
// или
b := append([]int{}, a...)
// или (Go 1.21+)
b := slices.Clone(a)
Удаление элемента по индексу¶
В Go нет встроенного метода, но есть идиома:
Или, начиная с Go 1.21:
Полезные функции из 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». Для различения:
Запомните этот шаблон — встретится в 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), но эта проверка чаще не нужна.
Итерация¶
Порядок итерации не определён и намеренно рандомизирован (чтобы код не зависел от случайного порядка). Хотите упорядоченный обход — сначала соберите ключи в срез и отсортируйте:
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¶
На практике почти всегда пишут &User{Name: "Bob"} — нагляднее.
Анонимные структуры¶
Можно создать структурный тип «на лету», без type:
Удобно для возврата одноразового результата или конфигурации в тестах.
Сравнение¶
Две структуры одного типа сравниваются ==, если все их поля сравнимы (срезы, карты, функции — несравнимы). Это, кстати, ещё одна причина, почему массив может быть ключом карты, а срез — нет.
Теги полей¶
Магические строки в обратных кавычках после типа поля — это теги. Они доступны через рефлексию и используются библиотеками (де)сериализации:
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 даёт делегирование. Теги — машинно-читаемые метаданные для (де)сериализации. В следующей лекции — методы и интерфейсы.