Уже несколько лет я знаком с наблюдением, что качество программистов является убывающей функцией от плотности операторов go to в программах, которые они создают. Не так давно я обнаружил, почему использование оператора go to имеет столь пагубные последствия, и пришел к убеждению, что оператор go to должен быть упразднен во всех языках программирования «высокого уровня» (то есть везде, кроме, пожалуй, прямого машинного кода). В то время я не придавал этому открытию большого значения; теперь же я представляю свои соображения для публикации, поскольку в ходе самых последних дискуссий, где эта тема всплывала, меня к этому настоятельно призывали.

Прежде всего отмечу, что, хотя работа программиста завершается построением правильной программы, истинным предметом его деятельности является процесс, протекающий под управлением его программы, ведь именно этот процесс призван достичь желаемого результата; именно этот процесс своим динамическим поведением должен удовлетворять заданным спецификациям. Однако, как только программа готова, «создание» соответствующего процесса делегируется машине.

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

Давайте теперь рассмотрим, как можно охарактеризовать ход процесса. (Эту проблему можно рассматривать вполне конкретно: допустим, процесс, представляющий собой временну́ю последовательность действий, прерывается после некоторого произвольного действия; какие данные нам нужно зафиксировать, чтобы можно было воспроизвести процесс вплоть до той же самой точки?) Если текст программы представляет собой простую последовательность, скажем, операторов присваивания (которые в рамках данного обсуждения считаются описаниями отдельных действий), достаточно указать точку в тексте программы между двумя последовательными описаниями действий. (При отсутствии оператора go to я могу позволить себе синтаксическую неоднозначность в последних трех словах предыдущего предложения: если мы разбираем их как «последовательные (описания действий)», то речь идет о последовательности в пространстве текста; если же как «(описания) последовательных действий» — то о последовательности во времени.) Давайте назовем такие указатели на подходящее место в тексте «текстовым индексом».

Мы можем добавлять условные конструкции (if B then A), альтернативные (if B then A1 else A2), операторы выбора, предложенные Ч.Э.Р. Хоаром (сase[i] of(A1, A2, ... , An)), или условные выражения, введенные Д. Маккарти (B1E1, B2E2, , BnEn) — но ход процесса все так же определяется единственным текстовым индексом.

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

Давайте теперь рассмотрим операторы повторения (например, while B repeat A или repeat A until B). С логической точки зрения такие операторы излишни, поскольку повторение можно выразить с помощью рекурсивных процедур. Однако я не хочу исключать их из практических соображений: с одной стороны, операторы повторения вполне удобно реализуются на современном ограниченном оборудовании; с другой стороны, шаблон рассуждений, известный как «индукция», хорошо позволяет нам сохранять интеллектуальный контроль над процессами, порождаемыми операторами повторения. При введении операторов повторения текстовых индексов оказывается уже недостаточно для описания динамического хода процесса. Однако с каждым входом в оператор повторения можно связать так называемый «динамический индекс», неукоснительно отсчитывающий порядковый номер текущего повторения. Поскольку операторы повторения (как и вызовы процедур) могут быть вложенными, мы видим, что теперь ход процесса всегда можно однозначно охарактеризовать с помощью (смешанной) последовательности текстовых и/или динамических индексов. 

Суть в том, что значения этих индексов находятся вне контроля программиста; они создаются (либо текстом его программы, либо динамическим развитием процесса) независимо от его желания. Они задают независимые координаты, в которых можно описать ход процесса.

Зачем же нам нужны такие независимые координаты? Причина — и это, по-видимому, свойство, присущее всем последовательным процессам, — в том, что мы можем интерпретировать значение переменной лишь в контексте хода процесса. Если мы хотим подсчитать количество людей в изначально пустой комнате — назовем его n — этого можно добиться, увеличивая n на единицу всякий раз, когда мы видим, что кто-то входит в комнату. В тот промежуточный момент, когда мы уже увидели, как кто-то вошел в комнату, но еще не выполнили последующее увеличение n, его значение равно количеству людей в комнате минус один! 

Бесконтрольное использование оператора go to имеет своим непосредственным следствием то, что становится чрезвычайно трудно найти осмысленный набор координат для описания хода процесса. Обычно в расчет также принимают значения некоторых тщательно выбранных переменных, но это неприемлемо, поскольку смысл этих значений может быть понят лишь относительно хода процесса. С помощью оператора go to, разумеется, все еще можно однозначно описать ход процесса, добавив счетчик, считающий количество действий, выполненных с момента запуска программы (своего рода нормализованные часы). Проблема в том, что такая координата, хоть и уникальна, совершенно бесполезна. В такой системе координат становится в высшей степени сложной задачей определить все те точки выполнения, где, скажем, n равно количеству людей в комнате минус один!

Оператор go to в его нынешнем виде попросту слишком примитивен; он практически провоцирует программиста устроить в своей программе бардак. Рассмотренные операторы можно рассматривать как средство обуздания его использования. Я не утверждаю, что упомянутые операторы исчерпывают все потребности в том смысле, что удовлетворят любые нужды, но какие бы операторы ни предлагались (например, операторы аварийного завершения), они должны отвечать требованию: должна существовать возможность поддерживать не зависящую от программиста систему координат, описывающую процесс полезным и удобным для управления образом.

Трудно завершить эту статью справедливыми благодарностями. Суждено ли мне определить, кто повлиял на ход моих мыслей? Довольно очевидно, что я не избежал влияния Питера Лэндина и Кристофера Стрейчи. И, наконец, я хотел бы засвидетельствовать (поскольку помню это совершенно отчетливо), как Хайнц Земанек на совещании по подготовке Algol в начале 1959 года в Копенгагене вполне определенно высказал свои сомнения в том, следует ли рассматривать оператор go to как синтаксически равноправный оператору присваивания. В некоторой, пусть и небольшой, мере я виню себя за то, что не сделал тогда должных выводов из его замечания.

Мнение о нежелательности использования оператора go to далеко не ново. Я припоминаю, что читал явную рекомендацию ограничивать его применение аварийными выходами, но не смог найти источник; вероятно, это замечание принадлежит Ч.Э.Р. Хоару. В [1, Sec. 3.2.1.] Вирт и Хоар высказывают соображение в том же духе, обосновывая использование конструкции case: «Как и условная конструкция, она отражает динамическую структуру программы яснее, чем операторы go to и switch, а также избавляет от необходимости вводить в программу большое количество меток». 

В [2] Джузеппе Якопини, кажется, доказал (логическую) избыточность оператора go to. Однако упражнение по более или менее механическому преобразованию произвольной блок-схемы в схему без переходов рекомендовать не стоит. В этом случае нельзя ожидать, что получившаяся схема окажется прозрачнее исходной.

Источники

1. Wirth N., Hoare C.A.R. A contribution to the development of ALGOL. Comm. Acm 9 (June 1966), 413–432.

