4th November 2025

Goto Considered Obsolete

The popular essay “Goto Considered Harmful” by Dijkstra has long inspired an almost religious hatred of the construct, but this is no longer justified. The GOTO of which Dijkstra spoke was unrestrained, and so programmers would often use it to jump from the body of one function into the body of another. Such reckless use of control flow is clearly objectionable. Modern goto is much less powerful than that, and mainly causes issues in languages with manual memory management due to variables being initialised to unexpected values (this makes its omission from Java even more questionable as Java doesn't have manual memory management). But I should like to make a stronger thesis, that goto as a programming construct is made entirely obsolete by common PL techniques and has been so for many years. I will show how these techniques can likely already be used in C using common compiler extensions. Finally, I shall present a rough draft of how such a design may be integrated well into a future C-style language.

Functional Goto §

My realisation stems from the Scheme language, which is a Lisp and based the lambda calculus. The primary abstraction in Scheme is the function. Almost all of the language constructs stem from functions in one way or another. For instance, the variable binding construct is often said to be equivalent to a function in the following way.

 1  (let ((a 1)
 2        (b 2))
 3    (+ a b))
 4  => ((lambda (a b) (+ a b)) 1 2)

This gives rise to Scheme's primary looping construct, the named let. Here, we bind the lambda to a name through letrec (let recursive, this can also be turned into a lambda but doing so is more complicated) and then call that function with the supplied arguments.

  1  (let loop ((i 0)
  2             (sum 0))
  3    (if (> i 10)
  4        sum
  5        (loop (+ i 1) (+ sum i))))
  6  => (letrec (loop (lambda (i sum)
  7                     (if (> i 10)
  8                         sum
  9                         (loop (+ i 1) (+ sum i)))))
 10       (loop 0 0))

This works because Scheme has tail call optimisation (TCO) which causes any statement of the form return f(); to compile into something along the lines of goto f; instead of call f; return;. Doing it this way eliminates the stack overflow errors you'd otherwise get when a loop like this gets too deeply nested.

It's a fairly trivial to see how this can replace goto, since they are effectively doing the same thing under the hood. You just need to break each labelled block in the source code up into a function and replace gotos with calls to those functions. This is actually how most modern compilers represent gotos internally, by converting to something called static single assignment (SSA) form. Suffice to say that anything expressed with goto can be equally expressed in this way.

Replacing Goto in C §

So why do we still have goto then? I think the way of doing things with return statements is generally much cleaner and removes the initialisation risks associated with using gotos as each function is forced to be a self-contained block. The GNU C compiler even has an extension that allows you to write that exact code. With optimisations enabled, it compiles identically to the loop-based equivalent.

  1  #include <stdio.h>
  2  #include <stdlib.h>
  3  int main(int argc, char **argv) {
  4    int max = atoi(argv[1]);
  5    int loop(int i, int sum) {
  6      if (i < max)
  7        return loop(i + 1, sum + i);
  8      else
  9        return sum;
 10    }
 11    printf("%d\n", loop(0, 0));
 12    return 0;
 13  }

While this can look better for some of the more complicated for loops you might write, it is quite ugly for the simple kind like this. So, let's see what this looks like when I use it as a replacement for a somewhat typical use of goto—breaking out of a loop.

  1  // Locate some entries in an array.
  2  T *find_using_goto(T *array, size_t length) {
  3    T *result = NULL;
  4    buffer_t buffer = make_buffer();
  5  
  6    for (int i = 0; i < length; ++i) {
  7      if (should_finish(&array[i], &buffer))
  8        goto found;
  9      else if (check(&array[i], &buffer))
 10        push(&result, &array[i]);
 11    }
 12  
 13    printf("ERROR: didn't locate all requested items.\n");
 14    result = NULL;
 15    goto cleanup;
 16   found:
 17    validate_result(result);
 18   cleanup:
 19    free_buffer(buffer);
 20    return result;
 21  }
 22  // The same as above, with functions instead of gotos.
 23  T *find_functional(T *array, size_t length) {
 24    buffer_t buffer = make_buffer();
 25    T *cleanup(T *final) {
 26      free_buffer(buffer);
 27      return final;
 28    }
 29    T *found(T *result) {
 30      validate_result(result);
 31      return cleanup(result);
 32    }
 33    T *result = NULL;
 34    for (int i = 0; i < length; ++i) {
 35      if (should_finish(&array[i], &buffer))
 36        return found(result);
 37      else if (check(&array[i], &buffer))
 38        push(&result, &array[i]);
 39    }
 40    printf("ERROR: didn't locate all requested items.\n");
 41    return cleanup(NULL);
 42  }

The functional method has some readability advantages. The cleanup code can be placed next to the initialisation code, which I imagine helps to prevent bugs. There is no implicit fallthrough, so the order of the definitions of blocks doesn't affect how they execute, preventing potential errors similar to those you'll commonly see in switch statements. Fewer variables need to be shared between the blocks (only the stateful buffer, and not the result). Most importantly, I echo Dijkstra's original complaint about goto, that it can create spaghetti code. With the functional approach, each block must act as a function and so has a clear purpose and responsibility. This better encapsulates logical intent of labels as they are used in code, that is to delimit blocks of statements that perform a given function. As a final benefit, no one will be able to complain at you for using goto when you do it this way. You will instead get much more interesting and original complaints, possibly with the inclusion of epithets and general questions of sanity.

In Future Languages §

I implore the designers of future languages: ditch harmful goto and adopt nested functions instead. I think this proposition is really a strict positive if you explicitly include it in the design of the language. Though it would benefit some additional rules and features to improve usability and ensure good compilation.

In the matter of usage, there would be some rules needed to turn these nested functions into things that work in a lower level language. You'll note from previous examples that they can form implicit closures and capture variables in the calling context. The capturing property of nested functions is also transitive—any nested function which calls a nested function must also be considered to capture whichever variables it does. This will require the language to forbid creating a function pointer to any capturing function. Non-capturing functions are just ordinary functions and so can be exempt from any special restriction.

Another important note regards usage of the stack. While it is possible to write head-recursive capturing functions, they will take up space on the stack, and special instructions will need to be emitted by the compiler that allow such functions to peer up the stack and gain access to their captured variables. This is generally undesirable for both the compiler writer who doesn't want to complicate their program, and the programmer who likely doesn't want nested functions that may cause overflows. So it would make sense to allow a call to a capturing function only in the tail position. This way it is truly guaranteed to compile to an efficient goto instruction. Though this limitation can be relaxed if a capturing function is only called once, as the functions body can be directly inserted into the call site in that case.

Finally, there is the matter of ergonomics. I believe the ergonomic issues of these functions in C-style languages can be alleviated with two small changes. First of all, allow them to be defined out of order and used without being pre-declared. Secondly, a new syntax for expressions using the keyword run that allows a nested function to be defined and called simultaneously, similar to how let worked in Scheme. Here's a short example showcasing the new feature.

  1  #include <stdio.h>
  2  #include <stdlib.h>
  3  int main(int argc, char **argv) {
  4    int max = atoi(argv[1]);
  5    printf("%d\n", run int loop(int i = 0, int sum = 0) {
  6      if (i < max)
  7        return loop(i + 1, sum + i);
  8      else
  9        return sum;
 10    });
 11    return 0;
 12  }

I find it particularly pleasing how the loop can be placed into the arguments of printf as an expression. This run syntax could even be extended to take an arbitrary statement following it, and introduce an implicit function when it does so. Combine that with a more advanced version of return, and you can write some really elegant initialisation expressions.

  1  conversion_result convert_cat(cat src) {
  2    // Here f is explicitly named
  3    dog dst = run dog f(void) { switch (src) { ... } };
  4    // whereas here an unnamed function is implicitly introduced.
  5    dog dst = run switch (src) {
  6      case TABBY: return TERRIER;
  7      case MANX: return COLLIE;
  8      default:
  9        printf("Couldn't convert cat to dog.\n");
 10        return CONVERSION_FAIL from convert_cat;
 11    };
 12    run_conversion(src, dst);
 13    printf("Converted %s to %s.\n", cat_name(src), dog_name(dst));
 14    return CONVERSION_SUCCESS;
 15  }

I shall stop myself there before I am suckered, once again, into redesigning the entire C programming language.

Anyway, I hope I have here convinced you that there is really no need for new programming languages to contain a goto construct. I believe this alternative is both more expressive than goto in the contexts for which goto is used, and also more widely applicable to other areas.