Память Go: значимые и “ссылочные” типы, передача параметров
В Go есть один секрет, который в первый месяц ломает интуицию у людей, пришедших из Java, Python или JS. Звучит он так: в Go все аргументы передаются по значению. Всегда. Без исключений. При этом slice, map и channel “работают как ссылки”, и это противоречие, пока не разложишь у себя в голове, что внутри у этих типов.
Этот урок про то, как разложить. Опираемся на карту памяти из 01-languages-and-runtime, дальше идём в 03-strings-runes-builder.
1. Значимые типы и их семантика копирования
Значимый тип это тип, при копировании которого ты получаешь полностью независимую копию. Изменения в одной не видны в другой.
В Go значимыми типами являются:
| Тип | Размер на 64-bit | Что внутри |
|---|---|---|
bool | 1 байт | true или false |
int, int64, uint64 | 8 байт | число |
float64 | 8 байт | IEEE 754 double |
complex128 | 16 байт | две 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, float64 | 8 (1 для bool) | ничего, полная независимость |
[N]T массив | N * sizeof(T) | ничего |
struct{...} | сумма полей | ничего |
string | 16 байт (Data, Len) | байты, immutable, безопасно |
[]T slice | 24 байта (Data, Len, Cap) | backing-массив |
map[K]V | 8 байт (указатель на hmap) | вся мапа |
chan T | 8 байт (указатель на hchan) | весь канал |
func() | 16 байт (указатель кода плюс closure) | замкнутые переменные |
interface{...} | 16 байт (itab, data) | объект под интерфейсом |
*T указатель | 8 байт | T, на который указывает |
Все они по языку значимые. Просто у некоторых “значение”, это указатель.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.