Урок 2: Типы данных, коллекции и управление памятью

Урок 2. Типы данных, коллекции и управление памятью

🔄 Node.js → Go (ключевые отличия):

📋 Что изучаем

📦 Инициализация проекта

mkdir go-types && cd go-types
go mod init go-types

⭐ Длина (len) и Ёмкость (cap) — полное руководство

🏠 Аналогия со стаканами в коробке

Представь, что у тебя есть коробка на 6 стаканов. Ты поставил в неё 4 стакана.

📐 Техническое определение

Характеристикаlen (длина)cap (ёмкость)
ОпределениеКоличество элементов, которые РЕАЛЬНО находятся в слайсе прямо сейчасМаксимальное количество элементов, которое слайс МОЖЕТ вместить без выделения новой памяти
Доступ к элементамМожно читать/писать по индексам 0..len-1Нельзя обратиться к индексу ≥ len (паника: index out of range)
append()Увеличивается на 1 при добавлении элементаЕсли len < cap — не меняется. Если len == cap — увеличивается (обычно ×2)
make([]T, len, cap)Первый числовой аргументВторой числовой аргумент
ПамятьИспользуется прямо сейчасВыделена, но пока не используется (зарезервирована)
АналогияЗанятые места в кинотеатреВсего мест в зале

║ СТАРЫЙ МАССИВ (40 байт) НОВЫЙ МАССИВ (80 байт) ║ ║ ┌───┬───┬───┬───┬───┐ ┌───┬───┬───┬───┬───┬───┐ ║ ║ │ 1 │ 2 │ 3 │ 4 │ 5 │ │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ ║ ║ └───┴───┴───┴───┴───┘ └───┴───┴───┴───┴───┴───┘ ║ ║ ↑ ║ ║ ┌──────────────┐ └── cap=10 (обычно ×2) ║ ║ │ ptr ─────────┼──→ НОВЫЙ УКАЗАТЕЛЬ (старый массив) ║ ║ │ len = 6 │ ║ ║ │ cap = 10 │ ⚠️ Старый массив будет удалён GC ║ ║ └──────────────┘ ║ ╚══════════════════════════════════════════════════════════════╝

🎯 Ключевые различия len и cap

💻 Код: демонстрация len и cap

package main

import "fmt"

func main() {
    // ==========================================
    // 1. СОЗДАНИЕ С РАЗНЫМИ len И cap
    // ==========================================
    
    // len=0, cap=5 — память выделена, но элементов нет
    s1 := make([]int, 0, 5)
    fmt.Printf("s1: len=%d cap=%d %v\n", len(s1), cap(s1), s1)
    // Вывод: s1: len=0 cap=5 []
    // s1[0] — ПАНИКА! len=0, нельзя обратиться

    // len=3, cap=5 — 3 элемента (нули), 2 свободных слота
    s2 := make([]int, 3, 5)
    fmt.Printf("s2: len=%d cap=%d %v\n", len(s2), cap(s2), s2)
    // Вывод: s2: len=3 cap=5 [0 0 0]
    // s2[0]=42 — МОЖНО (0 ≤ индекс < 3)
    // s2[3] — ПАНИКА! (индекс 3 ≥ len=3)

    // len=5, cap=5 — заполнен полностью
    s3 := make([]int, 5)
    fmt.Printf("s3: len=%d cap=%d %v\n", len(s3), cap(s3), s3)
    // Вывод: s3: len=5 cap=5 [0 0 0 0 0]

    // ==========================================
    // 2. APPEND И РОСТ cap
    // ==========================================
    
    s := make([]int, 0, 4) // len=0, cap=4
    fmt.Printf("\n--- Добавление элементов ---\n")
    fmt.Printf("Начало:        len=%d cap=%d %v\n", len(s), cap(s), s)

    for i := 1; i <= 6; i++ {
        s = append(s, i)
        fmt.Printf("append(%d):    len=%d cap=%d %v", i, len(s), cap(s), s)
        if i > 4 && cap(s) > 4 {
            fmt.Print(" ← НОВАЯ АЛЛОКАЦИЯ! cap увеличился")
        }
        fmt.Println()
    }
    // Вывод:
    // Начало:        len=0 cap=4 []
    // append(1):    len=1 cap=4 [1]
    // append(2):    len=2 cap=4 [1 2]
    // append(3):    len=3 cap=4 [1 2 3]
    // append(4):    len=4 cap=4 [1 2 3 4]
    // append(5):    len=5 cap=8 [1 2 3 4 5] ← НОВАЯ АЛЛОКАЦИЯ! cap=8
    // append(6):    len=6 cap=8 [1 2 3 4 5 6]

    // ==========================================
    // 3. ПЕРЕДАЧА СЛАЙСА В ФУНКЦИЮ
    // ==========================================
    
    fmt.Printf("\n--- Передача в функцию ---\n")
    original := []int{10, 20, 30}
    fmt.Printf("До:   len=%d cap=%d %v\n", len(original), cap(original), original)
    
    modifySlice(original)
    fmt.Printf("После: len=%d cap=%d %v\n", len(original), cap(original), original)
    // original[0] изменился! (слайс разделяет память)
}

