Whoever has lived in the era of the early BASIC home computers learned to hate the ugly spaghetti code that derived from languages, like BASIC, that did not support structured programming. The main accused for that mess was undoubtedly the goto statement.

Since then, the simple presence of the goto statement immediately denoted bad programming. But is this always true?

Goto is still with us

The key point in favor of structured programming, i.e. what makes the difference with spaghetti coding, is that we can consider each code block as a "black box":

if (condition) {
  ...some code
}
...some other code

Thanks to structured programming, if we are reading the code above, we don’t need to go into the details of some code to know that some other code will be always executed, no matter of condition. Right?

Wrong! Most programming languages, including ANSI C, allow statements like break, return and so on. Let’s look at this snippet:

if (condition) {
  return;
}
...some other code

If condition is true, "some other code" will never be executed. This is exactly the same as:

if (condition) {
  goto exit;
}
...some other code
:exit

We apparently got rid of the goto statement, but it is still with us, hidden under false names.

A good use for goto

We can now see how goto can be used to enhance the quality of our code. Let’s consider a simple C function that creates a copy of a file. The implementation of the function executes the following sequence of logical steps:

  1. retrieve the size of the source file
  2. allocate a memory buffer as big as the source file
  3. open the input file
  4. open the output file
  5. read the data from the input file into the buffer
  6. write the data from the buffer to the output file
  7. close the files and free the buffer

A naïve implementation of the steps above would look like this:

void copyFile (const char* fileSrc, const char* fileDst)
{
	FILE* fSrc;
	FILE* fDst;
	char* buffer;
	struct stat statBuf;
	
	/* 1.retrieve the size of the source file */
	stat (fileSrc, &statBuf);
		
	/* 2.allocate a memory buffer big enough */
	buffer = malloc (statBuf.st_size);

	/* 3.open the input file */
	fSrc = fopen (fileSrc, "rb");
			
	/* 4.open the output file */
	fDst = fopen (fileDst, "wb");

	/* 5.read the data from the input file into the buffer */
	fread (buffer, statBuf.st_size, 1, fSrc);

	/* 6.write the data from the buffer to the output file */
	fwrite (buffer, statBuf.st_size, 1, fDst);

	/* 7.close the files and free the allocated memory */
	fclose (fDst);
	fclose (fSrc);
	free (buffer);
}

The function above is obviously missing all the error handling code: anything wrong and this function will crash! Let’s implement the same function with error handling and perfect structured code:

/* Copy "fileSrc" to "fileDst". Returns 0=ok 1=error */
int copyFile (const char* fileSrc, const char* fileDst)
{
	FILE* fSrc;
	FILE* fDst;
	char* buffer;
	struct stat statBuf;
	int ret;
	
	/* 1.retrieve the size of the source file */
	if (stat (fileSrc, &statBuf) == 0) {
		
		/* 2.allocate a memory buffer big enough */
		buffer = malloc (statBuf.st_size);

		if (buffer != NULL) {
		
			/* 3.open the input file */
			fSrc = fopen (fileSrc, "rb");
			if (fSrc != NULL) {
				
				/* 4.open the output file */
				fDst = fopen (fileDst, "wb");
				if (fDst != NULL) {
				
					/* 5.read the data from the input file into the buffer */
					if (fread (buffer, statBuf.st_size, 1, fSrc) == 1) {
						
						/* 6.write the data from the buffer to the output file */
						if (fwrite (buffer, statBuf.st_size, 1, fDst) == 1) {
							/* fwrite ok */
							ret = 0;
						}
						else {						
							/* fwrite failed */
							ret = 1;
						}
					}
					else {
						/* fread failed */
						ret = 1;
					}
					
					/* 7.close the files and free the allocated memory (fDst) */
					fclose (fDst);
				}
				else {
					ret = 1; /* fopen (fDst) failed */
				}

				/* 7.close the files and free the allocated memory (fSrc) */
				fclose (fSrc);
			}
			else {
				ret = 1; /* fopen (fSrc) failed */
			}

			/* 7.close the files and free the allocated memory (buffer) */
			free (buffer);
		}
		else {
			ret = 1; /* malloc failed */
		}
	}
	else {
		ret = 1; /* stat failed */
	}
	
	/* Return the result */
	return ret;
}

