Указатель как параметр
В прошлом уроке указатель был бумажкой с адресом дома. Теперь передадим такую бумажку в функцию — так функция сможет изменить вашу переменную, а не свою копию.
Сначала вспомним, как функция получает аргументы. Она получает копию каждого из них — как ксерокопию документа. Что бы функция ни написала на ксерокопии, ваш оригинал не изменится:
package main
import "fmt"
func inc(n int) {
n++
}
func main() {
x := 1
inc(x)
fmt.Println(x) // 1
}Давайте разберём по шагам:
inc(x)передаёт в функцию копию значенияx.n++увеличивает эту копиюn.fmt.Println(x)печатает1:xвmainне изменился.
Чтобы функция меняла переменную вызывающего кода, ей нужна не копия значения, а адрес этой переменной.
Два вида параметров
Параметр функции может хранить само значение или адрес переменной. От этого зависит, увидит ли вызывающий код изменения.
Параметр-значение
Параметр n int получает копию числа, как в первом примере. Вызов inc(x) не меняет x никогда: функция работает со своей ксерокопией.
Такой параметр выбирают, когда функция только вычисляет новое значение. Тогда результат проще вернуть через return.
Параметр-указатель
Если параметр имеет тип *int, функция получает указатель и пишет через него. При вызове перед переменной ставят & — так вы отдаёте функции бумажку с адресом:
func inc(n *int) {
*n++
}
func main() {
x := 1
inc(&x)
inc(&x)
fmt.Println(x) // 3
}Разберём по шагам:
inc(&x)передаёт адресx.- Внутри функции
*n— это самx, и*n++увеличивает его на единицу. - Второй вызов
inc(&x)увеличиваетxещё раз, иfmt.Println(x)печатает3.
Через тот же указатель функция может и читать: fmt.Println(*n) внутри inc напечатает текущее значение x.
Амперсанд в вызове заодно подсказывает читателю: «эта функция может изменить переменную». Вызов inc(&x) её менять может. Поэтому параметр-указатель выбирают, когда функция должна изменить переменную вызывающего кода.
Соберём оба вида параметров в одну программу. Нажмите «Следующий шаг» и следите за x в рамке main: он меняется только после вызова incPtr(&x).
1–4package main · import "fmt"5func incValue(n int) {6 n++7}8 9func incPtr(n *int) {10 *n++11}12 13func main() {14 x := 115 incValue(x)16 fmt.Println(x)17 incPtr(&x)18 fmt.Println(x)19}
main создаёт переменную x со значением 1.
По значению или по ссылке
Параметр-указатель не нарушает правило о копиях: функция по-прежнему получает копию — только копию бумажки, а не дома. Копия хранит тот же адрес, поэтому ведёт к тому же x.
Этот вопрос часто задают на собеседовании: «Как Go передаёт аргументы — по значению или по ссылке?» Ответ:
- Go всегда передаёт аргументы по значению;
- для обычного параметра копируется число;
- для параметра-указателя копируется адрес.
В Go нет передачи по ссылке: при ней функция получила бы саму переменную вызывающего кода, а не копию.
Срезы и map
Со срезами всё немного хитрее. Срез — это маленькая записка из трёх полей:
- указатель на массив;
- длина;
- ёмкость.
Go копирует в функцию записку, но указатель в копии ведёт к тому же массиву. Поэтому запись в элемент внутри функции видна снаружи. С map то же самое: копия ссылается на ту же таблицу ключей и значений.
package main
import "fmt"
func setFirst(s []int, m map[string]int) {
s[0] = 9
m["a"] = 1
}
func main() {
s := []int{0, 0}
m := map[string]int{}
setFirst(s, m)
fmt.Println(s, m) // [9 0] map[a:1]
}Давайте разберём пример по шагам:
setFirst(s, m)получает копии записки среза иmap.s[0] = 9пишет в общий массив, аm["a"] = 1— в общую таблицу.fmt.Println(s, m)вmainпечатает[9 0] map[a:1]: обе записи видны снаружи.
Указатель на срез или на map для этого не нужен. Но длина среза хранится в самой записке, а записка — копия. Если функция вызовет append, вызывающий код новую длину не увидит. Поэтому функция, которая добавляет элементы, возвращает новый срез, как и сам append.
Частая ошибка
Функции с параметром *int передали саму переменную, а не её адрес:
x := 1
inc(x) // ✗
fmt.Println(x)cannot use x (variable of type int) as *int value in argument to incПрограмма не компилируется: функция ждёт бумажку с адресом, а получает число. Исправление — inc(&x). Обратная ошибка тоже не компилируется: параметру типа int нельзя передать &x, потому что у &x тип *int, а не int.
Главное
- Go всегда передаёт аргументы по значению: функция получает копию.
- Чтобы функция изменила вашу переменную, передайте её адрес: параметр
*int, вызовinc(&x). - Срез и
mapв аргументе делят данные с оригиналом, но новую длину послеappendвызывающий код не увидит.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.