JavaRush /Курсы /Go SELF /В Go всё передаётся по значению

В Go всё передаётся по значению

Go SELF
19 уровень , 1 лекция
Открыта

1. Значения, параметры и указатели

Если вы уже писали на языках, где «по умолчанию всё по ссылке» или «по умолчанию всё по значению», Go может казаться хитрым: вы передали слайс в функцию — и он изменился; передали map — и она изменилась; передали int — и он не изменился. Хочется сказать: «ну вот же, значит не всё по значению!». Но в Go всё честно — просто важно различать изменение переменной и изменение данных, на которые переменная указывает.

Представьте, что переменная — это записка на столе. Передать «по значению» — это сделать копию записки и отдать её в функцию. Но если на записке написан адрес склада, то копия записки всё равно указывает на тот же склад. Функция может изменить содержимое склада, и вы «снаружи» это увидите. Однако сама записка (ваша переменная) при этом не обязана меняться.

Присваивание копирует значение

Прежде чем говорить про функции, полезно закрепить совсем приземлённую вещь: когда вы пишете b = a, Go копирует значение a в b. Для простых типов это максимально очевидно: скопировали число — получили другое число. Для “составных” значений (указатель, слайс, map) копируется тоже значение, просто оно “не простое” и содержит внутри ссылки на данные.

Давайте разогреемся на числах: тут магии нет, и это хорошо — магию мы оставим для фокусников и некоторых JavaScript‑фреймворков.

package main

import "fmt"

func main() {
	a := 10
	b := a
	b = 99

	fmt.Println(a) // 10
	fmt.Println(b) // 99
}

Именно такой “стиль мышления” нужно держать в голове и дальше: копируются значения. Разница начинается, когда значение само по себе является “адресом”.

Параметры функции — это локальные копии аргументов

Ключевой момент: при вызове функции Go создаёт новые переменные‑параметры и копирует в них значения аргументов. То есть параметр x внутри функции — это не “волшебный порт” в переменную вызывающего кода. Это отдельная переменная, просто инициализированная копией.

Самый честный пример — инкремент числа.

package main

import "fmt"

func incVal(x int) {
	x++
	fmt.Println("inside:", x) // inside: 11
}

func main() {
	a := 10
	incVal(a)
	fmt.Println("outside:", a) // outside: 10
}

Это фундаментальная модель: параметр — копия.

Если вам нужно “изменить a снаружи”, у вас есть два нормальных пути (и они не взаимоисключающие): либо вернуть новое значение и присвоить его, либо передать указатель. Дальше разберём, почему это не противоречит “передаче по значению”.

Указатели тоже передаются по значению: копируется адрес

Здесь обычно у студентов происходит маленький когнитивный взрыв: «если указатель передаётся по значению, почему тогда можно менять a через *p внутри функции?». Ответ простой: копируется сам указатель, то есть адрес. А адрес (в обеих копиях) указывает на одно и то же место в памяти. Поэтому изменение по адресу видно всем, кто держит копию этого адреса.

package main

import "fmt"

func incPtr(p *int) {
	*p++
}

func main() {
	a := 10
	incPtr(&a)
	fmt.Println(a) // 11
}

Обратите внимание на важную тонкость: мы не “передали переменную a по ссылке”, мы передали значение, которое является адресом a. И это значение скопировалось в параметр p.

Чтобы почувствовать разницу, попробуем внутри функции поменять сам указатель (то есть сделать так, чтобы он “смотрел” на другое место). Это не изменит a, потому что вы поменяете только локальную копию указателя.

package main

import "fmt"

func pointToOther(p *int) {
	x := 777
	p = &x // меняем только локальную копию p
}

func main() {
	a := 10
	pointToOther(&a)
	fmt.Println(a) // 10
}

Это важное наблюдение: можно менять данные по адресу, но переназначение самого указателя не влияет на то, что было снаружи, потому что указатель передан по значению.

3. Слайсы и map: «ссылочное» поведение без передачи по ссылке

Со слайсами всё интереснее, потому что слайс — это не массив. Слайс — это “окошко” (view) на массив данных. Внутри слайса есть служебная часть: указатель на данные, длина (len) и ёмкость (cap). Именно поэтому слайс ведёт себя “ссылочно”: два разных слайса могут смотреть на один и тот же массив данных.

Эту внутреннюю идею стоит не просто запомнить, а “увидеть” как схему:

flowchart LR
    A[Переменная s: slice header] -->|ptr| B[(Underlying array)]
    A -->|len| L[len]
    A -->|cap| C[cap]

Факт про “ptr + len + cap” важен не как теория ради теории: он объясняет 80% странностей со слайсами.

Почему изменение s[0] видно снаружи

Если функция получает копию slice header, то оба заголовка указывают на один и тот же underlying array. Поэтому изменение элемента меняет общие данные.

package main

import "fmt"