This might be a perfectly structured function but, to be honest, it’s ugly. The code nesting is confusing because it is not representing the real nature of this function, which is a flat sequence of operations. The error handling code gets so far away from the related calls that, without a comment like /* malloc failed */, it would be very hard to tell at a glance which is which.

We can rewrite the same function using a flat layout and the "politically correct" return statement:

/* Copy "fileSrc" to "fileDst". Returns 0=ok 1=error */
int copyFile (const char* fileSrc, const char* fileDst)
{
	FILE* fSrc;
	FILE* fDst;
	char* buffer;
	struct stat statBuf;
	
	/* 1.retrieve the size of the source file */
	if (stat (fileSrc, &statBuf)) return 1;
		
	/* 2.allocate a memory buffer big enough */
	buffer = malloc (statBuf.st_size);
	if (buffer==NULL) return 1;

	/* 3.open the input file */
	fSrc = fopen (fileSrc, "rb");
	if (fSrc==NULL) {
		free (buffer);
		return 1;
	}
			
	/* 4.open the output file */
	fDst = fopen (fileDst, "wb");
	if (fSrc==NULL) {
		free (buffer);
		fclose (fSrc);
		return 1;
	}

	/* 5.read the data from the input file into the buffer */
	if (fread (buffer, statBuf.st_size, 1, fSrc) != 1) {
		free (buffer);
		fclose (fSrc);
		fclose (fDst);
		return 1;
	}

	/* 6.write the data from the buffer to the output file */
	if (fwrite (buffer, statBuf.st_size, 1, fDst) != 1) {
		free (buffer);
		fclose (fSrc);
		fclose (fDst);
		return 1;
	}

	/* 7.close the files and free the allocated memory (buffer) */
	free (buffer);
	fclose (fSrc);
	fclose (fDst);
	return 0;
}

Now the code has become flat and the sequence of serial operations is clear again. However, at any failure point we must remember to free the resources succesfully allocated so far. For example, if the fopen at line 24 fails, we must remember to free the memory of buffer and the file fSrc that was succesfully opened at line 17. With this implementation, there is a clear and present danger to forget some deallocations, being them spread and duplicated all over.

Now let’s look at the version with the evil goto:

/* Copy "fileSrc" to "fileDst". Returns 0=ok 1=error */
int copyFile (const char* fileSrc, const char* fileDst)
{
	/* 0.initialize the variables */
	FILE* fSrc = NULL;
	FILE* fDst = NULL;
	char* buffer = NULL;
	struct stat statBuf;
	int ret = 1;
	
	/* 1.retrieve the size of the source file */
	if (stat (fileSrc, &statBuf)) goto exitFunc;
		
	/* 2.allocate a memory buffer big enough */
	buffer = malloc (statBuf.st_size);
	if (buffer==NULL) goto exitFunc;

	/* 3.open the input file */
	fSrc = fopen (fileSrc, "rb");
	if (fSrc==NULL) goto exitFunc;
			
	/* 4.open the output file */
	fDst = fopen (fileDst, "wb");
	if (fSrc==NULL) goto exitFunc;

	/* 5.read the data from the input file into the buffer */
	if (fread (buffer, statBuf.st_size, 1, fSrc) != 1) goto exitFunc;

	/* 6.write the data from the buffer to the output file */
	if (fwrite (buffer, statBuf.st_size, 1, fDst) != 1) goto exitFunc;

	/* Got here? Everything went ok */
	ret = 0;

exitFunc:
	/* 7.close the files and free the allocated memory */
	if (fDst) fclose (fDst);
	if (fSrc) fclose (fSrc);
	if (buffer) free (buffer);
	return ret;
}

In this version, we used a different approach:

  • the variables to be allocated are initialized to NULL: this value is used as a marker to understand at any point if that variable needs freeing or not;
  • when an error is detected, the only required action is goto exitFunc;;
  • the ret variable is initialized at 1 (line 9): in this way, any early interruption with goto will automatically cause the error code “1” to be returned;
  • the deallocation code, used for both correct and abnormal terminations, is concentrated in one single deinitialization area (lines 37-39).

Conclusions

The goto statement can be succesfully used to jump to a common exit point in case of error. Unlike return, the goto to an exit point allows the execution of deinitialization code before exiting. This approach is the C implementation of the concept of exception handling and, in my opinion, it is strongly recommended.