func modifySlice(s []int) {
    s[0] = 999 // Меняет ОРИГИНАЛ (общий массив)
    // s = append(s, 999) — это создаст НОВЫЙ слайс (если cap не хватит),
    // но оригинал НЕ изменится, потому что s — копия структуры слайса
}

🔑 Когда использовать len и cap?

СитуацияЧто использоватьПример
Итерация по элементамlenfor i := 0; i < len(s); i++ {}
Проверка, пуст ли слайсlenif len(s) == 0 {}
Предвыделение памятиcapmake([]int, 0, 1000)
Проверка, нужна ли реаллокацияcapif len(s) == cap(s) { /* будет реаллокация */ }
Срез (slicing)len и cap срезаsub := s[2:5] — len=3, cap зависит от cap(s)
💡 Практический совет

Если ты ЗНАЕШЬ заранее, сколько элементов будет в слайсе — всегда используй make([]T, 0, expectedSize).

Это предотвратит множественные реаллокации и ускорит код в 2-5 раз при больших объёмах данных.

🔬 Визуализация в памяти

╔══════════════════════════════════════════════════════════════╗ ║ ШАГ 1: make([]int, 0, 5) ║ ║ ║ ║ СЛАЙС (24 байта) ВЫДЕЛЕННЫЙ МАССИВ (40 байт) ║ ║ ┌──────────────┐ ┌───┬───┬───┬───┬───┐ ║ ║ │ ptr ─────────┼───────→ │ ? │ ? │ ? │ ? │ ? │ ║ ║ │ len = 0 │ └───┴───┴───┴───┴───┘ ║ ║ │ cap = 5 │ ↑ ║ ║ └──────────────┘ └── МЕСТО ЕСТЬ, НО ЭЛЕМЕНТОВ 0 ║ ║ ║ ║ Доступ по индексу [0] → ПАНИКА (index out of range [0]) ║ ║ т.к. len=0 — элементов нет, хотя память выделена! ║ ╚══════════════════════════════════════════════════════════════╝

╔══════════════════════════════════════════════════════════════╗ ║ ШАГ 2: append(slice, 1, 2, 3) ║ ║ ║ ║ ┌──────────────┐ ┌───┬───┬───┬───┬───┐ ║ ║ │ ptr ─────────┼───────→ │ 1 │ 2 │ 3 │ ? │ ? │ ║ ║ │ len = 3 │ └───┴───┴───┴───┴───┘ ║ ║ │ cap = 5 │ ↑ ↑ ║ ║ └──────────────┘ └─ len=3 ─┘└─ ещё 2 слота ─→ ║ ║ ║ ║ Доступ: [0]=1 ✅, [1]=2 ✅, [2]=3 ✅, [3] ❌ ПАНИКА ║ ║ append(slice, 4) → len=4, cap=5 (БЕЗ новой аллокации!) ║ ╚══════════════════════════════════════════════════════════════╝

╔══════════════════════════════════════════════════════════════╗ ║ ШАГ 3: append(slice, 4, 5, 6) — len стал бы 6 > cap=5 ║ ║ ║ ║ СТАРЫЙ МАССИВ (40 байт) НОВЫЙ МАССИВ (80 байт) ║ ║ ┌───┬───┬───┬───┬───┐ ┌───┬───┬───┬───┬───┬───┐ ║ ║ │ 1 │ 2 │ 3 │ 4 │ 5 │ │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ ║ ║ └───┴───┴───┴───┴───┘ └───┴───┴───┴───┴───┴───┘ ║ ║ ↑ ║ ║ ┌──────────────┐ └── cap=10 (обычно ×2) ║ ║ │ ptr ─────────┼──→ НОВЫЙ УКАЗАТЕЛЬ (старый массив) ║ ║ │ len = 6 │ ║ ║ │ cap = 10 │ ⚠️ Старый массив будет удалён GC ║ ║ └──────────────┘ ║ ╚══════════════════════════════════════════════════════════════╝

