1
0
Fork 0
You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
blog/content/python-goto.md

5.8 KiB

Goto in Python

I really know that Python was meant to be used without a goto, but sometimes it really makes sense to use it.

Goto usage patterns:

Let's first show a few common and in my opinion sensible (at least sometimes) uses of goto. Also, I'll discuss possible goto-free alternatives of the code and why they feel worse.

Error handling

The most common one, used all over the Linux kernel for example:

// ...

if(! something_we_dont_want_to_fail()):
	goto err;
// We know it hasn't failed, so we can use it

// ...

return result; // OK

err:
clean_up()
return 0; // This did not work.

Often, there are more goto's for one label, and fall through is utilized, e.g.:

// Open the file
if (file_empty(f))
	goto clean_files;
// Read the file
if (bad_data(f))
	goto clean_files;
// Load data into memory
if (could_not_process(data))
	goto clean_memory;
return processed_data;

clean_memory:
free(data);
clean_files:
close(f);

return 0;

The goto-free alternative is to put everything into functions and check all the functions. This might lead to creating functions with no semantics, and that does not generally make the code more readable.

The lucky part is, this is the case that is simple to solve with exceptions in much the same way.

There is still a case to be made with the multiple cleanup blocks and fall-through, but one of the ways of solving that is to have the exception know what to clean up. That might be a bit ugly, but the goto is way worse.

Alternative methods of evaluation

Sometimes, one would like to try several different paths to get a result, in some order, i.e. with pseudocode like this:

int result = 0;

// Method 1 (the most likely to work)
result = method_1();
if(result)
	goto done;

// Method 1 did not work, try method 2:
result = method_2();
if(result)
	goto done;

// Try method 3 maybe?
result = method_3();
if (! result)
	return ERROR;

done:
// We now have result, so process that
// ...

Possible options to fixing this:

  • Create a function that gets the result. In that function, the goto's can be replaced by return's, so that is nice, but the function might not have sensible name and need an unreasonable number of arguments, if the methods are complicated enough.
  • Reorder the methods to try from the least preffered to most. That obscures the algorithm, and might not be an option, if the methods have side effects.

There exists a solution with exceptions: Raise an exception when one method fails, and try other methods in the exception handling block. That leads to complicated catch[^I will call it "catch" block even though in Python this technically is an "except" block, because it seems more obvious what I mean.] blocks, which is not a good practice. And what is worse, for the third method the blocks would be nested!

Retries

Sometimes, you need to try something several times, or skip something. In some code (not originally in C), I had something along the lines of:

struct line_with_number get_next_reasonable_line():
	// Imagine here a static FILE object, from which I read the lines.
	int linelen = 255;
	char *line;
	static int line_number

next_line:
	getline(line, linelen, file);
	if (line == NULL)
		// Just return some kind of error
		return {.line = NULL, .number = line_number};
	
	line = strip_comments(line);
	if (strlen(line) == 0)
		goto next_line;
	// Look at the line more closely
	if (we_do_not_like(line))
		goto next_line;
	
	return {.line = line, .number = line_number};

The "obvious" solution to this is to use while and continue. But that is not necessarily better -- the code is quite likely to pass only once, so there is no semantics of a cycle. Also, what would be the condition here? We have no data, so maybe while(true). Or in the better case, do ... while(strlen(line) == 0 || we_do_not_like(line)). When the condition is not so simple, using infinite loop is the way to go. But such code is not prettier, since it uses unreasonable flow control and loses the jump's meaning.

There may be a few more places where a goto is a sensible option, but I don't recall any of them, so they might not be as important.

What to do in Python?

There is no simple way around the second use-case for a goto. In some code, I ended up with following code (and to make it worse, that was inside a function, all of it):

# NOTE: This is really ugly goto, simulated using exceptions
class EverythingIsOK(Exception): pass

try:
	# Method 1:
	result = method_1()
	if result: raise EverythingIsOK()	# Goto

	# Method 1 did not work, try method 2:
	result = method_2()
	if result: raise EverythingIsOK()

	# Try method 3 maybe?
	result = method_3()
	if not result:
		raise OperationalError() # Not OK, Not goto.
except EverythingIsOK:	# Label
	pass

# Process result

This is ugly enough, but can only jump downwards. Jumping upwards probably needs the trick with continue, and unfortunately, there is no do-while in Python.

while True:	# Label
	# code
	continue	# Goto
	# code
	break	# Boilerplate

If you do not like that the goto's look different, you can use this mess:

class MyGoto(Exception): pass
while True:	# Label
	try:
		# code
		raise MyGoto()	# Goto
		# code
		break
	except MyGoto:	# Even more boilerplate.
		continue

I have no solution for using a single label for jumping from above and below. But I hope I will never see code that needs that.

This post is not meant to encourage use of goto. It is considered harmful, and in most cases, rightfully so. But still, there are some reasonable use cases, so it is worth knowing when to use it and what can you do when the language does not support goto.

And probably needless to say: I would still rather have native goto support, than need to use awful hacks to simulate the flow which I have in mind.