Передача по значению или по указателю
Один из первых вопросов на любом Go-собеседовании: почему функция иногда «не видит» изменения, а иногда видит?
Передача по значению — это когда функция получает не саму переменную, а её копию. Изменения внутри функции не затрагивают оригинал. В Go именно так передаётся всё, включая указатели — просто указатель хранит адрес, и его копия указывает туда же.
Что выведет этот код?
Смотрим на структуру «Кот» и функцию, которая пытается отпраздновать его день рождения:
package main
import "fmt"
type Cat struct {
Name string
Age int
}
func birthday(c Cat) {
c.Age++
}
func main() {
tom := Cat{Name: "Tom", Age: 3}
birthday(tom)
fmt.Println(tom.Age)
}
Вывод:
3
Коту не исполнилось 4 года. Функция birthday честно увеличила Age — но не у настоящего кота, а у его точной копии, которая перестала существовать, как только функция закончилась.
Почему так происходит
В Go, когда вы передаёте структуру в функцию как func birthday(c Cat), компилятор берёт всё содержимое переменной — все поля структуры — и копирует их в новую переменную c внутри функции. Это отдельный кусок памяти. Что бы функция ни делала с c, снаружи её просто не существует.
Представьте, что вы даёте другу не сам паспорт, а его ксерокопию. Он может хоть всю её исчеркать — в вашем настоящем паспорте ничего не изменится.
Как починить: передаём указатель
Если нужно, чтобы функция меняла оригинал, ей нужно дать не копию, а адрес переменной в памяти — указатель. В Go это делается символом & (взять адрес) при вызове и * (тип «указатель на») в объявлении параметра:
package main
import "fmt"
type Cat struct {
Name string
Age int
}
func birthday(c *Cat) {
c.Age++
}
func main() {
tom := Cat{Name: "Tom", Age: 3}
birthday(&tom)
fmt.Println(tom.Age)
}
Вывод:
4
Теперь работает. &tom — это «адрес переменной tom в памяти», что-то вроде номера ячейки. Параметр c *Cat получает этот адрес и внутри функции обращается по нему к настоящему коту, а не к копии.
Как это работает под капотом
Внутри функции строка c.Age++ для указателя — это сокращённая запись. Go автоматически «разыменовывает» указатель, то есть идёт по адресу и работает уже с данными, которые там лежат:
func birthday(c *Cat) {
c.Age++
}
// Go сам разворачивает это в (*c).Age++
// вручную писать (*c).Age тоже можно, но никто так не делает
Полезно держать в голове простую модель: у каждой переменной есть адрес в памяти (как номер дома) и значение (что находится по этому адресу). Указатель — это просто переменная, которая хранит чужой адрес вместо значения. *p означает «сходить по адресу и взять то, что там лежит».
А почему слайсы вдруг «видят» изменения?
Тут начинается ловушка, в которую попадают даже опытные разработчики. Слайсы, карты (map) и каналы в Go — это не сами данные, а маленькие структуры-обёртки, внутри которых лежит указатель на настоящие данные. Когда вы копируете слайс, копируется обёртка, а указатель внутри неё продолжает смотреть на тот же массив в памяти:
package main
import "fmt"
func addOne(nums []int) {
nums[0] = 100
}
func main() {
arr := []int{1, 2, 3}
addOne(arr)
fmt.Println(arr)
}
Вывод:
[100 2 3]
Хотя nums — это копия заголовка слайса, указатель внутри неё ведёт на тот же массив, что и arr. Изменение nums[0] и изменение arr[0] — это буквально одна и та же ячейка памяти. Подробнее про устройство слайса — в следующем уроке.
Value receiver и pointer receiver у методов
Та же логика распространяется на методы структур. Метод можно объявить с получателем (receiver) по значению или по указателю — это меняет, работает ли он с копией или с оригиналом:
type Cat struct {
Name string
Age int
}
// value receiver — работает с КОПИЕЙ Cat
func (c Cat) Older() Cat {
c.Age++
return c
}
// pointer receiver — работает с ОРИГИНАЛОМ через адрес
func (c *Cat) GrowUp() {
c.Age++
}
Вызов tom.Older() вернёт нового кота на год старше, но самого tom не тронет. А вызов tom.GrowUp() изменит tom напрямую — Go сам подставит &tom, когда видит, что метод объявлен с указателем-получателем.
Частые ошибки на собеседовании
- Путают «Go передаёт по ссылке» и «Go передаёт по значению, а указатель — это тоже значение (адрес)». Второе верно. Указатель не превращает Go в язык передачи по ссылке — он просто передаёт по значению адрес.
- Ждут, что структура внутри функции изменится сама по себе, забыв, что параметр — это независимая копия.
- Думают, что раз слайс «сам меняется», то и append внутри функции всегда виден снаружи — это не так, если слайс при этом пересоздаётся (см. следующий урок).
- Смешивают receiver: в одном типе часть методов с value receiver, часть — с pointer receiver. Компилятор это разрешит, но на собеседовании спросят «почему так плохо» — правило: если хоть одному методу типа нужен pointer receiver, делайте pointer receiver у всех методов этого типа для единообразия.
Итоги-шпаргалка
- Go всегда передаёт аргументы по значению — то есть копирует то, что передаёте.
- Указатель (
*T) — это тоже значение, просто его содержимое — адрес другой переменной. Передав указатель, функция получает возможность менять оригинал по этому адресу. &xберёт адрес переменной,*pразыменовывает указатель (идёт по адресу и достаёт значение).- Слайсы, карты и каналы «выглядят» изменяемыми при передаче, потому что внутри них уже есть указатель на данные — копируется только обёртка.
- Pointer receiver у метода — для изменения оригинала или для больших структур (не копировать зря); value receiver — когда метод только читает данные и структура маленькая.