💻 Код программы (полный)

ТИПЫ ДАННЫХ, КОЛЛЕКЦИИ, ПАМЯТЬ

1.БАЗОВЫЕ ТИПЫ

package main

import (
  "fmt"
  "math/cmplx"
)



func main() {
  // Целочисленные типы: int8, int16, int32 (rune), int64
  var (
      small  int8  = 127                   // от -128 до 127
      medium int16 = 32767                 // от -32768 до 32767
      normal int   = 42                    // 32 или 64 бита (зависит от платформы)
      big    int64 = 9223372036854775807   // от -2^63 до 2^63-1
  )
  fmt.Printf("int8: %d | int16: %d | int (arch): %d | int64: %d\n",
      small, medium, normal, big)

  // Беззнаковые: uint8 (byte), uint16, uint32, uint64
  var (
      b byte = 255 // byte — алиас для uint8
      u uint64 = 1&lt;&lt;63 // 2^63
  )
  fmt.Printf("byte: %d | uint64: %d\n", b, u)

  // Числа с плавающей точкой: float32, float64
  var (
      f32 float32 = 3.1415927
      f64 float64 = 3.141592653589793
  )
  fmt.Printf("float32: %.7f | float64: %.15f\n", f32, f64)

  // Комплексные числа (есть из коробки!)
  c := complex(3, 4) // 3 + 4i
  fmt.Printf("complex: %v | модуль: %.2f\n", c, cmplx.Abs(c))

  // Булево значение (по умолчанию false)
  var flag bool
  fmt.Printf("bool (default): %v\n", flag)
}

2. СТРОКИ И РУНЫ

package main

import (
  "fmt"
  "unicode/utf8"
)

func main() {
  // Строка в Go — иммутабельный слайс байт в кодировке UTF-8.
  // len() возвращает количество БАЙТ, не символов!
  s := "Привет, Gopher!"
  fmt.Printf("Строка: %q\n", s)
  fmt.Printf("  len() байт: %d\n", len(s))
  fmt.Printf("  utf8.RuneCountInString() символов: %d\n",
      utf8.RuneCountInString(s))

  // Итерация по строке: по байтам vs по рунам
  fmt.Println("  По байтам (for i := 0):")
  for i := 0; i < len(s); i++ {
      fmt.Printf("    [%d] = %x (%c)\n", i, s[i], s[i])
  }

  fmt.Println("  По рунам (for range):")
  for i, r := range s {
      fmt.Printf("    позиция %d: %c (U+%04X)\n", i, r, r)
  }

  // rune — это int32, представляет Unicode-кодпоинт
  var ru rune = ''
  fmt.Printf("rune '€': код %d, символ %c\n", ru, ru)
}

3. МАССИВЫ vs СЛАЙСЫ

package main

import "fmt"

func main(){
  // МАССИВ: фиксированный размер, часть типа.
  // [3]int и [4]int — РАЗНЫЕ типы.
  // При передаче в функцию — ПОЛНОЕ КОПИРОВАНИЕ.
  arr := [3]int{10, 20, 30}
  fmt.Printf("Массив: %v | тип: %T | len=%d\n", arr, arr, len(arr))

  // СЛАЙС: динамический массив. Сам слайс — это структура:
  // { указатель_на_массив, длина, ёмкость }
  // При передаче в функцию копируется структура, но массив общий!

  // Создание слайса через make(type, length, capacity)
  slice := make([]int, 0, 5) // len=0, cap=5
  fmt.Printf("\nСлайс (пустой): len=%d cap=%d %v\n", len(slice), cap(slice), slice)

  // append() добавляет элементы. Если cap хватает — O(1).
  // Если нет — аллокация нового массива (обычно x2).
  slice = append(slice, 1, 2, 3, 4, 5)
  fmt.Printf("После append 5: len=%d cap=%d %v\n", len(slice), cap(slice), slice)

  // Добавляем сверх cap — происходит реаллокация
  slice = append(slice, 6)
  fmt.Printf("После append 6 (реаллокация): len=%d cap=%d %v\n",
      len(slice), cap(slice), slice)

  // СРЕЗЫ (slicing) — делят память с оригиналом!
  // sub := slice[2:5] — элементы с индексами 2,3,4
  sub := slice[2:5]
  fmt.Printf("\nСрез [2:5]: %v\n", sub)
  fmt.Println("⚠️ Срез РАЗДЕЛЯЕТ память с оригиналом!")

  sub[0] = 999 // Изменение через срез меняет ОРИГИНАЛ
  fmt.Printf("После sub[0] = 999:\n  slice=%v\n  sub=%v\n", slice, sub)

  // copy() — глубокое копирование слайса
  copied := make([]int, len(slice))
  copy(copied, slice)
  copied[0] = 777
  fmt.Printf("\nПосле copy() и изменения копии:\n  slice=%v\n  copy=%v\n", slice, copied)
}

