Урок 4: Указатели и управление памятью (глубокое погружение)

Урок 4. Указатели и управление памятью (глубокое погружение)

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

📋 Что изучаем

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

mkdir go-pointers && cd go-pointers
go mod init go-pointers

💻 Код программы

package main

import (
    "fmt"
    "strings"
    "unsafe"
)

// ╔══════════════════════════════════════════════════════════╗
// ║  1. СТРУКТУРЫ ДЛЯ ДЕМОНСТРАЦИИ                         ║
// ╚══════════════════════════════════════════════════════════╝

// SmallStruct — маленькая структура (16 байт)
type SmallStruct struct {
    ID   int
    Name string
}

// LargeStruct — большая структура (~88 байт)
// Копировать такую структуру при каждом вызове — дорого.
type LargeStruct struct {
    ID       int
    Name     string
    Email    string
    Payload  [10]float64 // 80 байт (10 × 8)
    Metadata map[string]string
}

// ProcessValue — value receiver: КОПИРУЕТ LargeStruct целиком (~88+ байт)
func (d LargeStruct) ProcessValue() string {
    return fmt.Sprintf("Processed (value copy): %s <%s>", d.Name, d.Email)
}

// ProcessPointer — pointer receiver: передаётся только АДРЕС (8 байт)
func (d *LargeStruct) ProcessPointer() string {
    return fmt.Sprintf("Processed (pointer): %s <%s>", d.Name, d.Email)
}

// UpdateName — pointer receiver: изменяет оригинал
func (d *LargeStruct) UpdateName(name string) {
    d.Name = name
}

// ╔══════════════════════════════════════════════════════════╗
// ║  2. ФУНКЦИИ, ВОЗВРАЩАЮЩИЕ УКАЗАТЕЛИ                    ║
// ╚══════════════════════════════════════════════════════════╝

// NewLargeStruct — фабричная функция (идиоматический паттерн)
func NewLargeStruct(id int, name, email string) *LargeStruct {
    // Локальная переменная "убегает" в кучу (escape analysis)
    return &LargeStruct{
        ID:      id,
        Name:    name,
        Email:   email,
        Payload: [10]float64{},
        Metadata: make(map[string]string),
    }
}

// FindByID — имитация поиска. Возвращает nil, если не найдено.
func FindByID(id int) *LargeStruct {
    if id <= 0 {
        return nil // nil-указатель — сигнал "не найдено"
    }
    return &LargeStruct{ID: id, Name: fmt.Sprintf("User %d", id)}
}

// ╔══════════════════════════════════════════════════════════╗
// ║  3. ДЕМОНСТРАЦИЯ РАБОТЫ С УКАЗАТЕЛЯМИ                  ║
// ╚══════════════════════════════════════════════════════════╝

// modifyInt — принимает указатель и меняет значение по адресу
func modifyInt(p *int) {
    *p = 100 // разыменование: меняем значение, на которое указывает p
}

// modifyFail — принимает ЗНАЧЕНИЕ. Изменения не видны снаружи.
func modifyFail(x int) {
    x = 100 // меняет локальную копию
}

// modifySlice — слайс: меняет элементы (разделяет память)
func modifySlice(s []int) {
    s[0] = 999 // меняет ОРИГИНАЛ (слайс содержит указатель на массив)
}

// modifyMap — мапа всегда передаётся по ссылке
func modifyMap(m map[string]int) {
    m["key"] = 42
}

// appendSlice — демонстрация, почему append требует присваивания
func appendSlice(s []int) []int {
    // append может вернуть НОВЫЙ слайс (если не хватило cap)
    return append(s, 999)
}

// ╔══════════════════════════════════════════════════════════╗
// ║  4. ESCAPE ANALYSIS — демонстрация                      ║
// ╚══════════════════════════════════════════════════════════╝

// stackAlloc — переменная остаётся на стеке (не убегает)
func stackAlloc() int {
    x := 42
    return x // значение копируется, x на стеке
}

// heapAlloc — переменная "убегает" в кучу (возвращается указатель)
func heapAlloc() *int {
    y := 42
    return &y // y перемещается в кучу
}

// noEscape — слайс не убегает, если не выходит за пределы функции
func noEscape() []int {
    s := make([]int, 3)
    for i := range s {
        s[i] = i
    }
    return s // слайс может убежать (зависит от использования)
}

// ╔══════════════════════════════════════════════════════════╗
// ║  MAIN                                                    ║
// ╚══════════════════════════════════════════════════════════╝

