The Go-To Statement is Good, Actually

Introduction

The go-to statement in languages like C and C++ (i.e. goto) is actually a Good Thing, and has legitimate application to good program design. Modern programmers' compulsive dislike of it is based on obsolete concerns taken out of context, and is propagated in the modern era by sloppy, cargo-cult programming.

Decades ago, before the rise of modern programming practices, then-modern languages were just adopting a thing called a 'function'. Curiously enough, no one seemed to like them—preferring instead to use hundreds of go-to statements to jump around one or two of them. To counter this, Edsger Dijkstra, a prominent computer scientist, wrote an infamous essay entitled "Go To Considered Harmful"[1][2], wherein he advocated against the go-to statement as a propagator of poor programming practice.

Dijkstra was referring to the indiscriminate use of go-to as a substitute for function calls and actual structured programming in higher-level languages[3]. That problem is no longer relevant today, yet programmers still parrot it mindlessly, outside of this context. If that's you, you need to please stop.


Superior Code

Often, the objectively best program design uses a go-to or two.


Named Break

A common and easy-to-understand application is the problem of breaking out of nested loops. In languages without named break (which, by the way, is nothing but go-to), breaking out of multiple levels of nested loops requires at least one extra variable and a conditional. This is a completely unnecessary expense, hurting space, instruction locality, and instruction counts. Most importantly, it is confusing, especially if you have to do it in more than one place. However, a simple go-to statement solves the problem cleanly. The following is a typical example:

1
2
3
4
5
6
7
8
9
10
11
//Without `goto`
bool continuing = true;
for ( int j=0; j<4096&&continuing; ++j )
for ( int i=0; i<4096            ; ++i )
{
	if ( !process(i,j) )
	{
		continuing = false; //prepare break out of outer loop
		break;              //break out of inner loop
	}
}
1
2
3
4
5
6
7
//With `goto`
for ( int j=0; j<4096; ++j )
for ( int i=0; i<4096; ++i )
{
	if ( !process(i,j) ) goto END;
}
END:;

In the first code block, continuing is checked each time through the outer loop. We also have to store it, which is a register. (If you think the compiler can elide these very real costs, you're wrong because it can't—even in this simple example[4].)

More importantly, without go-to, the end result is messy. Look how two different mechanisms (the break and the variable) are needed to break out of the nested loop! We could get rid of the boolean variable by setting j to its upper bound or returning from a newly created function, but these are even less clear (and have their own performance costs). Whereas, in the second example, it is immediately obvious what's happening. The go-to saves lines and compiled instructions, and this advantage only becomes clearer the more nestings there are.

Why would anyone ever forgo a go-to here? It is the cleanest and fastest solution!


Loop Optimization

A second, and somewhat related, application is a pattern I see in loops frequently. At each loop step, there is an expensive calculation needed to continue to the next iteration. If the loop only runs a few times, the superfluous final calculation, which is pointless on the last iteration, can be significant overhead.

1
2
3
4
5
6
7
8
//Without `goto`
int i = 0;
do
{
	iteration_operation(i);
	prepare_for_next_iteration(); //expensive
}
while ( ++i < n ); //`n` is fairly small
1
2
3
4
5
6
7
8
9
//With `goto`
int i = 0;
LOOP:
	iteration_operation(i);
	if ( ++i < n ) //`n` is fairly small
	{
		prepare_for_next_iteration(); //expensive
		goto LOOP;
	}

In the second version, we completely avoided that expensive prepare_for_next_iteration() call that wasn't necessary the last time through the loop!—(and no, you again cannot rely on the compiler to make this transformation for you; it usually cannot prove a given prepare_for_next_iteration() has no significant side-effects.)

This sort of thing occurs frequently in text processing. I use go-to-esque "loops" like the above for example in my ".obj" file loader in my graphics library. Little things like this add up: my loader runs 50% faster than any other loader I've used, and I haven't done much in the way of optimization—just by allowing these control flows, the same algorithm is made more efficient!


Error Handling

A third application of go-to is code organization within a function. This most often comes up in error handling. For example, suppose you have some error conditions, and various points where they could occur in a function, but you still need to do cleanup in case of an error. If we didn't have go-to, we'd need to duplicate this cleanup code at each point. With go-to, the error code can not only be specified once, but it can be moved to the bottom of the function where it clearly is distinct from the algorithm proper.

