В Go map передается по значению или по ссылке? Почему изменения внутри функции видны снаружи?
Короткий ответ: 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 или мьютексы.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги