Указатели: почему & в Scanln?

Краткое описание

Объясняем, зачем нужен амперсанд & при работе с fmt.Scanln(). Знакомимся с концепцией указателей, адресами переменных и передачей данных по ссылке. Понимаем разницу между передачей по значению и по указателю.

Зачем нужен амперсанд (&) в Scanln?

var value int
fmt.Scanln(&value)  // Почему & ?

Ответ: Scanln должна изменить переменную value, но функция получает копию. Чтобы изменить оригинал, нужно передать адрес переменной (&value).

Адреса переменных

Каждая переменная имеет адрес в памяти — место на стеке, где она хранится.

var volume int = 123
fmt.Printf("Значение: %d, Адрес: %p\n", volume, &volume)
// Значение: 123, Адрес: 0xc0000b4008

Спецификатор %p — показывает адрес переменной.

Оператор & — возвращает адрес переменной.

&volume  // Адрес переменной volume

Проблема: передача по значению

Go копирует данные при передаче в функцию.

func inc(arg int) {
    arg = arg + 1
    fmt.Printf("Внутри inc: %d, адрес: %p\n", arg, &arg)
}

func main() {
    var volume int = 123
    fmt.Printf("До: %d, адрес: %p\n", volume, &volume)
    
    inc(volume)  // Передаётся КОПИЯ
    
    fmt.Printf("После: %d, адрес: %p\n", volume, &volume)
}

Вывод:

До: 123, адрес: 0xc0000b4008
Внутри inc: 124, адрес: 0xc0000b4010  ← ДРУГОЙ адрес!
После: 123, адрес: 0xc0000b4008       ← НЕ изменилось!

Проблема: arg и volumeразные переменные в разных местах памяти. Изменение arg не влияет на volume.

Решение: указатели

Указатель — переменная, хранящая адрес другой переменной.

var volume int = 123
var pointer *int = &volume  // Указатель на volume

Синтаксис:

  • *int — тип “указатель на int”
  • &volume — адрес переменной volume

Работа с указателем

var volume int = 123
var pointer *int = &volume

fmt.Printf("volume: %d, адрес: %p\n", volume, &volume)
fmt.Printf("pointer хранит: %p, pointer расположен: %p\n", pointer, &pointer)

Вывод:

volume: 123, адрес: 0xc0000b4008
pointer хранит: 0xc0000b4008, pointer расположен: 0xc0000b4010

Визуализация:

Stack (функция main)
┌──────────────────────┐
│ volume = 123         │  адрес: 0x...008
├──────────────────────┤
│ pointer = 0x...008   │  адрес: 0x...010 (хранит адрес volume)
└──────────────────────┘
       │
       └─────┐ (pointer указывает на volume)
             ▼

Разыменование указателя

Оператор * — получить значение, на которое указывает указатель.

var volume int = 123
var pointer *int = &volume

fmt.Println(*pointer)  // 123 (значение, на которое указывает pointer)

Изменение через указатель

var volume int = 123
var pointer *int = &volume

*pointer = 200  // Изменяем volume через pointer

fmt.Println(volume)   // 200 (изменилось!)
fmt.Println(*pointer) // 200

Работает: *pointer и volumeодна и та же переменная в памяти!

Передача указателя в функцию

Чтобы функция могла изменить переменную, передаём указатель.

func inc(arg *int) {  // Принимает указатель
    *arg = *arg + 1   // Разыменование для чтения и записи
    fmt.Printf("Внутри inc: %d, адрес: %p\n", *arg, arg)
}

func main() {
    var volume int = 123
    fmt.Printf("До: %d, адрес: %p\n", volume, &volume)
    
    inc(&volume)  // Передаём АДРЕС
    
    fmt.Printf("После: %d, адрес: %p\n", volume, &volume)
}

Вывод:

До: 123, адрес: 0xc0000b4008
Внутри inc: 124, адрес: 0xc0000b4008  ← ТОТ ЖЕ адрес!
После: 124, адрес: 0xc0000b4008       ← ИЗМЕНИЛОСЬ!

