← Назад к вопросам

Что выведет код? Range и указатели

2.3 Middle🔥 181 комментариев
#Конкурентность и горутины#Основы Go

Условие

Определите, что выведет следующий код:

package main

import "fmt"

func main() {
    ch := make(chan *int, 4)
    
    for i := 0; i < 4; i++ {
        go func() {
            ch <- &i
        }()
    }
    
    for j := 0; j < 4; j++ {
        value := <-ch
        fmt.Println(*value)
    }
}

Вопросы

  1. Что выведет программа?
  2. Почему результат может быть неожиданным?
  3. Как исправить код?

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

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

Ответ на вопросы

1. Что выведет программа?

Программа может вывести:

4
4
4
4

ИЛИ какую-то комбинацию вроде 2 3 4 4, но скорее всего 4 4 4 4.

НЕ выведет 0, 1, 2, 3 — это самое важное!

2. Почему результат неожиданный?

Проблема в классической ошибке с closures и переменными цикла:

Анализ кода

for i := 0; i < 4; i++ {        // i = 0, 1, 2, 3
    go func() {
        ch <- &i              // ключевая ошибка!
    }()
}

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

  1. Итерация 1: i = 0

    • Создаётся горутина, которая захватывает ссылку на переменную i (не значение!)
    • &i — это адрес переменной i
  2. Итерация 2: i = 1

    • Переменная i изменяется на 1
    • Первая горутина всё ещё в очереди, но когда она выполнится, i будет 1 или больше
  3. Итерация 3: i = 2

    • i становится 2
  4. Итерация 4: i = 3

    • i становится 3
  5. Цикл заканчивается: i = 4 (условие i < 4 ложно)

    • Теперь ГОРУТИНЫ ВЫПОЛНЯЮТСЯ
    • Все четыре горутины отправляют &i в канал
    • Все они отправляют адрес ОДНОЙ переменной i, которая уже равна 4

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

Оперативная память:
┌─────────────┐
│ i: 4 (на стеке main)│
└─────────────┘
      ↑
   адрес 0x1234

Все четыре горутины отправляют в канал: &i = 0x1234

Когда читаем из канала:
value := <-ch          // value = 0x1234
*value                 // разыменовываем → i.value = 4

Пошаговое выполнение

1. Цикл for создаёт 4 горутины:
   for i := 0; i < 4; i++
   ├─ i=0: go func() { ch <- &i }()  // отправить адрес i
   ├─ i=1: go func() { ch <- &i }()  // отправить адрес i
   ├─ i=2: go func() { ch <- &i }()  // отправить адрес i
   └─ i=3: go func() { ch <- &i }()  // отправить адрес i
   
   После цикла: i = 4

2. Горутины начинают выполняться (порядок неопределён):
   Горутина 1: ch <- &i  // &i указывает на переменную i, которая СЕЙЧАС равна 4
   Горутина 2: ch <- &i  // всё ещё 4
   Горутина 3: ch <- &i  // всё ещё 4
   Горутина 4: ch <- &i  // всё ещё 4

3. Main читает из канала:
   j=0: value = <-ch; *value = 4
   j=1: value = <-ch; *value = 4
   j=2: value = <-ch; *value = 4
   j=3: value = <-ch; *value = 4

Почему это происходит?

Closure захватывает переменную, не значение:

// ❌ Плохо
for i := 0; i < 4; i++ {
    go func() {
        ch <- &i  // захватывает переменную i
    }()
}

// ✅ Хорошо вариант 1: передать параметр
for i := 0; i < 4; i++ {
    go func(val int) {
        ch <- &val  // захватывает параметр val
    }(i)  // передаём значение i в момент создания
}

// ✅ Хорошо вариант 2: локальная переменная
for i := 0; i < 4; i++ {
    i := i  // создаём локальную копию
    go func() {
        ch <- &i
    }()
}

Исправленный код (Вариант 1 — передать параметр)

func main() {
    ch := make(chan *int, 4)
    
    for i := 0; i < 4; i++ {
        go func(val int) {  // параметр функции
            ch <- &val      // отправляем адрес параметра, не i
        }(i)  // передаём значение i
    }
    
    for j := 0; j < 4; j++ {
        value := <-ch
        fmt.Println(*value)  // выведет: 0, 1, 2, 3
    }
}

Исправленный код (Вариант 2 — локальная переменная)

func main() {
    ch := make(chan *int, 4)
    
    for i := 0; i < 4; i++ {
        i := i  // создаём новую локальную переменную i для каждой итерации
        go func() {
            ch <- &i
        }()
    }
    
    for j := 0; j < 4; j++ {
        value := <-ch
        fmt.Println(*value)  // выведет: 0, 1, 2, 3
    }
}

Исправленный код (Вариант 3 — без указателей)

func main() {
    ch := make(chan int, 4)  // хранить значения, не указатели
    
    for i := 0; i < 4; i++ {
        go func(val int) {
            ch <- val  // отправляем значение
        }(i)
    }
    
    for j := 0; j < 4; j++ {
        value := <-ch
        fmt.Println(value)  // выведет: 0, 1, 2, 3
    }
}

Почему вариант 1 работает?

go func(val int) {  // val — это ПАРАМЕТР, локальная переменная функции
    ch <- &val
}(i)  // передаём значение i в момент создания

// Каждая горутина имеет СВОЮ переменную val:
// Горутина 1: val = 0 (адрес: 0x1000)
// Горутина 2: val = 1 (адрес: 0x1100)
// Горутина 3: val = 2 (адрес: 0x1200)
// Горутина 4: val = 3 (адрес: 0x1300)

Почему вариант 2 работает?

for i := 0; i < 4; i++ {
    i := i  // в КАЖДОЙ итерации создаём новую переменную i
    //      на стеке блока for
    go func() {
        ch <- &i  // захватываем i, но для каждой итерации это разная переменная
    }()
}

// Переменные на стеке:
// Итерация 1: i (блочная) = 0
// Итерация 2: i (блочная) = 1
// Итерация 3: i (блочная) = 2
// Итерация 4: i (блочная) = 3
// (каждая имеет свой адрес)

Когда это может быть проблема?

Эта ошибка встречается везде, где используются closures:

// ❌ При создании callbacks
buttons := []*Button{}
for i := 0; i < 4; i++ {
    btn := NewButton()
    btn.OnClick = func() {
        fmt.Println(i)  // всегда выведет 3
    }
    buttons = append(buttons, btn)
}

// ✅ Исправить
for i := 0; i < 4; i++ {
    i := i
    btn := NewButton()
    btn.OnClick = func() {
        fmt.Println(i)  // выведет 0, 1, 2, 3
    }
    buttons = append(buttons, btn)
}

Ключевой вывод

Closures в Go захватывают переменные, не значения. Если переменная цикла должна быть частью closure, либо передайте её как параметр, либо создайте локальную копию в каждой итерации.

Это одна из самых частых ошибок в Go, и понимание её демонстрирует серьёзный уровень знания языка.

Что выведет код? Range и указатели | PrepBro