func main() {
    fmt.Println("╔══════════════════════════════════════════╗")
    fmt.Println("║   УКАЗАТЕЛИ И УПРАВЛЕНИЕ ПАМЯТЬЮ       ║")
    fmt.Println("╚══════════════════════════════════════════╝")

    // ==========================================
    // 1. БАЗОВЫЙ СИНТАКСИС УКАЗАТЕЛЕЙ
    // ==========================================
    fmt.Println("\n── 1. БАЗОВЫЙ СИНТАКСИС ──")

    x := 42
    fmt.Printf("x = %d\n", x)

    // & — оператор взятия адреса (address-of)
    p := &x
    fmt.Printf("&x = %p (тип: %T)\n", p, p)

    // * — оператор разыменования (dereference)
    fmt.Printf("*p = %d\n", *p)

    // Изменение через указатель
    *p = 100
    fmt.Printf("После *p = 100: x = %d\n", x)

    // Указатель на указатель (редко, но возможно)
    pp := &p
    fmt.Printf("&&x = %p, **pp = %d\n", pp, **pp)

    // ==========================================
    // 2. УКАЗАТЕЛИ НА СТРУКТУРЫ
    // ==========================================
    fmt.Println("\n── 2. УКАЗАТЕЛИ НА СТРУКТУРЫ ──")

    // Создание через & (предпочтительный способ)
    ls := &LargeStruct{
        ID:    1,
        Name:  "Alice",
        Email: "alice@example.com",
    }

    // Доступ к полям: ls.Name (Go авторазыменовывает)
    // Эквивалентно (*ls).Name
    fmt.Printf("ls.Name = %s (через авторазыменование)\n", ls.Name)
    fmt.Printf("(*ls).Name = %s (явное разыменование)\n", (*ls).Name)

    // Вызов методов: Go автоматически берёт адрес или разыменовывает
    fmt.Println(ls.ProcessPointer())
    ls.UpdateName("Alice Updated")
    fmt.Printf("После UpdateName: %s\n", ls.Name)

    // Value receiver с указателя: Go разыменовывает автоматически
    fmt.Println(ls.ProcessValue())

    // ==========================================
    // 3. nil-УКАЗАТЕЛИ
    // ==========================================
    fmt.Println("\n── 3. nil-УКАЗАТЕЛИ ──")

    // Нулевое значение для указателя — nil
    var nilPtr *int
    fmt.Printf("nilPtr = %v (nil = %v)\n", nilPtr, nilPtr == nil)

    // nil-указатель на структуру: можно вызывать методы с pointer receiver!
    var nilUser *LargeStruct
    fmt.Printf("nilUser = %v\n", nilUser)

    // Безопасная проверка в методе
    if nilUser != nil {
        fmt.Println(nilUser.ProcessPointer())
    } else {
        fmt.Println("nilUser: пользователь не найден")
    }

    // FindByID возвращает nil для несуществующих
    user := FindByID(-1)
    if user == nil {
        fmt.Println("FindByID(-1): пользователь не найден")
    }

    user = FindByID(42)
    fmt.Printf("FindByID(42): %+v\n", user)

    // ==========================================
    // 4. new() vs &T{} vs make()
    // ==========================================
    fmt.Println("\n── 4. new() vs &T{} vs make() ──")

    // new(T) — выделяет память, заполняет нулями, возвращает *T
    p1 := new(LargeStruct)
    fmt.Printf("new(LargeStruct): ID=%d, Name=%q\n", p1.ID, p1.Name)

    // &T{} — литерал с возможностью инициализации (предпочтительнее)
    p2 := &LargeStruct{ID: 2, Name: "Bob"}
    fmt.Printf("&LargeStruct{...}: ID=%d, Name=%q\n", p2.ID, p2.Name)

    // make() — ТОЛЬКО для слайсов, мап, каналов
    slice := make([]int, 0, 10)
    m := make(map[string]int)
    ch := make(chan int, 5)
    fmt.Printf("make: slice(len=%d,cap=%d), map(len=%d), chan(cap=%d)\n",
        len(slice), cap(slice), len(m), cap(ch))

    // ==========================================
    // 5. СЛАЙСЫ И МАПЫ — ССЫЛОЧНЫЕ ТИПЫ
    // ==========================================
    fmt.Println("\n── 5. СЛАЙСЫ И МАПЫ (ссылочные типы) ──")

    // Слайс — структура {ptr, len, cap}, передаётся по значению,
    // но УКАЗАТЕЛЬ на данные внутри — общий.
    numbers := []int{1, 2, 3, 4, 5}
    fmt.Printf("До modifySlice: %v\n", numbers)
    modifySlice(numbers)
    fmt.Printf("После modifySlice: %v (изменён!)\n", numbers)

    // append МОЖЕТ создать новый слайс (если cap не хватает)
    fmt.Printf("До appendSlice: len=%d cap=%d %v\n",
        len(numbers), cap(numbers), numbers)
    numbers = appendSlice(numbers) // ВАЖНО: присвоить результат!
    fmt.Printf("После appendSlice: len=%d cap=%d %v\n",
        len(numbers), cap(numbers), numbers)

    // Мапа: всегда передаётся как указатель под капотом
    scores := map[string]int{"Alice": 10}
    fmt.Printf("До modifyMap: %v\n", scores)
    modifyMap(scores)
    fmt.Printf("После modifyMap: %v\n", scores)

    // ==========================================
    // 6. РАЗМЕРЫ ТИПОВ И ВЫРАВНИВАНИЕ
    // ==========================================
    fmt.Println("\n── 6. РАЗМЕРЫ ТИПОВ ──")

    fmt.Printf("Размеры типов на 64-битной архитектуре:\n")
    fmt.Printf("  int:       %d байт\n", unsafe.Sizeof(int(0)))
    fmt.Printf("  float64:   %d байт\n", unsafe.Sizeof(float64(0)))
    fmt.Printf("  string:    %d байт (заголовок)\n", unsafe.Sizeof(""))
    fmt.Printf("  указатель: %d байт\n", unsafe.Sizeof(&x))
    fmt.Printf("  SmallStruct: %d байт\n", unsafe.Sizeof(SmallStruct{}))
    fmt.Printf("  LargeStruct: %d байт\n", unsafe.Sizeof(LargeStruct{}))

    // ==========================================
    // 7. ДЕМОНСТРАЦИЯ УТЕЧКИ (escape analysis)
    // ==========================================
    fmt.Println("\n── 7. ESCAPE ANALYSIS ──")
    fmt.Println("Запусти: go build -gcflags='-m' main.go")
    fmt.Println("Компилятор покажет, какие переменные уходят в кучу.")

    a := stackAlloc()
    b := heapAlloc()
    fmt.Printf("stackAlloc: %d, heapAlloc: %d\n", a, *b)

    // ==========================================
    // 8. ОПАСНОСТИ УКАЗАТЕЛЕЙ
    // ==========================================
    fmt.Println("\n── 8. ОПАСНОСТИ ──")

    // Go НЕ позволяет арифметику указателей (если не unsafe.Pointer)
    // arr := [3]int{1,2,3}
    // p := &arr[0]
    // p++ // ОШИБКА КОМПИЛЯЦИИ!

    // Висячие указатели: Go защищает через escape analysis.
    // Нельзя вернуть указатель на переменную, которая "не убежала" в кучу.
    // Компилятор сам переместит в кучу, если нужно.

    // Разыменование nil-указателя → ПАНИКА (аналог NullPointerException)
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("  Восстановление после паники:", r)
        }
    }()
    // Раскомментируй для демонстрации паники:
    // var np *int
    // *np = 42 // PANIC: nil pointer dereference
    fmt.Println("  (nil pointer dereference закомментирован)")
}