Работает: функция получает адрес, работает с оригиналом.

Полный пример

package main

import "fmt"

func inc(arg *int) {
    fmt.Printf("2. Внутри inc: значение=%d, адрес=%p\n", *arg, arg)
    *arg = *arg + 1
    fmt.Printf("3. После изменения: значение=%d, адрес=%p\n", *arg, arg)
}

func main() {
    var volume int = 123
    fmt.Printf("1. До: значение=%d, адрес=%p\n", volume, &volume)
    
    inc(&volume)  // Передаём адрес
    
    fmt.Printf("4. После: значение=%d, адрес=%p\n", volume, &volume)
}

Вывод:

1. До: значение=123, адрес=0xc0000b4008
2. Внутри inc: значение=123, адрес=0xc0000b4008
3. После изменения: значение=124, адрес=0xc0000b4008
4. После: значение=124, адрес=0xc0000b4008

Все адреса одинаковые → работаем с одной переменной.

Теперь про Scanln

var value int
fmt.Scanln(&value)  // Передаём адрес

Что происходит:

  1. Scanln принимает *int (указатель)
  2. Получает адрес переменной value
  3. Читает ввод пользователя
  4. Записывает в адрес через разыменование: *arg = введённое_значение
  5. Переменная value изменяется

Без & не сработает:

var value int
fmt.Scanln(value)  // ❌ ОШИБКА: передана копия, а нужен адрес

Синтаксис указателей

Символ Значение Пример
*int Тип “указатель на int” var ptr *int
&x Адрес переменной x ptr = &x
*ptr Разыменование (получить значение) val := *ptr
*ptr = 10 Запись по адресу *ptr = 10

Ключевые моменты

1. Go передаёт данные по значению (копирует)

func foo(x int) {
    x = 10  // Изменяет копию
}

2. Чтобы изменить оригинал — нужен указатель

func foo(x *int) {
    *x = 10  // Изменяет оригинал
}
// Вызов: foo(&variable)

3. Адрес — &переменная

&volume  // Адрес переменной volume

4. Разыменование — *указатель

*pointer  // Значение, на которое указывает pointer

5. Scanln требует адрес

fmt.Scanln(&value)  // & обязателен!

6. Тип указателя — *тип

var ptr *int     // Указатель на int
var ptr2 *string // Указатель на string

7. Один адрес = одна переменная

Если адреса одинаковые → работаем с одной переменной в памяти.

Визуальная схема

Передача по значению (копирование)

main()                    inc(arg int)
┌──────────────┐         ┌──────────────┐
│ volume = 123 │ ──────→ │ arg = 123    │ (копия)
│ адрес: 0x008 │         │ адрес: 0x010 │
└──────────────┘         └──────────────┘
                          arg = 124
                          
volume остаётся 123 ❌

Передача по указателю (адрес)

main()                    inc(arg *int)
┌──────────────┐         ┌──────────────┐
│ volume = 123 │ ────┐   │ arg = 0x008  │ (адрес)
│ адрес: 0x008 │     └──→│              │
└──────────────┘         └──────────────┘
       ▲                        │
       └────────────────────────┘
              *arg = 124
              
volume становится 124 ✅

Что запомнить

  • & — получить адрес переменной
  • *тип — объявить указатель
  • *ptr — разыменовать указатель (получить/изменить значение)
  • Go копирует данные при передаче в функцию
  • Указатели позволяют изменять оригинал
  • fmt.Scanln(&var) — нужен &, чтобы функция могла изменить переменную
  • Адрес указывает на место в памяти
  • Одинаковые адреса = одна переменная

Для новичков

Не обязательно глубоко понимать указатели на старте. Достаточно знать:

fmt.Scanln(&переменная) — всегда пишем &
& нужен, чтобы функция могла изменить переменную
✅ Позже указатели станут понятнее с практикой

Главное: можете использовать Scanln, не думая о деталях. Понимание придёт с опытом!