2. Böhm C., Jacopini G. Flow diagrams, Turing machines and languages with only two formation rules. Comm. ACM 9 (May 1966), 363–371.

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


  1. Mephi1984
    13.12.2025 06:05

    Забавный факт, но в c# и в c++ до сих пор в 2025 году есть оператор goto


    1. saipr
      13.12.2025 06:05

      Я часто ссылаюсь на книгу Эндрю Таненбаума «Operating Systems Design and Implementation» на ее первое издание и на то, как в ней представлен язык Си.

      В этом, по моему уникальном описании Си, отсутствует оператор goto.


      1. longtolik
        13.12.2025 06:05

        Главное, чтобы он поддерживался компилятором.


        1. saipr
          13.12.2025 06:05

          Да, поддерживается.


          1. longtolik
            13.12.2025 06:05

            Спасибо, те, с которыми я имел дело, все поддерживали, но, возможно, есть и такие, которые не поддерживают. Оператор перехода goto встречался мне в довольно профессиональных проектах.


            1. maisvendoo
              13.12.2025 06:05

              В исходном коде Qt 5 я насчитал 2007 случаев использования goto


    1. Kelbon
      13.12.2025 06:05

      проблемы?


    1. kenomimi
      13.12.2025 06:05

      Без него простая логика превращается в нечитаемую лапшу. Например, возврат из многократно вложеного цикла, при обходе N-мерного массива. Или очистка тучи полей после malloc(), если в какой-то момент что-то пошло не так - иначе у нас будет копипаст блока кода для очистки ресурсов N раз. В ядре linux очень хорошо видны правильные примеры применения goto...

      Боль - это когда его используют в высокоуровневом коде как jmp в ассемблере, заменяя нормальные управляющие конструкции языка.


    1. ewaq
      13.12.2025 06:05

      Он есть даже в языке go, который создан относительно недавно.


  1. NeoNN
    13.12.2025 06:05

    Goto прекрасно используется везде, где его использование оправдано. Во внутреннем коде дотнета его использования достаточно много.


  1. a2g
    13.12.2025 06:05

    Злоупотребление goto в языках высокого уровня это симптом плохого кода. Принципиальное его отрицание - сектантство vulgaris.


    1. helgifisher
      13.12.2025 06:05

      Хороший компилятор все распедаливает как надо. Не сцать.


    1. d3d11
      13.12.2025 06:05

      старательное избегание goto приводит к такому:

      while (true) {
      ...

      if (....) break;

      ....

      break;
      }

      // попадаем сюда


      1. martin_wanderer
        13.12.2025 06:05

        Так последний абзац статьи ровно об этом


  1. liutas4x4
    13.12.2025 06:05

    Дийкстра очень любил такие глобальные заявления. Потом кто-то (Уэзерелл?) ссылался на вот такое, ругая код отвалившегося (слава богу до старта) Апполона, что goto перерыгнул код аварийного останова.

    У него же (?) или у Кнута была доп. концепция сопоставимого масштаба, что ошибка цикла должна вызывать его бесконечное исполнение, во избежание прыжка в запрещенные области.

    Срач по этим темам был жуткий и длился лет 10.


  1. Apxont
    13.12.2025 06:05

    Готу в 25 году? Реали? Когда только начинал кодить 20 лет назад уже говорили что готу это плохо. Да и вообще его не убирают только из обратной совместимости, по моему, хотя надо бы уже давно.


    1. NeoNN
      13.12.2025 06:05

      Для кода бизнес-логики плохо, а для стейт машин (таких как в async await) и оптимизации циклов (и не только циклов), клинапа во внутреннем коде - хорошо.


    1. zVlad909
      13.12.2025 06:05

      В языке REXX, которому "сто" лет, Go To нет изначально. Правда есть оператор SIGNAL, который может иметь смысл Go To, но основное назначение SIGNAL не для этого, а для обработки исключительных ситуаций. Например SIGNAL SYNTAX <label>, или SIGNAL ERROR <label>.


  1. rukhi7
    13.12.2025 06:05

    Давайте теперь рассмотрим операторы повторения (например, while B repeat A или repeat A until B). С логической точки зрения такие операторы излишни, поскольку повторение можно выразить с помощью рекурсивных процедур.

    ужасно интересно посмотреть как эти очень сложные конструкции переписать через простую рекурсию.


    1. zVlad909
      13.12.2025 06:05

      Возможно это неудачный перевод. Надо смотреть оригинал.

      P.S. Может это процедура, которая может вызывать саму сяба при определенных условиях выполнения. Но это тогда есть скрытый "оператора повторения".


    1. cupraer
      13.12.2025 06:05

      Покажите любой код, я вам расскажу, как его переписать через рекурсию.


      1. rukhi7
        13.12.2025 06:05

        быстрое преобразование Фурье (БПФ), прям интересно!

        Но вообще то это тоже код:

        while B repeat A

        но рассказывать не надо, надо переписать через рекурсию, начинать надо с самого простого, с фундаментального.


        1. cupraer
          13.12.2025 06:05

          Какое слово вам лично оказалось непонятно в слове «код»?


          1. rukhi7
            13.12.2025 06:05

                for (i = 0; i < 3; i++) //цикл по 3м точкам в строке 
                {
                    if (col[x]) // проверяем флаг для текущего столбца
                    {
                        x += inc[level];
                        continue;
                    }
            
                    if (level == 27) // для последней строки инкрементируем счетчик решений
                    {
                        count++;
                        break;
                    }
            
                    col[x] = 1; // устанавливаем флаг
                    line(level + 1); // запускаем поиск для следующей строки
                    col[x] = 0; // сбрасываем флаг
                    x += inc[level]; //переходим к следующему элементу
                }

            вот здесь можете посмотреть в контексте, там есть и рекурсия, но без цикла как-то не обошлось. Можно заменить цикл на рекурсию тоже?


            1. cupraer
              13.12.2025 06:05

              На псевдоязыке, синтаксис си у меня в RAM стёрся. Функция возвращает кортеж (col, count).

              λ iter(n, &col, x, count, inc, level) {
                if (n >= 3) { return (col, count); }
                if (col[x]) { return iter(n - 1, col, count, x + inc[level], level); }
                if (level == 27) { return (col, ++count); }
              
                col[x] = 1;
                line(level + 1);
                col[x] = 0;
                x += inc[level];
                return iter(n - 1, col, count, inc, level);
              }


              1. rukhi7
                13.12.2025 06:05

                спасибо за ответ. Мне всегда интересен взгляд со стороны. Но мне привычнее классическая запись, тем более что на Си это просто не скомпилируется, так писать нельзя и это меня вполне устраивает. Хотя на Си всегда можно было сохранять, передавать, отложено вызывать функции и построить на этом сколь угодно сложную логику. Поэтому зачем изобретать свой синтаксис мне не особо понятно. Прежде чем изобретать что-то свое надо бы освоить то что люди давно придумали и применяют десятилетиями, мне кажется.


                1. cupraer
                  13.12.2025 06:05

                  Я не могу держать в памяти все 20 с лишним языков, на которых профессионально писал код.

                  Я ничего не изобретал — это псевдокод. Перевести его на С — вопрос пяти секунд, если помнить синтаксис. Я могу прямо сейчас его за те же пять секунд перевести на те языки, с которыми активно работаю в настоящее время: на эрланг, эликсир, раст, идрис, руби. Надо?


                  1. rukhi7
                    13.12.2025 06:05

                    на эти языки нет стандартов, поэтому код на них это всегда лотерея с пониманием. Поэтому смысла нет.


                  1. rukhi7
                    13.12.2025 06:05

                    потом я же в общем понял что вы написали, я просто не вижу смысла так писать. Вы считаете что в таком стиле код выглядит лучше - ваше право! Но вы же не занимаетесь анализом эффективности сгенерированного машинного кода, насколько я понимаю, так как пишите на интерпретируемых языках. У нас с вами разные области применения кода разрабатываемого. Наверно я не в курсе какой-то вашей специфики, также как вы не в курсе какой-то моей специфики работы.


                    1. cupraer
                      13.12.2025 06:05

                      Вы считаете что в таком стиле код выглядит лучше

                      Это еще почему? Я всего лишь сказал, что не существует кода, который невозможно реализовать на рекурсии. ПРо лучше/хуже — это ваши фантазии.

                      на эти языки нет стандартов

                      пишите на интерпретируемых языках

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

                      вы же не занимаетесь анализом эффективности сгенерированного машинного кода

                      Это диагноз по аватарке, или что?


                      1. rukhi7
                        13.12.2025 06:05

                        получается что я ошибся в своих предположениях. Извините.


  1. zVlad909
    13.12.2025 06:05

    Мы можем добавлять условные конструкции (if B then A), альтернативные (if B then A1 else A2),

    If ...Then ... Else ... тоже еще та засада когда в предложениях Then и/или Else снова появляются IF. Это бомба. Читать такие конструкции сложно. Кроме написавшего никто не сножит охватить всей логики таких вложений.

    Альтернативой явлется конструкция (аналог упоминавшегося в статье case):

    Select

    When <condition> <statement| block DO>

    When <condition> <statement| block DO>

    ....

    Otherwise <condition> <statement| block DO>

    End /* Select*/

    Давайте теперь рассмотрим операторы повторения (например, while B repeat A или repeat A until B). С логической точки зрения такие операторы излишни, поскольку повторение можно выразить с помощью рекурсивных процедур.

    Никогда не имел проблем с операторами "повторения" (цикла обычно говорят программисты). Но есть много разных форм этих опереторов в разных языках и порой не очень удачных. Например For в языке С. С оператором DO в REXX у меня не было никаикх проблем. Этот опреатор может быть с индексной переменной или без нее, с WHILE/UNTIL и даже FOREVER (уравновешенный оператором LEAVE в теле цикла). А еще там есть оператор ITERATE досрочно завершающий цикл.


  1. DungeonLords
    13.12.2025 06:05

    Goto необходим, вот знаменитая статья почему это так


    1. rukhi7
      13.12.2025 06:05

      для примера, вот это:

      int* foo(int bar)
      {
          int* return_value = NULL;
      
          if (!do_something(bar)) {
              goto error_didnt_sth;
          }
          if (!init_stuff(bar)) {
              goto error_bad_init;
          }
          if (!prepare_stuff(bar)) {
              goto error_bad_prep;
          }
          return_value = do_the_thing(bar);
      
      error_bad_prep:
          clean_stuff();
      error_bad_init:
          destroy_stuff();
      error_didnt_sth:
          undo_something();
      
          return return_value;
      }

      не лучше и не хуже чем вот это:

      int* foo(int bar)
      {
          int* return_value = NULL;
      
          if (do_something(bar)) {
            if (init_stuff(bar)) {
              if (prepare_stuff(bar)) {
                return_value = do_the_thing(bar);
      
              }
              clean_stuff();
            }
            destroy_stuff();
          }
          undo_something();
      
          return return_value;
      }

      но по объективным показателям, количество строк и/или количество знаков (особенно без учета white spaces), количество лексических единиц второй вариант, очевидно, маленько короче. Но субъективно для кого-то лучше воспринимается первый вариант, для кого-то второй. То есть это просто дело привычки и, наверно, индивидуальных особенностей восприятия разных видов форматирования текста, во основном.

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

      ЗЫ: ну еще в первом варианте есть одна, мало для кого очевидная, проблема, связанная с возможностью более безопасной модификации-расширения этого куска кода впоследствии. Но кто же в наше время об этом думает! Как-то исподволь все пришло к тому что за программистами должен подчищать компилятор всегда и везде.


      1. cupraer
        13.12.2025 06:05

        Вот еще короче.

        int* foo(int bar)
        {
            return do_something(bar)
              && (init_stuff(bar)
                && (repare_stuff(bar)
                    && do_the_thing(bar)
                    || (clean_stuff() && NULL))
                || (destroy_stuff() && NULL))
              || undo_something() && NULL
        }


  1. Flyingfolds
    13.12.2025 06:05

    Каждый пишет как он хочет.

    Не исспользую гото, но если у меня его отберут, то буду крайне недоволен. И буду рассматривать возможность не исспользовать этот язык


  1. freim_enigma
    13.12.2025 06:05

    Go to...

    Ну собственно можно обойтись и без него.

    Но когда пислал на ассемблере, то на ассемблере нет цикла, есть только переход.

    Т.е. все циклы исключительно на базе переходов и сделаны.

    Go to плох в отладке огромного полотна кода.


  1. Mizantrop777
    13.12.2025 06:05

    Использую goto в catch когда надо изменить какой то параметр и прогнать часть кода заново. Хз какие проблемы. .net 10 если что