Вибір структури даних¶
Тепер у вас є чотири будівельні блоки для моделювання даних: власні типи, структури, зрізи та map (плюс масиви, до яких напряму вдаєшся рідко). Реальні програми поєднують їх. Ця стаття — практичний путівник із вибору правильного блока — і правильної комбінації — для конкретного завдання.
Путівник в один рядок¶
| Вам потрібно… | Беріть |
|---|---|
| тримати впорядковану послідовність, ітерувати, дозволяти дублікати, звертатися за позицією | зріз []T |
| шукати щось за ключем, перевіряти наявність, рахувати чи групувати | map map[K]V |
| описати одну річ із фіксованим набором іменованих полів різних типів | структуру |
| дати значенню окрему назву/поведінку поверх наявного типу (одиниці, ID, переліки) | власний тип type T U |
| буфер фіксованого розміру, порівнюваний, що копіюється за значенням | масив [N]T (рідко) |
Решта статті розкриває кожен із них, а потім показує, як вони поєднуються.
Зрізи — типова колекція¶
Якщо вам просто потрібна «купа значень по порядку», беріть зріз. Це
робоча конячка: впорядкований, нарощуваний через append, дешевий для
ітерування й розкладений у пам'яті неперервно (дружній до кешу).
scores := []int{90, 85, 90}
scores = append(scores, 70)
total := 0
for _, s := range scores {
total += s
}
fmt.Println(total) // output: 335
Його слабке місце — пошук: з'ясувати, чи присутнє значення, або знайти його за якимось ключем, — це O(n)-перебір. Для малих чи рідко шуканих колекцій це нормально — не хапайтеся за map лише тому, що у вас є зріз.
Map — доступ за ключем¶
Беріть map, коли ви шукаєте за ключем, а не обходите всю колекцію. У середньому O(1) на get/set. Чотири класичні задачі:
Пошук за ключем:
Підрахунок (частотність): нульове значення відсутнього ключа робить
++ робочим одразу:
counts := map[string]int{}
for _, w := range []string{"go", "py", "go"} {
counts[w]++
}
fmt.Println(counts["go"]) // output: 2
Групування (один-до-багатьох) через map[K][]V:
byParity := map[string][]int{}
for _, n := range []int{1, 2, 3, 4} {
if n%2 == 0 {
byParity["even"] = append(byParity["even"], n)
} else {
byParity["odd"] = append(byParity["odd"], n)
}
}
fmt.Println(byParity["even"]) // output: [2 4]
Множина / наявність через map[T]struct{} (або map[T]bool):
seen := map[string]struct{}{}
seen["go"] = struct{}{}
_, ok := seen["go"]
fmt.Println(ok) // output: true
Слабкі місця map: порядок ітерування рандомізований, є накладні витрати на гешування кожного запису, і map — хибний інструмент для фіксованого, відомого набору полів — це робота структури.
Структури — одна річ з іменованими полями¶
Коли значення має фіксовану, відому форму — кілька атрибутів різних типів, що належать одне одному, — беріть структуру. Імена та типи полів перевіряє компілятор.
type User struct {
Name string
Age int
}
u := User{Name: "Ada", Age: 36}
fmt.Println(u.Name) // output: Ada
Утримайтеся від моделювання відомого запису як map[string]any: ви
втрачаєте типобезпеку, перевірку імен полів та ясність. Map — для
відкритого набору ключів; структура — для закритого набору полів.
Власні типи — сенс і поведінка¶
Беріть визначений тип (type T U), коли самого базового типу замало:
щоб не дати змішуватися несумісним значенням, щоб прикріпити методи або
щоб побудувати перелік через iota.
type Celsius float64
type Fahrenheit float64 // різні: не можна випадково додати одне до одного
type Suit int
const (
Spades Suit = iota
Hearts
Diamonds
Clubs
)
fmt.Println(Hearts) // output: 1
Поєднання — реальні патерни¶
Більшість моделей даних — це композиції. Найпоширеніші:
| Патерн | Значення |
|---|---|
[]User |
впорядкований список записів — найбуденніша форма |
map[int]User |
записи, проіндексовані за id, для O(1) пошуку |
map[string][]Order |
групування один-до-багатьох (замовлення на клієнта) |
map[string]struct{} |
множина |
| структура з полями-зрізами/map | агрегат, що володіє колекціями |
Структура, що володіє колекціями, проіндексована для швидкого доступу:
type Library struct {
Name string
Books []string
}
shelf := map[string]Library{}
shelf["scifi"] = Library{Name: "Sci-Fi", Books: []string{"Dune"}}
fmt.Println(shelf["scifi"].Books[0]) // output: Dune
Лоб у лоб: що працює краще й коли¶
Зріз проти map для пошуку. Для жменьки елементів лінійний перебір зрізу простіший і часто швидший (без гешування, дружній до кешу). Переходьте на map, коли колекція велика або ви часто шукаєте за ключем.
Map проти структури. Відомі імена полів → структура (безпека на етапі
компіляції). Ключі визначаються під час виконання → map. Не вдавайте
структуру через map[string]any.
Масив проти зрізу. За замовчуванням — зріз. Масив беріть, лише коли
довжина справді фіксована й є частиною типу або коли ви хочете
копіювання за значенням / порівнюваність (наприклад, [32]byte для
гешу).
Власний тип проти сирого примітива. Загортайте примітив, коли важить
його сенс (UserID не має додаватися до OrderID) або коли вам
потрібні методи на ньому. Інакше звичайний тип цілком годиться.
З погляду Python: відповідність приблизна — зріз Go ≈
list, map ≈dict, структура ≈@dataclass(чи клас із фіксованими атрибутами), а множинаmap[T]struct{}≈set. Велика різниця в тому, що Go фіксує поля структури на етапі компіляції, тоді як об'єкт Python може нарощувати атрибути як завгодно.
Розібраний приклад¶
Підрахунок за довжиною слів поєднує кілька з цих блоків — map за ключем довжини, значення якої є зрізами структур:
type Word struct {
Text string
Length int
}
byLen := map[int][]Word{}
for _, t := range []string{"go", "rust", "py", "java"} {
w := Word{Text: t, Length: len(t)}
byLen[w.Length] = append(byLen[w.Length], w)
}
fmt.Println(byLen[2][0].Text, byLen[2][1].Text) // output: go py
Це map[int][]Word: map для групування за ключем, зріз для
впорядкованої групи та структура для кожного запису — кожен обраний
за те, що він робить найкраще.
Коли різним типам треба ділити поведінку за однією абстракцією, жоден із цих блоків не є відповіддю — для цього існують інтерфейси, що розглядаються в пізнішій статті.
Шпаргалка¶
- Впорядковано, ітерувати, дублікати → зріз
- Пошук / наявність / підрахунок / групування → map
- Фіксовані іменовані поля для однієї сутності → структура
- Окремий сенс, методи чи перелік → власний тип
- Порівнюваний буфер фіксованого розміру → масив
- Реальні моделі поєднують ці блоки —
[]Struct,map[K]Struct,map[K][]V,map[K]struct{}.