func setFirst(s []int) {
	if len(s) > 0 {
		s[0] = 99
	}
}

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

Снаружи видно изменение, но это не “нарушение правила”. Это следствие того, что в копии заголовка хранится тот же указатель на те же данные.

Почему append часто требует return

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

Это не “совет по стилю”, это необходимость, вытекающая из устройства слайса и из того, что изменение длины — это изменение заголовка, а заголовок у нас локальная копия.

Давайте сделаем пример неправильного кода (классика жанра):

package main

import "fmt"

func addTaskBad(tasks []string, title string) {
	tasks = append(tasks, title) // изменили локальную копию заголовка
}

func main() {
	tasks := make([]string, 0, 1)
	addTaskBad(tasks, "learn Go")
	fmt.Println(tasks) // []
}

Почти всегда студент на этом месте говорит: “Но я же добавил!”. Да, вы добавили — но в локальную переменную tasks внутри функции, потому что присваивание результата append меняет header (len/cap/ptr).

Правильно — вернуть слайс:

package main

import "fmt"

func addTask(tasks []string, title string) []string {
	tasks = append(tasks, title)
	return tasks
}

func main() {
	tasks := make([]string, 0, 1)
	tasks = addTask(tasks, "learn Go")
	fmt.Println(tasks) // [learn Go]
}

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

Кстати, многие функции стандартной библиотеки и пакет slices возвращают новый слайс именно по этой причине: они меняют длину/содержимое и хотят, чтобы вызывающий код работал с корректным результатом, а не со старым “инвалидированным” заголовком.

map: изменение содержимого видно, переназначение — нет

С map ощущения ещё хитрее: вы почти всегда видите “изменения снаружи”, даже если ничего не возвращаете. От этого легко сделать неверный вывод: “map передаётся по ссылке”. На практике модель такая же, как и со слайсом: значение map содержит ссылку на внутреннее хранилище (таблицу), и при передаче копируется “заголовок”, который указывает на те же данные.

То есть когда вы делаете m["x"] = 1 в функции, вы меняете общее хранилище, и это видно снаружи.

package main

import "fmt"

func markDone(done map[string]bool, title string) {
	done[title] = true
}

func main() {
	done := make(map[string]bool)
	markDone(done, "learn Go")
	fmt.Println(done["learn Go"]) // true
}

Но и тут важно различать “изменить содержимое” и “переназначить переменную”. Если внутри функции вы сделаете done = make(map[string]bool), вы создадите новую карту и присвоите её локальной переменной done, а снаружи ничего не поменяется.

package main

import "fmt"

func resetMapBad(done map[string]bool) {
	done = make(map[string]bool) // переназначили только локальную переменную
	done["x"] = true
}

func main() {
	done := map[string]bool{"learn Go": true}
	resetMapBad(done)
	fmt.Println(done["learn Go"]) // true
	fmt.Println(done["x"])        // false
}

Если вы хотите “сбросить карту”, делайте это как со слайсом: либо возвращайте новую map, либо очищайте существующую (очистка — это изменение общего хранилища, и оно будет видно снаружи).

Шпаргалка: что копируется и что может поменяться

Когда вы читаете код, мозгу полезно не держать 30 исключений, а держать одну опорную таблицу: что будет, если функция поменяет параметр, и что будет, если функция поменяет данные “внутри”.

Тип аргумента Что копируется при вызове x = ... внутри функции влияет на внешнее? Изменение “внутренностей” влияет на внешнее?
int, bool, string
значение нет не применимо
*T
адрес нет (переназначение
p
)
да (
*p = ...
)
[]T
заголовок (ptr/len/cap) нет (переназначение
s
)
да (
s[i] = ...
)
map[K]V
заголовок (ссылка на таблицу) нет (переназначение
m
)
да (
m[k] = v
,
delete(m, k)
)

Ключевой смысл этой таблицы: правило “всё передаётся по значению” не ломается ни в одном из случаев. Просто иногда значение содержит “ссылку” на общие данные.

4. Мини‑приложение «Список задач» без структур

Давайте аккуратно продолжим учебный проект (условно назовём его tasklite). Мы пока не используем структуры, поэтому будем хранить список задач как []string, а статус выполнения как map[string]bool.

Идея простая: у нас будут маленькие функции, которые либо модифицируют общие данные (например, map), либо возвращают новое значение (например, обновлённый слайс).

Добавление задачи: функция возвращает слайс

Начнём с безопасного добавления задачи. Мы уже видели, что вариант без return очень легко “теряет” добавление.

package main

import "fmt"

func addTask(tasks []string, title string) []string {
	return append(tasks, title)
}

func main() {
	tasks := make([]string, 0)
	tasks = addTask(tasks, "learn Go")
	tasks = addTask(tasks, "drink water")

	fmt.Println(tasks) // [learn Go drink water]
}

