I'm assuming that using defer would have prevented the gotos in the first case, and the bug.
C is hard enough as is to get right and every tool and development pattern that helps avoid common pitfalls is welcome.
By putting all the cleanup code at the end of the function after a cleanup label, you have reduced the complexity of resource management: you have one place where the resource is acquired, and one place where the resource is freed. This is actually manageable. Before you return, you check every resource you might have acquired, and if your handle (pointer, file descriptor, PID, whatever) is not in its null state (null pointer, -1, whatever), you call the free function.
By comparison, if you try to put the correct cleanup functions at every exit point, the problem explodes in complexity. Whereas correctly adding a new resource using the 'goto cleanup' pattern requires adding a single 'if (my_resource is not its null value) { cleanup(my_resource) }' at the end of the function, correctly adding a new resource using the 'cleanup at every exit point' pattern requires going through every single exit point in the function, considering whether or not the resource will be acquired at that time, and if it is, adding the cleanup code. Adding a new exit point similarly requires going through all resources used by the function and determining which ones need to be cleaned up.
C is hard enough as it is to get right when you only need to remember to clean up resources in one place. It gets infinitely harder when you need to match up cleanup code with returns.
int ret;
FILE *fp;
if ((fp = fopen("hello.txt", "w")) == NULL) {
perror("fopen");
ret = -1;
} else {
const char message[] = "hello world\n";
if (fwrite(message, 1, sizeof message - 1, fp) != sizeof message - 1) {
perror("fwrite");
ret = -1;
} else {
ret = 0;
}
/* fallible cleanup is unpleasant: */
if (fclose(fp) < 0) {
perror("fclose");
ret = -1;
}
}
return ret;
It is in particular universal in Microsoft documentation (but notably not actual Microsoft code; e.g. https://github.com/dotnet/runtime has plenty of cleanup gotos).In practice, well, the “of doom” part applies: two fallible functions on the main path is (I think) about as many as you can use it for and still have the code look reasonable. A well-known unreasonable example is the official usage sample for IFileDialog: https://learn.microsoft.com/en-us/windows/win32/shell/common....
Using defer, the code would be:
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
return err;
return err;
This has the exact same bug: the function exits with a successful return code as long as the SHA hash update succeeds, skipping further certificate validity checks. The fact that resource cleanup has been relegated to defer so that 'goto fail;' can be replaced with 'return err;' fixes nothing.