Avatar ·

В Go map передается по значению или по ссылке? Почему изменения внутри функции видны снаружи?

Всем привет! Я только начал изучать Go и никак не могу разобраться с тем, как передаются map в функции. Написал вот такой код: ```go package main import "fmt" func modifyMap(m map[string]int) { m["key"] = 42 fmt.Println("Внутри функции:", m) } func main() { myMap := map[string]int{"key": 0} fmt.Println("До вызова:", myMap) modifyMap(myMap) fmt.Println("После вызова:", myMap) } ``` Вывод: ``` До вызова: map[key:0] Внутри функции: map[key:42] После вызова: map[key:42] ``` Я ожидал, что map будет передана по значению (как int или string), и изменения внутри функции не повлияют на оригинальную map. Но почему-то они влияют! При этом, если я попробую сделать то же самое с массивом: ```go func modifyArray(arr [3]int) { arr[0] = 100 } func main() { a := [3]int{1, 2, 3} modifyArray(a) fmt.Println(a) // [1 2 3] — без изменений! } ``` То массив ведет себя как обычное значение. В чем разница? Map передается по ссылке или нет? И почему тогда в документации Go говорят, что в языке нет ссылочных типов? Буду благодарен за подробное объяснение! <<>>

Короткий ответ: map в Go передается по значению, но это значение — указатель на внутреннюю структуру данных

Это самый частый источник путаницы у новичков в Go. Давайте разберемся по порядку.

Что на самом деле происходит?

В Go все типы передаются по значению. Это фундаментальное правило языка. Но для map, slice, channel, function и interface это значение является ссылкой на внутреннюю структуру (по сути — указателем).

Когда вы пишете:

myMap := map[string]int{"key": 0}
modifyMap(myMap)

В функцию передается копия указателя на хеш-таблицу. Оригинальная переменная myMap и параметр m в функции — это две разные переменные, но они указывают на одну и ту же область памяти. Поэтому изменения, сделанные через m, видны через myMap.

Аналогия с указателем

Вы можете представить map как указатель на структуру (хотя технически это не совсем так):

// Примерно так map устроена внутри
type mapHeader struct {
    data  unsafe.Pointer
    // ... другие поля
}

// При передаче копируется только заголовок
func modifyMap(m mapHeader) {
    // m — копия, но m.data указывает туда же
}

Почему массив ведет себя иначе?

Массив в Go — это значение целиком. Когда вы передаете массив в функцию, копируется весь блок памяти, содержащий все элементы. Поэтому изменения внутри функции не влияют на оригинал.

arr := [3]int{1, 2, 3}
// arr занимает 24 байта (3 * 8 байт для int на 64-битной системе)
// При передаче копируются все 24 байта

Важные следствия

  • Изменение элементов map через функцию — работает (как в вашем примере)
  • Переназначение самой переменной map — НЕ работает. Если внутри функции написать m = make(map[string]int), внешняя переменная не изменится
func tryReassign(m map[string]int) {
    m = make(map[string]int) // это создает новую map
    m["new"] = 1
    // внешняя переменная все еще указывает на старую map!
}

func main() {
    myMap := map[string]int{"old": 0}
    tryReassign(myMap)
    fmt.Println(myMap) // map[old:0]
}

Как правильно изменять map через функцию?

Если вам нужно не только модифицировать содержимое, но и заменить саму map, используйте указатель на map:

func replaceMap(m *map[string]int) {
    newMap := map[string]int{"new": 1}
    *m = newMap
}

func main() {
    myMap := map[string]int{"old": 0}
    replaceMap(&myMap)
    fmt.Println(myMap) // map[new:1]
}

Итог

Запомните простое правило:

  • Значения-значения (int, float, bool, string, array, struct) — копируются целиком
  • Значения-ссылки (map, slice, channel, function, interface) — копируется только заголовок, содержащий указатель на данные

Go не имеет ссылочных типов в классическом понимании (как C++ ссылки). Вместо этого используются указатели, которые передаются по значению. Это дает предсказуемое поведение и упрощает управление памятью.

<<>>

Дополнение: как slice отличается от map в этом контексте

Slice ведет себя похоже на map, но с одним важным отличием, которое часто приводит к багам.

Когда вы передаете slice в функцию, копируется структура SliceHeader, содержащая указатель на массив данных, длину и capacity:

type SliceHeader struct {
    Data unsafe.Pointer
    Len  int
    Cap  int
}

Изменение существующих элементов через slice работает (как с map):

func modifySlice(s []int) {
    s[0] = 100 // ОК — меняет элемент в исходном массиве
}

Но! append может не сработать, если capacity не хватает:

func appendSlice(s []int) {
    s = append(s, 4) // если capacity не хватает — создается НОВЫЙ массив
    // внешняя переменная не увидит добавленный элемент!
}

func main() {
    s := []int{1, 2, 3}
    appendSlice(s)
    fmt.Println(s) // [1, 2, 3] — без изменений!
}

Чтобы append работал, нужно возвращать новый slice или использовать указатель:

func appendSliceCorrect(s *[]int) {
    *s = append(*s, 4)
}

С map такой проблемы нет, потому что map всегда растет автоматически, и указатель на данные остается валидным. Но помните: map — не потокобезопасна, для конкурентного доступа используйте sync.Map или мьютексы.