В JS все объекты — ссылки (неявно). В Go указатели — явные.
Нет сборщика мусора V8 — Go имеет свой GC с низкими паузами.
Escape analysis: компилятор решает, стек или куча.
& — взять адрес, * — разыменовать.
Нет аналога undefined — есть nil для указателей.
*T, &v, разыменование
new() vs &T{} vs make()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
slice = append(slice, val) — обязательно!for _, v := range items переменная v ПЕРЕИСПОЛЬЗУЕТСЯ. Не берите &v — указывает на одно место.*T — тип “указатель на T”. &v — взятие адреса. *p — разыменование.
💡 Для Node.js разработчика: