MidТеория5 min

Указатели

Указатели в Go: взятие адреса, разыменование, nil, value vs pointer семантика, escape analysis и паттерны

Указатель — это переменная, хранящая адрес памяти другой переменной. В Go указатели типизированы и безопасны: нет арифметики указателей (кроме пакета unsafe).

Основы

package main

import "fmt"

func main() {
    // & — address-of operator (get pointer)
    x := 42
    p := &x // p is *int, holds address of x

    fmt.Println(x)  // 42       — value
    fmt.Println(p)  // 0xc00001a0b8 — address
    fmt.Println(*p) // 42       — dereference (value at address)

    // * — dereference operator (get value at address)
    *p = 100
    fmt.Println(x) // 100 — x is modified through the pointer

    // Type of a pointer
    fmt.Printf("Type of p: %T\n", p) // *int

    // Pointer to pointer
    pp := &p
    fmt.Printf("Type of pp: %T\n", pp) // **int
    fmt.Println(**pp)                    // 100

    // new() — allocates zero-value and returns pointer
    q := new(int)   // *int, points to 0
    fmt.Println(*q) // 0
    *q = 77
    fmt.Println(*q) // 77
}

Операторы указателей

Оператор Название Описание Пример
&x Address-of Получить адрес переменной p := &x
*p Dereference Получить значение по адресу val := *p
*T Pointer type Тип «указатель на T» var p *int
new(T) Allocation Выделить память, вернуть *T p := new(int)

nil-указатели

package main

import "fmt"

func main() {
    // Zero value of any pointer is nil
    var p *int
    fmt.Println(p)        // <nil>
    fmt.Println(p == nil) // true

    // Dereferencing nil pointer causes PANIC
    // fmt.Println(*p) // panic: runtime error: invalid memory address

    // Always check for nil before dereferencing
    if p != nil {
        fmt.Println(*p)
    }

    // Safe access pattern
    value := safeDeref(p, 42) // Returns default if nil
    fmt.Println(value)         // 42
}

// Generic-style safe dereference helper
func safeDeref[T any](p *T, defaultVal T) T {
    if p == nil {
        return defaultVal
    }
    return *p
}

Nil receiver в методах

package main

import "fmt"

type List struct {
    Value int
    Next  *List
}

// Pointer receiver can handle nil!
func (l *List) Len() int {
    if l == nil {
        return 0 // Gracefully handle nil
    }
    count := 0
    for current := l; current != nil; current = current.Next {
        count++
    }
    return count
}

func (l *List) String() string {
    if l == nil {
        return "[]"
    }
    return fmt.Sprintf("[%d -> %s]", l.Value, l.Next)
}

func main() {
    var empty *List
    fmt.Println(empty.Len())    // 0 (no panic!)
    fmt.Println(empty.String()) // []

    list := &List{1, &List{2, &List{3, nil}}}
    fmt.Println(list.Len())    // 3
    fmt.Println(list.String()) // [1 -> [2 -> [3 -> []]]]
}

Value vs Pointer семантика

Одна из ключевых концепций Go — понимание, когда передавать значение, а когда указатель.

Передача по значению (Value semantics)

package main

import "fmt"

type Point struct {
    X, Y int
}

// Value parameter — receives a COPY
func doubleValue(p Point) Point {
    p.X *= 2
    p.Y *= 2
    return p // Must return the modified copy
}

func main() {
    original := Point{10, 20}
    doubled := doubleValue(original)

    fmt.Println(original) // {10 20} — NOT modified
    fmt.Println(doubled)  // {20 40} — new copy
}

Передача по указателю (Pointer semantics)

package main

import "fmt"

type Point struct {
    X, Y int
}

// Pointer parameter — receives address, modifies original
func doublePointer(p *Point) {
    p.X *= 2
    p.Y *= 2
    // No need to return — modifies in place
}

func main() {
    original := Point{10, 20}
    doublePointer(&original)

    fmt.Println(original) // {20 40} — modified in place
}

Когда использовать указатели

Критерий Value Pointer
Размер структуры Маленькая (<= 64 байт) Большая (> 64 байт)
Мутация Не нужна Нужна
nil-семантика Не нужна Нужна (optional fields)
Интерфейсы T и *T удовлетворяют Только *T удовлетворяет
Слайс элементов []T (data locality) []*T (если элементы большие)
Конкурентный доступ Безопасно (копии) Нужна синхронизация
Встроенные типы int, string, bool — (передавать по значению)
// Value semantics — for small, immutable data
type Color struct {
    R, G, B uint8 // 3 bytes — definitely pass by value
}

// Pointer semantics — for large or mutable data
type DatabaseConfig struct {
    Host     string
    Port     int
    Username string
    Password string
    Database string
    SSLMode  string
    Options  map[string]string
    // Large struct — use pointer
}

// Pointer semantics — for shared state
type Cache struct {
    mu    sync.RWMutex
    items map[string]any
    // Must be shared — use pointer
}

Stack vs Heap (Escape Analysis)

