Перейти к содержимому
Теория · Указатели · урок 3 из 8

Указатель как параметр

В прошлом уроке указатель был бумажкой с адресом дома. Теперь передадим такую бумажку в функцию — так функция сможет изменить вашу переменную, а не свою копию.

Сначала вспомним, как функция получает аргументы. Она получает копию каждого из них — как ксерокопию документа. Что бы функция ни написала на ксерокопии, ваш оригинал не изменится:

GO
package main

import "fmt"

func inc(n int) {
    n++
}

func main() {
    x := 1
    inc(x)
    fmt.Println(x) // 1
}

Давайте разберём по шагам:

  1. inc(x) передаёт в функцию копию значения x.
  2. n++ увеличивает эту копию n.
  3. fmt.Println(x) печатает 1: x в main не изменился.

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

Два вида параметров

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

Параметр-значение

Параметр n int получает копию числа, как в первом примере. Вызов inc(x) не меняет x никогда: функция работает со своей ксерокопией.

Такой параметр выбирают, когда функция только вычисляет новое значение. Тогда результат проще вернуть через return.

Параметр-указатель

Если параметр имеет тип *int, функция получает указатель и пишет через него. При вызове перед переменной ставят & — так вы отдаёте функции бумажку с адресом:

GO
func inc(n *int) {
    *n++
}

func main() {
    x := 1
    inc(&x)
    inc(&x)
    fmt.Println(x) // 3
}

Разберём по шагам:

  1. inc(&x) передаёт адрес x.
  2. Внутри функции *n — это сам x, и *n++ увеличивает его на единицу.
  3. Второй вызов inc(&x) увеличивает x ещё раз, и fmt.Println(x) печатает 3.

Через тот же указатель функция может и читать: fmt.Println(*n) внутри inc напечатает текущее значение x.

Амперсанд в вызове заодно подсказывает читателю: «эта функция может изменить переменную». Вызов inc(&x) её менять может. Поэтому параметр-указатель выбирают, когда функция должна изменить переменную вызывающего кода.

Соберём оба вида параметров в одну программу. Нажмите «Следующий шаг» и следите за x в рамке main: он меняется только после вызова incPtr(&x).

ВживуюПараметр-значение и параметр-указательшаг 1 из 9 · рамок: 1

main создаёт переменную x со значением 1.

Следите за x в рамке main

По значению или по ссылке

Параметр-указатель не нарушает правило о копиях: функция по-прежнему получает копию — только копию бумажки, а не дома. Копия хранит тот же адрес, поэтому ведёт к тому же x.

Этот вопрос часто задают на собеседовании: «Как Go передаёт аргументы — по значению или по ссылке?» Ответ:

  • Go всегда передаёт аргументы по значению;
  • для обычного параметра копируется число;
  • для параметра-указателя копируется адрес.

В Go нет передачи по ссылке: при ней функция получила бы саму переменную вызывающего кода, а не копию.

Срезы и map

Со срезами всё немного хитрее. Срез — это маленькая записка из трёх полей:

  • указатель на массив;
  • длина;
  • ёмкость.

Go копирует в функцию записку, но указатель в копии ведёт к тому же массиву. Поэтому запись в элемент внутри функции видна снаружи. С map то же самое: копия ссылается на ту же таблицу ключей и значений.

GO
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]
}

Давайте разберём пример по шагам:

  1. setFirst(s, m) получает копии записки среза и map.
  2. s[0] = 9 пишет в общий массив, а m["a"] = 1 — в общую таблицу.
  3. fmt.Println(s, m) в main печатает [9 0] map[a:1]: обе записи видны снаружи.

Указатель на срез или на map для этого не нужен. Но длина среза хранится в самой записке, а записка — копия. Если функция вызовет append, вызывающий код новую длину не увидит. Поэтому функция, которая добавляет элементы, возвращает новый срез, как и сам append.

Частая ошибка

Функции с параметром *int передали саму переменную, а не её адрес:

GO
x := 1
inc(x) // ✗
fmt.Println(x)
TEXT
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 и подпишись на канал — откроются все уроки.

Войти другим способом