A few recent C best-practices guides in fact actually explicitly recommend this, citing specific examples from, among others, the Linux kernel. Python uses it all over. The following common pattern is inspired from such examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//Without `goto`
int func(void)
{
	char* buffer = (char*)calloc( 64, sizeof(char) );
	if ( buffer == NULL ) return -1;

	buffer[0] = 'a';
	if ( !process1(buffer) )
	{
		printf( "Final simulation buffer: \"%s\"\n", buffer );
		free(buffer);
		return -1;
	}

	buffer[0] = 'b';
	if ( !process2(buffer) )
	{
		printf( "Final simulation buffer: \"%s\"\n", buffer );
		free(buffer);
		return -1;
	}

	buffer[0] = 'c';
	if ( !process3(buffer) )
	{
		printf( "Final simulation buffer: \"%s\"\n", buffer );
		free(buffer);
		return -1;
	}

	printf( "Final simulation buffer: \"%s\"\n", buffer );
	int result = final_processing(buffer);
	free(buffer);

	return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//With `goto`
int func(void)
{
	char* buffer = (char*)calloc( 64, sizeof(char) );
	if ( buffer == NULL ) return -1;

	int result = -1;

	buffer[0] = 'a';
	if ( !process1(buffer) ) goto CLEANUP;

	buffer[0] = 'b';
	if ( !process2(buffer) ) goto CLEANUP;

	buffer[0] = 'c';
	if ( !process3(buffer) ) goto CLEANUP;

	result = final_processing(buffer);

	CLEANUP:
	printf( "Final simulation buffer: \"%s\"\n", buffer );
	free(buffer);

	return result;
}

With go-to, the program is shorter and more understandable! Moreover, even when none of the process operations fail, since it compiles to fewer instructions, it is more instruction-cache friendly and runs faster anyway.


Performance and Optimization

The go-to statement can make programs faster. As mentioned in the above examples, the code containing go-to statements is not just clearer, but simpler computationally as well, resulting in higher performance. Moreover, current compilers seem unable to rearrange alternative code to anything close, meaning the go-to statement is the only way to achieve the highest-performing code. That is, the go-to statement allows optimizations that are otherwise completely inexpressible using standard control flow.

In my experience, embracing the go-to statement often improves program performance. When go-to is mixed with pointer arithmetic and the under-appreciated restrict keyword[6], one can produce truly marvelous works of engineering literally 10 times faster than their go-to-less counterparts.

While the main reason to use go-to statements is for the superior clarity they can afford, the ability to capture such dramatic performance wins at the same time makes its enthusiastic use all the more obvious.

Various people say go-to prevents compiler optimization. In fact, the opposite is true. If you actually look at disassembly, as I regularly do, you will see that go-to statements usually make things better, not worse—and this is borne out objectively in profiling. While go-to can make compilers harder to implement, optimization clearly still works, and in my experience, using go-to intelligently is almost always a performance win in practice.

In any case, the go-to statement is one major tool for programmer optimization. Even if the compiler can't optimize something, that may be fine since the programmer, using go-to, already did. Compilers are very good at doing optimization for you already, but outdoing the compiler much of the time is something basically anyone can do with just a bit of practice—and go-to is a powerful tool for doing that. I strongly encourage any naysayer to compile the examples themself.


Conclusion

Dijkstra was right in some sense. The go-to statement is not a substitute for function calls, and it obviously shouldn't be.

However, the plain fact is that the go-to statement is just another language feature. The fact that in the 1960s programmers used go-to instead of functions does not make go-to bad[7]—it's more a reflection of changing practice and the tools available at the time. As demonstrated above, the go-to statement has genuine application to good, modern program design, and any blanket suggestion that it should be removed from high-level languages is ignorantly prescriptivist.

Edsger Dijkstra was attempting to force structured programming into a world that didn't want it, and his suggestion to remove go-to from all higher-level languages may have been meant literally. But Dijkstra could not have anticipated the incredible advances in languages and technology that would literally completely change how code is written. In any case, now that it is more than 50 years later, I find it astonishing that people still parrot it like an absolute truth instead of noting it for the ancient and mostly irrelevant historical tidbit that it now is.

Indeed, as described in the Jargon File, Dijkstra's essay was merely an early effort exploring the emerging field of structured programming, and was quickly obsoleted by more sophisticated analysis. In a later criticism, also in CACM[8], Rubin noted that:

"[T]he belief that GOTOs are harmful appears to have become a religious doctrine, unassailable by [all counter-]evidence" "[, and] has caused incalculable harm to the field of programming, which has lost an efficacious tool. It is like butchers banning knives because workers sometimes cut themselves. Programmers must devise elaborate workarounds, use extra flags, nest statements excessively, or use gratuitous subroutines. The result is that GOTO-less programs are harder and costlier to create, test, and modify. The cost to business has already been hundreds of millions of dollars in excess development and maintenance costs, plus the hidden cost of programs never developed due to insufficient resources."

Rubin supported this with a further example and an objective study on 13 expert programmers: the shortest solutions used go-to, required fewer calculations, and were written in the least amount of time. The solutions without go-to were all twice as long, and one was even incorrect!

The go-to statement often produces cleaner, faster, more expressive, better code. Don't reject it because of outdated dogma! The greats of Computer Science understood that blind adherence to any practice is a Bad Thing. Just because someone said something a half-century ago does not mean that all programmers everywhere should suddenly treat it unquestioningly and for all time as if it were divine law.


References


Return to the list of blog posts.