Если раньше в циклах были проблемы с замыканиями, так как переменная цикла имела скоуп всего цикла, а не одной его итерации, то в 1.22 это поведение поменяют.


проще показать на примере:


 funcs := []func(){}

 for i := 0; i < 5; i++ {
  funcs = append(funcs, func() {
   fmt.Println(i)
  })
 }

 funcs[0]()

Последняя строка примера напечатает 5 в go 1.21, но в go 1.22 будет уже интуитивно понятный 0.


С одной стороны, это нарушение обратной совместимости, но зато не надо писать пугающее новичков i := i для починки скоупа.


На самом деле, сложно представить кейс, чтобы кто-то хотел во все функции замкнуть именно последнее значение цикла. В тоже время такая неинтуитивная ситуация, как сейчас, регулярно выстреливает в ногу, вот пример реального бага в Lets Encrypt:


// authz2ModelMapToPB converts a mapping of domain name to authz2Models into a
// protobuf authorizations map
func authz2ModelMapToPB(m map[string]authz2Model) (*sapb.Authorizations, error) {
    resp := &sapb.Authorizations{}
    for k, v := range m {
        // Make a copy of k because it will be reassigned with each loop.
        kCopy := k
        authzPB, err := modelToAuthzPB(&v)
        if err != nil {
            return nil, err
        }
        resp.Authz = append(resp.Authz, &sapb.Authorizations_MapElement{
            Domain: &kCopy,
            Authz: authzPB,
        })
    }
    return resp, nil
}

Здесь разработчик скопировал переменную k, а вот v — уже забыл. В итоге функция modelToAuthzPB получила указатели на одну и ту же переменную.


Новое поведение языка Go можно включить уже в 1.21 с помощью переменной окружения GOEXPERIMENT=loopvar и протестировать вашу программу. В любом случае, переход с 1.21 на 1.22 надо будет делать осторожно, возможно у вас что-то сломается. А может, наоборот, заработает (смайлик).


