Вопрос: Почему Golang обрабатывает закрытие по-разному в goroutines?


Рассмотрим следующий код Голанга (также на Игровая площадка ):

package main

import "fmt"
import "time"

func main() {
    for _, s := range []string{"foo", "bar"} {
        x := s
        func() {
            fmt.Printf("s: %s\n", s)
            fmt.Printf("x: %s\n", x)
        }()
    }
    fmt.Println()
    for _, s := range []string{"foo", "bar"} {
        x := s
        go func() {
            fmt.Printf("s: %s\n", s)
            fmt.Printf("x: %s\n", x)
        }()
    }
    time.Sleep(time.Second)
}

Этот код выводит следующий результат:

s: foo
x: foo
s: bar
x: bar

s: bar
x: foo
s: bar
x: bar

Предполагая, что это не какая-то нечетная ошибка компилятора, мне любопытно, почему: а) значение s интерпретируется по-разному в версии goroutine, а затем в обычном вызове func и b) и почему назначение его локальной переменной внутри цикла работает в оба случая.


3


источник


Ответы:


Закрытие в Go лексически ограничено. Это означает, что любые переменные, упомянутые в закрытии из «внешнего» объема, не являются копией, а фактически являются ссылкой. for цикл фактически повторно использует одну и ту же переменную несколько раз, поэтому вы вводите условие гонки между чтением / записью s переменная.

Но x выделяет новую переменную (с :=) и копирование s, что приводит к тому, что это правильный результат каждый раз.

В общем, лучше всего передать любые аргументы, которые вы хотите, чтобы у вас не было ссылок. Пример:

for _, s := range []string{"foo", "bar"} {
    x := s
    go func(s string) {
        fmt.Printf("s: %s\n", s)
        fmt.Printf("x: %s\n", x)
    }(s)
}

10



Совет: Вы можете использовать «get address operator» &  для подтверждения того, являются ли переменные одинаковыми ,

Давайте немного изменим вашу программу, чтобы помочь нашему пониманию.

package main

import "fmt"
import "time"

func main() {
    for _, s := range []string{"foo", "bar"} {
        x := s
        fmt.Println("  &s =", &s, "\t&x =", &x)
        func() {
            fmt.Println("-", "&s =", &s, "\t&x =", &x)
            fmt.Println("s =", s, ", x =", x)
        }()
    }

    fmt.Println("\n\n")

    for _, s := range []string{"foo", "bar"} {
        x := s
        fmt.Println("  &s =", &s, "\t&x =", &x)
        go func() {
            fmt.Println("-", "&s =", &s, "\t&x =", &x)
            fmt.Println("s =", s, ", x =", x)
        }()
    }
    time.Sleep(time.Second)
}

Выход:

  &s = 0x1040a120   &x = 0x1040a128
- &s = 0x1040a120   &x = 0x1040a128
s = foo , x = foo
  &s = 0x1040a120   &x = 0x1040a180
- &s = 0x1040a120   &x = 0x1040a180
s = bar , x = bar



  &s = 0x1040a1d8   &x = 0x1040a1e0
  &s = 0x1040a1d8   &x = 0x1040a1f8
- &s = 0x1040a1d8   &x = 0x1040a1e0
s = bar , x = foo
- &s = 0x1040a1d8   &x = 0x1040a1f8
s = bar , x = bar

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

  • Переменная s в каждой итерации цикла есть одна и та же переменная.
  • Локальная переменная x в каждой итерации цикла есть разные переменные, у них просто одноименное имя x
  • В первом цикле for func () {} () часть была выполнена на каждой итерации, и цикл продолжал свою следующую итерацию после func () {} () завершено.
  • Во втором цикле (версия goroutine), go func () {} () само заявление завершено мгновенно. Когда утверждения в func-теле, которые были выполнены, определяются планировщиком Go. Но когда они (инструкции в корпусе func) начинают выполняться, цикл for уже завершен! И переменная s является последним элементом в срезе, который является bar, Вот почему мы получили два «бара» на втором выходе цикла.

2