Go. Структуры данных
Указатели: основы работы
Введение в указатели
Представьте, что у вас есть книга, и вы хотите поделиться с другом определённой страницей. У вас есть два способа: переписать всю страницу на отдельный лист (копирование значения) или просто сказать другу номер страницы (передача указателя). Указатели в Go работают точно так же — они хранят адрес ячейки памяти, а не само значение.
Давайте разберёмся, что такое указатели, зачем они нужны и как правильно их использовать в Go.
Что такое указатель?
Указатель — это переменная, которая хранит адрес другой переменной в памяти. Вместо того чтобы хранить само значение, указатель «указывает» на место в памяти, где это значение находится.
package main

import "fmt"

func main() {
    // Обычная переменная
    message := "Hello, World!"
    
    // Создаём указатель на переменную message
    var ptr *string
    ptr = &message
    
    fmt.Println("Значение:", message)      // Hello, World!
    fmt.Println("Адрес:", &message)        // 0x... (адрес в памяти)
    fmt.Println("Указатель:", ptr)         // 0x... (тот же адрес)
    fmt.Println("Значение по указателю:", *ptr) // Hello, World!
}
Что здесь происходит?
  • message — обычная переменная типа string со значением "Hello, World!".
  • &message — операция взятия адреса, возвращает адрес переменной message.
  • ptr — указатель типа *string (указатель на строку).
  • *ptr — операция разыменования, возвращает значение по адресу, хранящемуся в указателе.
Зачем нужны указатели?
Основные причины использования указателей:
  • Эффективность
    Передача больших структур по ссылке быстрее, чем копирование
  • Изменение данных
    Возможность изменять значения оригинальных переменных
  • Разделение владения
    Чёткое понимание, кто может изменять данные
Давайте рассмотрим практический пример:
package main

import "fmt"

// Большая структура (представьте, что здесь много полей)
type User struct {
    ID       int
    Name     string
    Email    string
    // ... ещё 20 полей
}

// Функция без указателя (копирует всю структуру)
func updateUserCopy(user User) {
    user.Name = "Updated Name"
}

// Функция с указателем (работает с оригиналом)
func updateUserPtr(user *User) {
    user.Name = "Updated Name"
}

func main() {
    user := User{ID: 1, Name: "Original Name", Email: "user@example.com"}
    
    fmt.Println("До вызова:", user.Name) // Original Name
    
    updateUserCopy(user)
    fmt.Println("После updateUserCopy:", user.Name) // Original Name (не изменилось)
    
    updateUserPtr(&user)
    fmt.Println("После updateUserPtr:", user.Name)  // Updated Name (изменилось)
}
Объявление указателей
В Go существует несколько способов создать указатель, в зависимости от вашей задачи:
// Способ 1: с ключевым словом var
var ptr *int

// Способ 2: с помощью new() (выделяет память и возвращает указатель)
ptr2 := new(int)

// Способ 3: взятие адреса существующей переменной
x := 42
ptr3 := &x
Нулевой указатель
В Go есть специальное значение nil для указателей, которые ни на что не указывают. Попытка разыменовать нулевой указатель вызовет панику, поэтому всегда нужно проверять указатель перед использованием.
package main

import "fmt"

func main() {
    var ptr *int      // ptr == nil (нулевой указатель)
    fmt.Println(ptr)  // <nil>
    
    // ❌ Это вызовет панику!
    // fmt.Println(*ptr)
    
    // ✅ Правильно: сначала проверяем на nil
    if ptr != nil {
        fmt.Println(*ptr)
    } else {
        fmt.Println("Указатель равен nil, нельзя разыменовать")
    }
    
    // Присваиваем адрес
    x := 42
    ptr = &x
    fmt.Println(*ptr)
}
Работа с указателями на структуры
Go предоставляет удобный синтаксис для работы с полями структур через указатели:
package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    // Создаём структуру
    p := Person{Name: "Alice", Age: 30}
    
    // Создаём указатель на структуру
    ptr := &p
    
    // Два способа доступа к полям через указатель:
    fmt.Println((*ptr).Name) // Классический способ со скобками
    fmt.Println(ptr.Name)    // Go позволяет опускать * и скобки (синтаксический сахар)
    
    // Изменение полей через указатель
    ptr.Age = 31
    fmt.Println(p.Age) // 31 (оригинальная структура изменилась)
}
Когда использовать указатели?
Теперь, когда вы знаете основы, важно понимать, в каких ситуациях стоит использовать указатели, а в каких — обходиться значениями.
Используйте указатели, когда:
  • Нужно изменить данные внутри функции
  • Работаете с большими структурами
    (для снижения количества копирований)
Используйте значения, когда:
  • Данные маленькие (int, bool, string)
  • Нужна иммутабельность (защита от изменений)
Время жизни данных и сборщик мусора
Одна из важных особенностей работы с указателями — понимание того, как долго данные остаются в памяти. В Go за это отвечает автоматический сборщик мусора (Garbage Collector).
Важно

В Go данные, на которые существует хотя бы один указатель, не будут очищены сборщиком мусора.
Представьте, что у вас есть книга в библиотеке. Пока хотя бы один человек держит карточку с номером этой книги, книга не может быть убрана с полки. Точно так же работает сборщик мусора в Go — он удаляет данные только тогда, когда на них не остаётся ни одного указателя.
Давайте посмотрим на практическом примере:
package main

import "fmt"

func demonstrateLifetime() {
    // Создаём данные в куче (heap)
    data := make([]int, 1000)
    data[0] = 42
    
    // Создаём указатель на данные
    ptr := &data[0]
    
    fmt.Println(*ptr) // 42
    
    // Даже когда функция завершится, данные останутся в памяти,
    // если на них есть указатели из других мест
}

func main() {
    var globalPtr *int
    
    func() {
        // Локальная переменная
        local := 123
        globalPtr = &local // ← Указатель сохраняется глобально
        
        fmt.Println(*globalPtr) // 123
    }()
    
    // local больше не существует в области видимости,
    // но данные не будут удалены, пока globalPtr на них указывает
    fmt.Println(*globalPtr) // 123
    
    // Только когда globalPtr перестанет указывать на эти данные,
    // сборщик мусора сможет их удалить
    globalPtr = nil
    
    // Теперь сборщик мусора может очистить память
}
Что здесь происходит?
  • Данные создаются в памяти и получают указатели.
  • Сборщик мусора отслеживает все активные указатели.
  • Данные остаются в памяти, пока на них существует хотя бы один указатель.
  • Когда все указатели исчезают (становятся nil или выходят из области видимости), данные могут быть удалены.
Заключение
Теперь вы знаете основы работы с указателями в Go. Давайте подведём итоги.
Ключевые моменты:
  • Указатели хранят адресы переменных, а не сами значения
  • Указатели хранят адресы переменных, а не сами значения
  • Оператор & берёт адрес,
    а * разыменовывает указатель
  • Всегда проверяйте указатели на nil перед разыменованием
  • Используйте указатели для больших структур и когда нужно изменять данные
  • Данные остаются в памяти, пока на них существует хотя бы один указатель
Что дальше?
Теперь вы готовы изучать более сложные темы:
  • Указатели: как работает с памятью.
  • Указатели: unsafe/weak.
  • Массивы: объявление, инициализация, базовые операции.