Software reasoning · 1968 · Edsger W. Dijkstra
Go To Statement Considered Harmful
Structure control flow so a human can map source position to meaningful execution state and make correctness tractable.
How to read the original
- Read the discussion of textual and dynamic indices as the central reasoning argument.
- Compare the treatment of procedures and repetition with the claim about unrestricted jumps.
- Notice the warning that mechanically removing jumps does not automatically make a program transparent.
Control structure is a reasoning aid
Dijkstra’s letter argues about the relationship between static program text and the changing process it describes. The archive uses the manuscript title A Case against the GO TO Statement and explains the published title. The argument is not that deleting one keyword proves correctness. It concerns whether progress can be described through manageable coordinates supplied by structured control, including procedure context and loop iterations.
Consider a program finding the first negative element of a finite list. Define the result to be its zero-based index, or the list length when no negative element exists. The original teaching pseudocode is simple:
Pseudocode:
i = 0
while i < length(values) and values[i] >= 0:
i = i + 1
return i
The condition uses left-to-right short-circuit evaluation. At the loop header, 0<=i<=n and every earlier element is nonnegative. That statement explains the meaning of i at one stable textual point. Each iteration advances the processed prefix by one. On exit, either i=n or the element at i is negative. In either case the returned value meets the contract.
Compare coordinates with arbitrary paths
Now imagine several labels that jump into the middle of the condition and increment sequence. At one label, i might refer to the next unexamined element; at another, it might refer to the element just found negative; at a third, it might already have been incremented. The same variable value needs additional path history to interpret it. A reviewer must recover that hidden history before checking array bounds or first-occurrence behavior.
An execution counter uniquely identifies a moment in one deterministic run, but it rarely supplies a useful invariant across all runs. “After step 137” does not say which prefix has been checked when inputs differ. A structured loop header expresses a relation between program position, data, and progress that generalizes across inputs.
Mechanical transformations can preserve confusion
Any tangled flow can be represented by a dispatcher loop with a state variable and a large switch. That removes explicit goto syntax, but the state transitions may preserve exactly the same reasoning burden. The useful refactoring discovers coherent operations and invariants, not merely a different spelling of jumps.
For the search example, extracting a predicate that reports whether an element is negative can clarify intent if it is pure and total on valid inputs. If the predicate mutates i or changes the list, the previous invariant may fail. Local control structure does not excuse hidden effects or an unstable input collection.
Apply the argument without turning it into a slogan
Exceptions, callbacks, asynchronous tasks, and early exits also create nonlocal control relationships. Some are essential and can be used clearly. The question is which state and obligations remain understandable at each boundary. A finally block that releases a resource may improve reasoning even though control can arrive there from several failures.
Tests should cover empty input, a negative first element, a negative last element, and no negative element. These cases expose likely mistakes, but the invariant explains all valid finite inputs. The proof also requires a decreasing remaining-length measure for termination and an assumption that length and contents do not change during the scan.
The practical standard is therefore assessable: identify meaningful program points, state what variables mean there, and show how each path preserves the relevant facts. Structured syntax helps achieve that standard, while abstraction, effect discipline, and explicit contracts complete the reasoning.
Reconstruction exercise
A refactoring replaces the search loop with a while-true dispatcher using integer states START, CHECK, ADVANCE, and DONE. It preserves every old jump as a state assignment. Explain why eliminating goto syntax alone does not show an improvement. Recover a single loop invariant for the original search task, then propose the smallest useful tests and a termination argument. Finally state what additional obligation appears if another task may remove elements from the list during the scan.
Worked solution
The dispatcher may retain all of the original arbitrary transitions, so readers still need to reconstruct which state assignments are possible and what i means in each state. A clearer invariant at one loop header is 0<=i<=n and every element before i is nonnegative. Initialize i=0, advance only after checking a nonnegative element, and stop on the first negative or the end. Test an empty list, [-1], [2,-1], and [2,3], giving results 0,0,1,2. The nonnegative measure n-i decreases on every iteration, establishing termination when n is fixed. Concurrent removal invalidates both the stable-length assumption and the interpretation of already inspected indices. The program needs a snapshot, synchronization, or a separately specified concurrent-iteration contract before the old proof can be reused.
Test the limits and transfer the idea
1. Refactoring and proof · allow 45 minutes
Make a loop state explain its progress
Consider this labeled pseudocode for positive integer inputs x and y:
again: if x == y goto done
if x > y goto reduce_x
y = y - x
goto again
reduce_x: x = x - y
goto again
done: return x
Write an equivalent structured program. Prove preservation of the greatest common divisor and termination, then trace inputs (21,15). Explain which facts you must know to interpret x at the loop header versus at return. Does replacing every jump with a dispatcher variable automatically make those facts easier to see?
Get a starting hint
Use gcd(x,y) = gcd(x-y,y) when x > y. Positivity supplies a natural-number measure x + y that strictly decreases on every nonfinal iteration.
Compare your reasoning with the worked solution
A structured version is:
def gcd_subtract(x,y):
assert x > 0 and y > 0
while x != y:
if x > y:
x -= y
else:
y -= x
return x
assert gcd_subtract(21,15) == 3
For x > y, a number divides both x and y exactly when it divides both x-y and y: subtracting or adding y preserves divisibility. The symmetric argument applies when y > x. Thus gcd(x,y) equals the original inputs' gcd at every loop header. Subtracting the smaller positive value from the larger leaves both values positive. The integer measure x+y strictly decreases, so an infinite sequence of iterations is impossible. At exit x = y, whose gcd is x, establishing the returned answer.
The states are (21,15), (6,15), (6,9), (6,3), and (3,3). At a loop header, x is only one component of a positive pair with the correct gcd; it need not equal the answer. At return, equality supplies the missing fact. This illustrates Dijkstra's concern with relating program position and process state.
A dispatcher such as while True: if pc == ... can reproduce the jumps, but the programmer must still reconstruct invariants across its states. Removing a keyword alone does not supply a clearer proof. This exercise is a modern proof-oriented refactoring, not a claim that the letter proves all structured programs correct or makes low-level branches unnecessary.
Check your reasoning
- Preserves the original branch behavior with a structured loop.
- Proves gcd preservation, positivity, termination, and the final result.
- Explains why state interpretation depends on control position rather than keyword choice.
2. Invariant and counterexample · allow 40 minutes
Explain a nested traversal without a transcript
A rectangular immutable matrix has R rows and C columns. A program visits entries in row-major order and accumulates their sum. Write a structured nested-loop implementation and an invariant at the beginning of the inner loop, with coordinates (i,j).
Evaluate the invariant for [[2,5,1],[4,0,3]] at (i,j) = (1,2). Show what goes wrong if j is initialized only once before the outer loop. Discuss C = 0 explicitly: when is the flattened coordinate k = iC + j useful, and when does it fail to distinguish outer-loop progress?
Get a starting hint
The accumulator should contain all earlier rows and only the first j entries of the current row. A statement about i or j in isolation misses part of that history.
Compare your reasoning with the worked solution
A correct implementation initializes j at each outer iteration:
def matrix_sum(a, columns):
assert all(len(row) == columns for row in a)
total = 0
for i in range(len(a)):
for j in range(columns):
total += a[i][j]
return total
assert matrix_sum([[2,5,1],[4,0,3]],3) == 15
assert matrix_sum([[],[]],0) == 0
At the inner-loop header before entry (i,j), total equals the sum of every entry in rows with index less than i, plus entries of row i with column index less than j. Bounds are 0 ≤ i < R and 0 ≤ j ≤ C, with j = C describing the completed inner traversal. At (1,2), total = 2 + 5 + 1 + 4 + 0 = 12; the next addition contributes 3.
In a while-loop translation, setting j = 0 only before the outer loop leaves j = C after row zero. Every later inner loop is skipped. The example returns 8 instead of 15. The invariant cannot be established at the next row's start because the claimed completed prefix does not match the work performed.
For C > 0, k = iC + j counts visited entries at the specified control point, with boundary conventions handled consistently. For C = 0, k is always zero, so it cannot distinguish which empty row the outer loop is visiting. The pair of loop coordinates still records that progress. This connects the letter's textual and dynamic indices to a concrete state assertion without claiming one counter captures every kind of execution.
Check your reasoning
- States the completed-rows plus current-prefix invariant and computes 12.
- Finds the missing-reset bug and its incorrect result 8.
- Handles empty rows and distinguishes entry count from full control progress.
Check your understanding
A goto-heavy program is mechanically rewritten as a switch inside one loop. What has been established?
- Its control flow is necessarily easier to reason about.
- Its correctness follows from structured syntax.
- Only a syntactic transformation, unless useful invariants and structure are recovered.
Explanation
Equivalent state dispatch can preserve the original complexity.
Why is the processed-prefix invariant more useful than an instruction count?
- It relates data meaning to a stable program point across different inputs.
- It removes all runtime costs.
- It proves mutable inputs cannot change.
Explanation
The invariant states a semantic relationship that supports local reasoning.
Original paper and sources
Edsger W. Dijkstra. Go To Statement Considered Harmful. Communications of the ACM 11(3), March 1968, pp. 147-148. Linked EWD215 transcription is titled A Case against the GO TO Statement and documents its relationship to the published letter.
Read the original paper →Read the primary paper: University of Texas at Austin
- Go To Statement Considered Harmful: University of Texas at Austin