🧠 Визуализация: стек и куча

┌─────────────────────────────────────────────────────────┐ │ ПРОЦЕСС GO │ │ │ │ ┌───────────────────┐ ┌───────────────────────────┐ │ │ │ СТЕК │ │ КУЧА │ │ │ │ (быстрый, LIFO) │ │ (медленнее, GC) │ │ │ │ │ │ │ │ │ │ stackAlloc(): │ │ heapAlloc(): │ │ │ │ x = 42 │ │ │ │ │ │ (удаляется при │ │ ┌─────────────────┐ │ │ │ │ выходе из ф-и) │ │ │ y = 42 │ │ │ │ │ │ │ │ (живёт пока есть │ │ │ │ │ modifyFail(): │ │ │ ссылка) │ │ │ │ │ x = 100 (копия) │ │ └─────────────────┘ │ │ │ │ (не влияет на │ │ │ │ │ │ оригинал) │ │ NewLargeStruct(): │ │ │ │ │ │ ┌──────────────────┐ │ │ │ │ │ │ │ LargeStruct{...} │ │ │ │ │ │ │ │ “убежала” в кучу │ │ │ │ │ │ │ └──────────────────┘ │ │ │ └───────────────────┘ └───────────────────────────┘ │ │ │ │ Escape Analysis (во время компиляции): │ │ • Если указатель на переменную покидает функцию │ │ → переменная перемещается в кучу │ │ • Если нет → остаётся на стеке (быстрее) │ │ • Сборщик мусора работает ТОЛЬКО с кучей │ └─────────────────────────────────────────────────────────┘

📊 Когда что использовать

СитуацияЗначение (T)Указатель (*T)
Маленькая неизменяемая структура❌ Избыточно
Большая структура (>64 байт)❌ Дорого копировать✅ 8 байт
Нужно изменить оригинал❌ Невозможно
Поле может отсутствовать✅ nil = отсутствует
Слайсы, мапы, каналы✅ Уже ссылочные✅ Можно для nil
Реализация интерфейса✅ T и *T⚠️ Только *T

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

# Обычный запуск
go run main.go

# Просмотр escape analysis (КТО УБЕЖАЛ В КУЧУ)
go build -gcflags="-m" main.go

# Более детальный вывод
go build -gcflags="-m -m" main.go 2>&1 | grep "escapes"

# Сборка
go build -o pointers-demo main.go
./pointers-demo
⚠️ Типичные ошибки с указателями:

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

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

💡 Для Node.js разработчика:

← Предыдущий урок Следующий урок →