Компилятор Go автоматически решает, где разместить переменную — на стеке или в куче. Это называется escape analysis.

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

// User stays on the stack — does not escape
func createLocal() {
    u := User{Name: "Alice", Age: 30}
    fmt.Println(u.Name) // u is used only within this function
}

// User escapes to the heap — returned pointer outlives the function
func createAndReturn() *User {
    u := User{Name: "Bob", Age: 25}
    return &u // u escapes! Allocated on heap
}

// User escapes to the heap — assigned to interface
func printUser() {
    u := User{Name: "Charlie", Age: 35}
    var i interface{} = u // u escapes! Interface stores on heap
    fmt.Println(i)
}

func main() {
    createLocal()
    u := createAndReturn()
    fmt.Println(u.Name)
    printUser()
}

Как проверить escape analysis

# Show escape analysis decisions
go build -gcflags="-m" ./...

# More verbose
go build -gcflags="-m -m" ./...

# Output example:
# ./main.go:10:2: u does not escape
# ./main.go:16:9: &u escapes to heap

Что вызывает escape

Причина escape Пример
Возврат указателя return &x
Запись в интерфейс var i interface{} = x
Запись в слайс s = append(s, &x)
Замыкание захватывает go func() { _ = x }()
Слишком большой объект Объект > определённого размера
Компилятор не может доказать безопасность Сложные flow

Совет: Escape на heap — не катастрофа. Не оптимизируйте преждевременно. Профилируйте реальную нагрузку через pprof, прежде чем беспокоиться о heap allocations.

Отсутствие арифметики указателей

Go запрещает арифметику указателей — это ключевое отличие от C/C++:

func main() {
    arr := [5]int{10, 20, 30, 40, 50}
    p := &arr[0]

    // C/C++: p++ to move to next element — ILLEGAL in Go
    // p++     // COMPILE ERROR
    // p += 1  // COMPILE ERROR
    // *(p+1)  // COMPILE ERROR

    // In Go, use indexing instead
    for i := range arr {
        fmt.Println(arr[i])
    }

    // unsafe.Pointer exists but should be avoided
    // Only for interop with C (cgo) or low-level runtime code
    _ = p
}

Это делает Go безопаснее — нет buffer overflows, use-after-free и других проблем, типичных для C.

Практические паттерны

Опциональные поля с указателями

package main

import (
    "encoding/json"
    "fmt"
)

type UpdateUserRequest struct {
    Name  *string `json:"name,omitempty"`  // nil = don't update
    Email *string `json:"email,omitempty"` // nil = don't update
    Age   *int    `json:"age,omitempty"`   // nil = don't update
}

// Helper function to create pointer to value
func ptr[T any](v T) *T {
    return &v
}

func main() {
    // Update only name
    req := UpdateUserRequest{
        Name: ptr("Alice"),
        // Email and Age are nil — not updated
    }

    data, _ := json.Marshal(req)
    fmt.Println(string(data)) // {"name":"Alice"}

    // Update name and age
    req2 := UpdateUserRequest{
        Name: ptr("Bob"),
        Age:  ptr(30),
    }
    data, _ = json.Marshal(req2)
    fmt.Println(string(data)) // {"name":"Bob","age":30}
}

Указатели для передачи больших структур

package main

import "fmt"

type LargeData struct {
    Matrix [1000][1000]float64 // 8 MB!
}

// BAD: copies 8 MB on each call
func processValue(data LargeData) float64 {
    return data.Matrix[0][0]
}

// GOOD: passes only 8 bytes (pointer)
func processPointer(data *LargeData) float64 {
    return data.Matrix[0][0]
}

// GOOD with const correctness note:
// Go doesn't have const pointers, but document intent
func processReadOnly(data *LargeData) float64 {
    // This function should NOT modify data
    return data.Matrix[0][0]
}

Паттерн: swap

func swap[T any](a, b *T) {
    *a, *b = *b, *a
}

func main() {
    x, y := 10, 20
    swap(&x, &y)
    fmt.Println(x, y) // 20, 10

    s1, s2 := "hello", "world"
    swap(&s1, &s2)
    fmt.Println(s1, s2) // world, hello
}

Ссылочные типы Go (уже являются указателями)

Некоторые типы в Go уже содержат указатели внутри — передавать их по указателю не нужно:

Тип Содержит указатель? Передавать *T?
slice ([]T) Да (ptr + len + cap) Нет (если не меняем len/cap)
map Да (pointer to hash table) Нет
channel Да (pointer to hchan) Нет
function Да (pointer) Нет
interface Да (type + data pointers) Нет
string Да (ptr + len) Нет
struct Нет Зависит от размера и мутации
array Нет Да (если большой)
int, bool, etc. Нет Нет (маленькие, копирование дёшево)

Проверь себя

Нужно ли передавать `map` по указателю в функцию для модификации?

Почему в Go нет арифметики указателей?

Какой паттерн используется для опциональных полей (partial update) в Go API?

Что такое escape analysis в Go?

Что произойдёт при разыменовании nil-указателя в Go?