4. МАПЫ (map)

package main

import "fmt"

func main(){
  // Создание мапы: make() или литерал
  scores := map[string]int{
      "Alice": 100,
      "Bob":   85,
  }
  scores["Charlie"] = 92 // Добавление
  fmt.Printf("Мапа: %v | len=%d\n", scores, len(scores))

  // Comma ok идиома: проверка наличия ключа
  // В Go НЕТ undefined. Есть НУЛЕВОЕ ЗНАЧЕНИЕ для типа.
  if score, ok := scores["Alice"]; ok {
      fmt.Printf("  Alice: %d (найдена)\n", score)
  }

  // Несуществующий ключ возвращает нулевое значение
  missing := scores["Zorro"]
  fmt.Printf("  scores[\"Zorro\"] = %d (нулевое значение int)\n", missing)

  // Удаление ключа
  delete(scores, "Bob")
  fmt.Printf("После delete(\"Bob\"): %v\n", scores)

  // Итерация — порядок НЕ ГАРАНТИРОВАН
  fmt.Println("  Итерация (порядок случаен):")
  for key, val := range scores {
      fmt.Printf("    %s%d\n", key, val)
  }
}

5. СТРУКТУРЫ

package main

import "fmt"

// Person — структура. В Go нет классов, только struct с полями.
type Person struct {
  Name   string
  Age    int
  Emails []string          // Слайс строк (динамический массив)
  Meta   map[string]string // Мапа: ключ string → значение string
}

// NewPerson — функция-конструктор. Возвращает УКАЗАТЕЛЬ на Person.
func NewPerson(name string, age int) *Person {
  // make() создаёт слайс длиной 0, ёмкостью 2
  return &Person{
      Name:   name,
      Age:    age,
      Emails: make([]string, 0, 2), // len=0, cap=2
      Meta:   make(map[string]string),
  }
}

func main(){
  person := NewPerson("Gopher", 13)
  person.Emails = append(person.Emails, "gopher@golang.org", "gopher@example.com")
  person.Meta["role"] = "mascot"
  person.Meta["lang"] = "Go"

  // %+v выводит структуру с именами полей (аналог util.inspect с depth)
  fmt.Printf("Person (%%+v): %+v\n", person)
  fmt.Printf("Emails: %v (len=%d, cap=%d)\n",
      person.Emails, len(person.Emails), cap(person.Emails))

  // Указатель на структуру: доступ через . (авторазыменование)
  p2 := &Person{Name: "Alice", Age: 30} // & — сразу указатель
  p2.Emails = []string{"alice@example.com"}
  fmt.Printf("p2 (*Person): %+v\n", p2)
  // p2.Name работает как (*p2).Name — Go делает авторазыменование
}

6. НУЛЕВЫЕ ЗНАЧЕНИЯ

package main

func main(){
  fmt.Println("\n── 6. НУЛЕВЫЕ ЗНАЧЕНИЯ ──")
  fmt.Println("В Go нет undefined. Всё инициализируется нулём:")
  fmt.Printf("  int: %d\n", 0)
  fmt.Printf("  float64: %f\n", 0.0)
  fmt.Printf("  string: %q\n", "")
  fmt.Printf("  bool: %v\n", false)
  fmt.Printf("  указатель: %v\n", (*int)(nil))
  fmt.Printf("  слайс: %v (nil=%v)\n", []int(nil), []int(nil) == nil)
  fmt.Printf("  мапа: %v (nil=%v)\n", map[string]int(nil), map[string]int(nil) == nil)
  fmt.Printf("  структура: %+v\n", Person{})
}

🚀 Запуск программы

# Основной запуск
go run main.go

# Сборка и запуск бинарника
go build -o types-demo main.go
./types-demo

# Запуск с GODEBUG для просмотра аллокаций
GODEBUG=allocfreetrace=1 go run main.go 2>&1 | head -20

🔑 Ключевые концепции

Что нужно запомнить из этого урока:

💡 Для Node.js разработчика:
← Предыдущий урок Следующий урок →