Что выведет код? Range и указатели
Условие
Определите, что выведет следующий код:
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)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Это классическая задача на понимание 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:
i = 0- Создаётся горутина, которая захватывает ссылку на переменную i (не значение!)
&i— это адрес переменной i
-
Итерация 2:
i = 1- Переменная i изменяется на 1
- Первая горутина всё ещё в очереди, но когда она выполнится, i будет 1 или больше
-
Итерация 3:
i = 2- i становится 2
-
Итерация 4:
i = 3- i становится 3
-
Цикл заканчивается: 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, и понимание её демонстрирует серьёзный уровень знания языка.