Если хотите больше новостей и полезной информации о разработке, подписывайтесь на мой tg-канал Cross Join

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


  1. Conacry
    20.09.2023 09:46
    +3

    Интересно почему изначально была выбрана видимость per-loop, а не per-iteration? Для экономии памяти?


    1. varanio Автор
      20.09.2023 09:46
      +3

      наверно, просто не продумали этот момент, сделали как проще


      1. 1755
        20.09.2023 09:46
        +1

        Вы прям идеально описали мои мысли, которые то тут то там возникали при освоении Go. Простой как валенок, в этом его прелесть, но к некоторым моментам было ощущение как вы описали.


        1. zahnah
          20.09.2023 09:46
          -1

          Ну, не думаю, что на этом ресурсе найдётся кто-то, кто сможет их осудить. Все мы своего рода мэйнтэйнеры Go.


    1. red75prim
      20.09.2023 09:46
      +1

      Традиция. Во многих языках есть (или было) такое же поведение: питон, ява, C#, javascript. По-отдельности реализация циклов и лямбд выглядит естественной, но их взаимодействие даёт неочевидное последствие.


      1. ris58h
        20.09.2023 09:46
        +8

        ява

        Будьте любезны, пример. Что-то придумать не могу, учитывая что в замыкание в Java можно захватить только final или effectively final переменную.


        1. red75prim
          20.09.2023 09:46

          Да, яву я зря добавил. Там компилятор заставляет копировать переменную цикла, чтобы её захватить в замыкание.


        1. vic_1
          20.09.2023 09:46
          -3

          Именно так, а go в мусорное ведро


      1. rmrfchik
        20.09.2023 09:46
        +15

        В java не так (там нельзя передавать не effectively final). В javascript не так (всё нормально захватывается). В python свободная переменная передаётся по имени, это вообще что-то из 60-х и алгола, но это понятное поведение.

        c# проверить не могу (и не хочу), но думаю, там или как java или как javascript.

        Дурость с for я видел только в Go. К сожалению, язык миновал стадию проектирования и сразу ушёл в продакшен.


        1. red75prim
          20.09.2023 09:46

          В javascript не так (всё нормально захватывается).

          Вот прямо сейчас запустил в браузере:

          var funcs = [];
          for (var i = 0; i < 3; i++) {
            funcs[i] = function() {
              console.log("My value:", i);
            };
          }
          for (var j = 0; j < 3; j++) {
            funcs[j]();
          }
          

          Результат:

          My value: 3
          My value: 3
          My value: 3
          

          c# проверить не могу (и не хочу), но думаю, там или как java или как javascript.

          Было как в javascript, потом пофиксили (нет, не так как сделано в java, где запрещено захватывать переменную цикла). Тут в комментариях уже про это писали.


          1. Format-X22
            20.09.2023 09:46
            +5

            Вы в курсе что с 2014 года уже не используется var? Как минимум в хроме, остальные подтянулись к 2016. То есть минимум 7 лет как проблемы нет.


            1. KivApple
              20.09.2023 09:46

              С let другое поведение?


              1. ris58h
                20.09.2023 09:46
                +11

                Да. Можно сказать, что он для этого и создавался - чтобы у переменной была понятная и ожидаемая (с точки зрения обычного человека) область видимости.


            1. red75prim
              20.09.2023 09:46
              +1

              Вы в курсе что с 2014 года уже не используется var

              Цитирую себя: "Во многих языках есть (или было) такое же поведение"

              В js, оказывается, и есть и было. Хорошо, буду знать, спасибо.


            1. vanxant
              20.09.2023 09:46
              +1

              Просто добавлю, что let был в фаерфоксе со времён первой браузерной. Но сначала его принципиально не хотел понимать ИЕ, потом хром. Потом разработчики хрома таки что-то поняли.


              1. mvv-rus
                20.09.2023 09:46

                Ну, разработчики IE тоже поняли, только поздновато — в 11-й версии ;-) И то не во всех режимах.
                Была у меня с IE такая вот хохма во дни минувшие. Делаю модуль расширения для ADFS на Win2012 R2. Ему там положено возвращать фрагменты HTML, которые ADFS вставляет в свой шаблон и возвращает получившуюся страницу пользователю. Проверяю работу в IE11- let в скриптах в фрагментах не работает. А те же самые фрагменты, вставленные в статический файл HTML — копию возвращаемой страницы — работают на ура. Сперва поофигевал, потом разобрался: AD FS передавал в IE заголовок, включающий режим совместимости с IE10 — а в том режиме let предусмотрен не был.


            1. Shagrat2
              20.09.2023 09:46

              Хорошо когда клиенты обновляют свое ПО и компьютеры.
              У меня 20% клиентов использует IE


        1. unreal_undead2
          20.09.2023 09:46

          В С++ никаких новых переменных на каждой итерации не создаётся. Но там захватывать переменную цикла по ссылке - уже взвод курка для выстрела в ногу.


          1. bogolt
            20.09.2023 09:46
            +4

            В плюсах такой проблемы нет, из-за другой модели работы с памятью. Если мы захватим переменную по указателю то по завершении цикла мы получим dangling pointer. В го же из-за автоматического управления памятью так сделать можно без того чтобы получить ошибку доступа к памяти.


            1. unreal_undead2
              20.09.2023 09:46

              Теоретически можно захватить счётчик по ссылке на одной итерации и использовать полученную лямбду на другой - это и в С++ валидно (и как раз такой код будет ломаться от описанного изменения в Go). Другое дело, что большого практического смысла нет.


        1. omaxx
          20.09.2023 09:46

          в питоне такое же поведение:

          In [1]: func = [lambda: i for i in range(5)]
          
          In [2]: func[0]()
          Out[2]: 4


          1. vadimr
            20.09.2023 09:46
            +2

            В питоне-то понятно, почему: тело функции (в данном случае лямбды) не интерпретируется до момента её вызова. Там любой лексически верный мусор можно написать между : и for.

            >>> func = [lambda: j for i in range(5)]
            >>> j = 'Hello'
            >>> func[0]()
            'Hello'


            1. omaxx
              20.09.2023 09:46

              А что вы хотели показать этим примером? Это вполне ожидаемое поведение, если нет локальной переменной с таким именем, то питон будет искать среди глобальных переменных (вернее Enclosing, Global, Built-in). Ну и при этом на момент объявления функции эта переменная существовать не обязана.

              In [1]: def f():
                 ...:     print(j)
                 ...:
              In [2]: j = "Hello"
              In [3]: f()
              Hello


              1. vadimr
                20.09.2023 09:46

                Ну да, так всё и есть.

                Я хотел показать именно то, что странно было бы ожидать, что значение i будет меняться в элементах списка func на каждой итерации, если оно фактически востребуется только один раз в момент вызова функции в последней строке.

                Для такого поведения, как в новом Go, в питоне нужно было б заводить отдельный контекст для каждой итерации цикла, что обессмыслило бы само понятие перементой цикла.


                1. omaxx
                  20.09.2023 09:46
                  +1

                  можно вот так сделать:

                  In [1]: [f() for f in map(lambda i: lambda: i, range(5))]
                  Out[1]: [0, 1, 2, 3, 4]


        1. sergeaunt
          20.09.2023 09:46
          +16

          К сожалению, язык миновал стадию проектирования и сразу ушёл в продакшен.

          Это лучшая фраза, которую я видел на Хабре за последний год.


        1. kasthack_phoenix
          20.09.2023 09:46
          +2

          c# проверить не могу (и не хочу), но думаю, там или как java или как javascript.

          Там loop-level в for, а вот в foreach зависит от версии языка: изначально было loop-level, но в C# 5 поменяли на iteration-level.


        1. ivan_mariychuk
          20.09.2023 09:46
          +1

          c# проверить не могу (и не хочу)

          Об этом было очень важно упомянуть.


          1. rmrfchik
            20.09.2023 09:46

            Посчитал, что важно. Чтобы отмести предложения сделать это разными способами.


            1. ivan_mariychuk
              20.09.2023 09:46

              Понял, извините.


      1. outcatcher
        20.09.2023 09:46

        В Python у циклов (как и у if) вообще нет своей области видимости, можно переменную вообще первый раз внутри цикла присвоить, и снаружи потом использовать. Если не присвоишь - просто будет NameError

        >>> for i in range(10):
        ...   a = 2
        ... 
        >>> print(i)
        9
        >>> print(a)
        2
        >>> print(c)
        Traceback (most recent call last):
          File "<stdin>", line 1, in <module>
        NameError: name 'c' is not defined
        


    1. arheops
      20.09.2023 09:46
      +3

      Вы еще спросите, почему в JS видимость this везде разная.
      Просто провтыкали, как обычно.


  1. nightlord189
    20.09.2023 09:46
    +31

    Прощай старый добрый способ докопаться на собесах)


    1. zergon321
      20.09.2023 09:46
      +3

      Я вот не понимаю, почему на собеседованиях спрашивают подобный кринж. Вот джаваскриптеров вообще любят спросить о том, что выведет лютая мешанина из разных скобок


      1. Lexicon
        20.09.2023 09:46
        +14

        На собесах хорошо отлавливает джунов

        Если человек помнит или хотя бы подзабыл, перед вами джун, если прокричал что-то на орочьем, достал топор и сближается, значит про опыт не врет


      1. mapcuk
        20.09.2023 09:46
        +1

        По-моему такой вопрос реально отражает опыт, я сам с этим поведением for ... range cтолкнулся.
        Меня больше смущают вопросы про поведение, если писать (или читать) в закрытый канал (буфферизованный), если так приходится делать - это уже плохой код. Зачем даже задуматься о том, какое будет поведение?


  1. nepriyatelev
    20.09.2023 09:46
    +7

    Минус один вопрос на собесах и в тестах! ха-ха(


    1. czz
      20.09.2023 09:46
      +11

      Зато +1 вопрос — breaking changes между go 1.21 и 1.22 :)


  1. impwx
    20.09.2023 09:46
    +7

    В С# сделали то же самое в 2012 году - полет нормальный, никто не жалуется.


    1. slepmog
      20.09.2023 09:46
      +2

      Они починили только foreach. for оставили.


  1. a_che
    20.09.2023 09:46

    на самом деле мне интересно, как это будет сделано.

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

    а будет как? на каждую итерацию будет новая переменная?

    звучит очень дорого, а если в цикле миллион итераций?


    1. KivApple
      20.09.2023 09:46
      +4

      Звучит бесплатно, если речь о примитивах.

      Ссылка на примитив как и любая другая ссылка весит 32 или 64 бита в зависимости от разрядности. Значение int весит 32 бита. В замыкание неизбежно что-то копировать да придётся - либо ссылку, либо значение. Копировать значение примитива стоит не дороже ссылки, а иногда дешевле (если у нас 64 битная ОС, а примитив 32 бита). Также чтение примитива по значению точно быстрее, чем по ссылке, потому что примитив читается за одну операцию, а примитив за ссылкой за две (сначала прочитать ссылку, потом значение по адресу из неё). Наконец, оптимизатор, зная что примитив никто за пределами лямбды не изменит, может лучше оптимизировать код.

      В языках типа C++, где ссылки создаются более явно, давно есть правило, что примитивы по ссылке передают только если очень надо. По значению эффективнее. По ссылкам хорошие программисты передают объекты, которые занимают в памяти больше размера 2-3 указателей (никакие примитивы столько не весят), либо если нужны особые свойства ссылок (возможность менять из нескольких мест и т. д.)


      1. unreal_undead2
        20.09.2023 09:46

        Насколько понимаю, семантически захват в Go всегда по ссылке и просто скопировать значение в общем случае не получится. Скажем, два захвата на одной итерации должны получить одну общую переменную.


        1. KivApple
          20.09.2023 09:46

          Новость об изменении этого поведения как раз. А мой комментарий о том, что для примитивов это повысит эффективность, а не понизит. Если примитивы в Go как в Java (выделяются не в куче и не имеют таблицы виртуальных методов), а не как в Python.


          1. unreal_undead2
            20.09.2023 09:46

            Новость об изменении этого поведения как раз

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


      1. WASD1
        20.09.2023 09:46

        да не копируем итератор итератор в отдельную переменную на каждой итерации (и замыкаемся по ссылке на копию).
        Если копией не воспользовались - всё отлично DCE её легко удалит.


    1. ris58h
      20.09.2023 09:46
      +4

      Думаю, что если нет захвата переменной в замыкание, то можно и не создавать лишнего.


    1. WASD1
      20.09.2023 09:46

      А как сейчас в Го продляется жизнь замкнутых переменных со стека?
      Через двойной указатель и копирование в кучу при выходе из скоупа?

      Ну и теоретически можно предусмотреть отдельную машинерию только если замыкание захватывает переменную цикла.

      Upd: если в го есть понятие "объект расположенный на стеке" (с конструкторами \ деструкторами которые компилятор умеет элиминировать если они пустые) - то даже специальный случай вроде не потребуется.


      1. neolink
        20.09.2023 09:46

        Она не продляется, для этого есть escape analysis - если он говорит что значение переживает функцию оно сразу аллоцируется в куче


        1. WASD1
          20.09.2023 09:46

          Спасибо.

          Тогда моё update наверное не верно. Если у примитивных типов нет технического деструктора (вызываемого language-runtime, в абсолютном большинстве случаев не вызываемого), не на что навесить нужную логику (вводить его сейчас, понятно, поздновато).


          И значит скорее всего надо отдельно делать циклы без замыканий (ничего не менять) \ отдельно циклы где замыкания захватывают переменную цикла.


    1. WASD1
      20.09.2023 09:46

      Копируете переменную цикла. Замыкаетесь по копии переменной. Удаляете ненужные скопированные переменные (на стеке DCE справится, а вот если сделали выделение памяти - надо уже ручками удалять).

      ПС
      Желательно эти 3 фазы поставить подряд.


  1. gandjustas
    20.09.2023 09:46
    +2

    Если анализ гитхаба и другого опенсорса говорит, что ничего не сломается от такого изменения, то ничего страшного.


  1. buldo
    20.09.2023 09:46
    +5

    Ломающее изменение и подъем только минорной версии языка? Что-то я не понимаю в semver


  1. Daddy_Cool
    20.09.2023 09:46
    +3

    "Если раньше в циклах были проблемы с замыканиями, так как переменная цикла имела скоуп".
    ...
    Им это слово много говорило. Жаргон это конечно хорошо, но все же...
    Даже ПЕРВЫЙ ЖЕ коммент использут нормальное слово "видимость".


    1. akurilov
      20.09.2023 09:46

      Тут в соседней статье пишут "холодный аутрич"


    1. varanio Автор
      20.09.2023 09:46
      +1

      скоуп уже давно общепринятый термин. Ну и кстати...

      жаргон - это французское слово
      коммент - это английское слово


      1. Daddy_Cool
        20.09.2023 09:46

        Троллинг засчитан. ;)
        "скоуп уже давно общепринятый термин". Сорри (анлийское слово), но нет.


    1. gravyzzap
      20.09.2023 09:46

      Scope часто переводят как область видимости. Но ключевое слово — область. "Видимость" в первом комментарии в контексте статьи не вызывает вопросов, но в другом контексте может вызывать.

      Если бы можно было всем договорится о "правильном" переводе, я бы выбрал "ареал".


  1. igorekudashev
    20.09.2023 09:46
    +4

    какой же у го отвратительный синтакс


  1. lanseg
    20.09.2023 09:46

    О, я с таким сталкивался, совершенно не ожидая такого поведения. Подумал "фу" и обернул тело цикла в вызов функции, совсем как в js когда-то