Здесь важно, что мы всегда присваиваем результат. Это тот же принцип, почему append возвращает значение, и почему многие функции из slices возвращают новый слайс: изменения длины требуют обновлённого заголовка.

Отметить “done”: можно без return, но важно понимать контракт

Со статусом done проще: запись в map меняет общее хранилище, поэтому возвращать карту не обязательно.

package main

import "fmt"

func markDone(done map[string]bool, title string) {
	done[title] = true
}

func main() {
	done := make(map[string]bool)

	markDone(done, "learn Go")
	fmt.Println(done["learn Go"]) // true
}

Но вот здесь и рождается типичный баг начинающего: “раз с картой можно не возвращать, значит и со слайсом можно”. Нет: со слайсом зависит от того, меняете ли вы только элементы или меняете длину/ёмкость.

Переименование задачи: слайс можно менять “на месте”

Если задача хранится как строка в массиве, то можно менять элемент по индексу — это изменение общих данных underlying array.

package main

import "fmt"

func renameTask(tasks []string, i int, newTitle string) {
	if i < 0 || i >= len(tasks) {
		return
	}
	tasks[i] = newTitle
}

func main() {
	tasks := []string{"learn Go", "drink water"}
	renameTask(tasks, 0, "learn Go seriously")

	fmt.Println(tasks) // [learn Go seriously drink water]
}

Здесь return не нужен, потому что длина не менялась, заголовок не менялся — мы меняли содержимое массива, на который оба заголовка указывают.

Сброс списка задач: “переназначить” vs “очистить”

Иногда хочется написать “сбросить список задач”. Если вы напишете так:

package main

import "fmt"

func resetTasksBad(tasks []string) {
	tasks = nil // это переназначение локальной переменной
}

func main() {
	tasks := []string{"a", "b"}
	resetTasksBad(tasks)
	fmt.Println(tasks) // [a b]
}

…то ничего снаружи не произойдёт, потому что вы переназначили локальную копию заголовка.

Нормальный “по‑Go” вариант для новичка — вернуть новый слайс:

package main

import "fmt"

func resetTasks(tasks []string) []string {
	return tasks[:0] // длина 0, но память может остаться (это нормально)
}

func main() {
	tasks := []string{"a", "b"}
	tasks = resetTasks(tasks)

	fmt.Println(tasks)      // []
	fmt.Println(len(tasks)) // 0
}

5. Типичные ошибки

Ошибка №1: ожидать, что func inc(x int) изменит исходную переменную.
Это один из самых частых “детских” багов, и он почти всегда из привычки к другим языкам или из ощущения, что “функция же работает с переменной”. В Go параметр — копия. Хотите новое значение — возвращайте его и присваивайте. Хотите изменять “на месте” — передавайте указатель, но только если это действительно делает код проще, а не страшнее.

Ошибка №2: делать append внутри функции и не возвращать результат.
Этот баг особенно коварен тем, что иногда “вроде работает” (если cap позволяет расшириться без переаллокации), а иногда “вдруг не работает”. Правильная ментальная модель такая: append возвращает новый слайс, и если вы меняете длину, вы обязаны работать с возвращённым значением. Это следует из того, что слайс — это заголовок (ptr/len/cap), и его изменение не может магически обновить внешний заголовок.

Ошибка №3: путать “изменить элемент” и “переназначить переменную”.
Фраза “я изменил слайс” может означать две разные вещи: “я поменял s[0]” и “я сделал s = append(s, ...)”. В первом случае меняются общие данные, во втором — меняется заголовок слайса, а заголовок в функции локальный. С map аналогично: m[k] = v — меняет общее состояние, а m = make(map[K]V) — просто переназначает локальную переменную.

Ошибка №4: пытаться “сбросить map” через m = make(...) внутри функции и ждать, что снаружи она станет пустой.
Это та же логика, что и с указателем: вы меняете только локальную копию значения. Если нужно именно заменить карту — возвращайте новую map и присваивайте снаружи. Если нужно очистить текущую — удаляйте ключи (это уже изменение общего состояния).

Ошибка №5: лечить всё указателями.
Когда человек впервые понял, что указатель “даёт изменения снаружи”, возникает соблазн передавать *int, *[]string, *map[string]bool вообще везде. Обычно это делает код более хрупким: появляется nil, появляется больше aliasing‑эффектов, сложнее читать, кто владеет данными. Хороший стиль — сначала пытаться сделать функцию чистой (вернула новое значение), и только если это реально неудобно — переходить к изменению по указателю.

1
Задача
Go SELF, 19 уровень, 1 лекция
Недоступна
Копия аргумента
Копия аргумента
1
Задача
Go SELF, 19 уровень, 1 лекция
Недоступна
Заголовок слайса
Заголовок слайса
1
Задача
Go SELF, 19 уровень, 1 лекция
Недоступна
Карта и мираж
Карта и мираж
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