Раздел 22 · Go

Память Go: значимые и "ссылочные" типы, передача параметров

junior~55 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Перед этим уроком

Память Go: значимые и “ссылочные” типы, передача параметров

В Go есть один секрет, который в первый месяц ломает интуицию у людей, пришедших из Java, Python или JS. Звучит он так: в Go все аргументы передаются по значению. Всегда. Без исключений. При этом slice, map и channel “работают как ссылки”, и это противоречие, пока не разложишь у себя в голове, что внутри у этих типов.

Этот урок про то, как разложить. Опираемся на карту памяти из 01-languages-and-runtime, дальше идём в 03-strings-runes-builder.

1. Значимые типы и их семантика копирования

Значимый тип это тип, при копировании которого ты получаешь полностью независимую копию. Изменения в одной не видны в другой.

В Go значимыми типами являются:

ТипРазмер на 64-bitЧто внутри
bool1 байтtrue или false
int, int64, uint648 байтчисло
float648 байтIEEE 754 double
complex12816 байтдве float64
[N]T (массив)N * sizeof(T)элементы лежат подряд
struct {...}сумма размеров полей плюс выравниваниеполя подряд в памяти

Ключевое: массивы и структуры значимые. Вот доказательство.

package main

import "fmt"

type Point struct {
    X, Y int
}

func main() {
    a := [3]int{1, 2, 3}
    b := a            // полная копия 24 байт
    b[0] = 99
    fmt.Println(a, b) // [1 2 3] [99 2 3], a не тронули

    p := Point{X: 1, Y: 2}
    q := p            // полная копия 16 байт
    q.X = 99
    fmt.Println(p, q) // {1 2} {99 2}
}

В отличие от Java или Python, где массив и любой Object это всегда указатель, в Go массив и структура копируются целиком при каждом присваивании. Отсюда вытекает дисциплина: большие структуры обычно передают по указателю, чтобы не копировать килобайты на каждом вызове (подробнее в 09-pointers).

2. “Ссылочные типы”, тёмная тайна Go

В книгах и туториалах про Go ты увидишь термин reference type для slice, map, channel, func и interface. С точки зрения языка их нет. В Go нет ключевого слова ref и нет двух семантик передачи параметров. Все эти типы значимые, просто внутри у них прячется указатель.

slice внутри это header

type sliceHeader struct {
    Data unsafe.Pointer // указатель на backing-массив
    Len  int            // длина
    Cap  int            // ёмкость
}

Размер этой шапки на 64-bit, 24 байта. Когда ты передаёшь []int в функцию, копируется эта 24-байтная шапка. Поле Data теперь указывает на тот же массив, что и оригинал. Поэтому модификации s[0] = 99 видны снаружи:

func mutate(s []int) {
    s[0] = 99 // тот же backing array
}

func main() {
    a := []int{1, 2, 3}
    mutate(a)
    fmt.Println(a) // [99 2 3]
}

Но если ты сделаешь append, и вместимости не хватит, рантайм аллоцирует новый массив, скопирует туда старые элементы и плюс новые, и заменит Data в локальной копии шапки. Снаружи ничего не изменится:

func grow(s []int) {
    s = append(s, 100, 200, 300, 400) // вышли за cap, новый массив
}

func main() {
    a := []int{1, 2, 3}
    grow(a)
    fmt.Println(a, len(a), cap(a)) // [1 2 3] 3 3
}

Это та самая ловушка: один и тот же append иногда виден снаружи (если cap хватает), иногда нет. Решение: либо возвращать слайс из функции (a = grow(a)), либо передавать *[]int. Идиоматично первое.

map внутри это указатель

type hmap struct {
    // ... много полей про ведра ...
}

// "тип map" в коде это *hmap под капотом

Когда ты пишешь m := make(map[string]int), переменная m хранит 8 байт, указатель на структуру hmap в heap. Передаёшь m в функцию, копируется указатель. Обе функции работают с одной мапой:

func setKey(m map[string]int) {
    m["x"] = 99 // мутируем общую мапу
}

func main() {
    m := map[string]int{"a": 1}
    setKey(m)
    fmt.Println(m) // map[a:1 x:99]
}

То же самое с channel и func value. Каждый из них это пара (или указатель), 8 или 16 байт, внутри которых живёт ссылка на runtime-объект.

Сводная таблица “что копируется”

ТипРазмер копии (header)Что разделяется при копировании
int, bool, float648 (1 для bool)ничего, полная независимость
[N]T массивN * sizeof(T)ничего
struct{...}сумма полейничего
string16 байт (Data, Len)байты, immutable, безопасно
[]T slice24 байта (Data, Len, Cap)backing-массив
map[K]V8 байт (указатель на hmap)вся мапа
chan T8 байт (указатель на hchan)весь канал
func()16 байт (указатель кода плюс closure)замкнутые переменные
interface{...}16 байт (itab, data)объект под интерфейсом
*T указатель8 байтT, на который указывает

Все они по языку значимые. Просто у некоторых “значение”, это указатель.

премиум-урок

Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.

Оплата пока не подключена: премиум и курсы открываются промокодом.