Implementing “defer”: Go vs Java vs C / CPP

1 Brief

The problem of resource leakage is a problem that many novices can easily ignore. Certain resources requested in the program need to be released explicitly, especially system resources, such as open file descriptors, tcp & udp sockets, dynamically requested heap memory, and so on. Even though there are many ways to avoid resource leaks, developers still cannot avoid resource leaks well. Avoiding resource leaks is not a very technical issue, but when everyone writes code and is stunned by various business logic, bugs are easy to come in. Go defer, cpp smart pointer, and java try-catch-finally are all tools to solve this kind of resource release problem.

2 Avoid resource leaks

It would be great if the language level could provide some ability to release resources in a timely manner after the resource application is successful and used. Before comparing in depth how to automatically release resources in C, C ++, Java, and Go, let us consider the difficulties in this process:

2.1 After the resource application is successfully completed and used normally, how can I judge that the resource has been used up?

Take the open file descriptor as an example, fd is an int type variable. After the file is opened, it is valid for the entire life cycle of the process. In fact, after the file access is complete, you can think that this fd can be closed and released; then apply dynamically As an example, the heap memory space is also valid during the process life cycle. Heap memory is accessed through pointers. If there is no pointer pointing to this heap memory area, the allocated heap memory can be considered freed; Take the applied lock as an example. After the lock is successful, the critical section can be accessed. From the moment the critical section exits, the lock can be considered to be released.

Different types of resources have different methods for judging whether they are used up, but one thing can be confirmed, the developer knows when the resources should be released.

2.2 Developers know when resources should be released, but how does the language level provide some mechanism to avoid developer oversight?

C does not provide a language-level mechanism to avoid the problem of resource leakage, but similar effects can be achieved by using the C extended attributes provided by gcc;

C ++ provides smart pointers. For example, malloc dynamically allocates heap memory. If the allocation succeeds, it returns a heap memory pointer. The pointer is used to initialize a smart pointer and is bound to the corresponding callback method. When the scope of the smart pointer is destroyed, its The callback method on the binding will also be called. If the method we will release the heap memory is free(ptr)registered as a callback method on the smart pointers, you can automatically release the heap memory when allocating memory where the scope of the destruction.

Java provides try-catch-finally, which applies and uses resources in try, catches possible exceptions and handles them in catch, and releases resources in finally. Take opening the file as an example. After the file is opened in the try and the file processing ends, the file.close () method is called in finally. Considering an extreme case, our file processing logic is more complicated, and more code is involved in the middle. After writing various logic processing and exception handling, is it easy for developers to forget to close the file in finally? This possibility is relatively large. Developers’ habits generally follow the principle of proximity. When defining variables, they are used before use. If there are no obvious file-related operations at the end of the try block, the developer may not think of closing the file.

2.3 Taking into account the developer’s habit of following the nearest principle, can I register a callback method for resource release immediately after the resource application is successful, and callback this callback method when the resource usage ends? This method is relatively easy to implement and sounds elegant.

Go defer provides the ability to register a resource release method immediately after the resource application is successful, select the function exit phase as the time point at which the application ends, and then call back the registered method to release the resource to finally complete the release of the resource. Not only can meet the programmer’s good style of “near use”, but also reduces the possibility of leaking resources due to forgetting, and the code is more maintainable.

Go defer may also bring some performance loss in some cases. For example, lock is used to protect the critical section, and the lock can be released after the critical section exits. However, the defer will only trigger the release operation of the resource when the function exits. This may cause the lock granularity to be too large and reduce the concurrent processing capacity. At this point, developers need to make a trade-off and make sure that there is no problem in choosing defer.

3.1 Simulating defer in C

C itself does not provide a defer or similar defer mechanism, but similar capabilities can be achieved with the help of gcc extensions. Use the extended attributes provided by gcc __cleanup__to decorate variables. When the variables leave the scope, they can automatically call the registered callback function.

The following is a description of the gcc extension attribute `` . In fact, this extended attribute provided by gcc is more fine-grained than go defer control, because the granularity it can control can be as fine as the “ scope level “, while go defer can only narrow the effective range to the “ function level “.

Example one cleanup_attribute_demo.c

# include <stdio.h> / * Demo code showing the usage of the cleanup variable 
attribute. See: http://gcc.gnu.org/onlinedocs/gcc/Variable-Attributes.html
* /
/ * cleanup function
the argument is a int * to accept the address
to the final value
* /
void clean_up (int * final_value)
{
printf ("Cleaning up \ n");
printf ("Final value:% d \ n", * final_value);
}
int main (int argc, char ** argv)
{
/ * declare cleanup attribute along with initiliazation
Without the cleanup attribute, this is equivalent
to:
int avar = 1;
* /

int avar __attribute__ ((__cleanup __ (clean_up))) = 1;
avar = 5;
return 0;
}

Compile and run:

$ gcc -Wall cleanup_attribute_demo.c 
$ ./a.out
Cleaning up
Final value: 5

Example two

/ * Demo code showing the usage of the cleanup variable 
attribute. See: http://gcc.gnu.org/onlinedocs/gcc/Variable-Attributes.html
* /
/ * Defines two cleanup functions to close and delete a temporary file
and free a buffer
* /
# include <stdlib.h>
# include <stdio.h>
# define TMP_FILE "/tmp/tmp.file" void free_buffer (char ** buffer)
{
printf ("Freeing buffer \ n");
free (* buffer);
}
void cleanup_file (FILE ** fp)
{
printf ("Closing file \ n");
fclose (* fp);
printf ("Deleting the file \ n");
remove (TMP_FILE);
}
int main (int argc, char ** argv)
{
char * buffer __attribute__ ((__cleanup __ (free_buffer))) = malloc (20);
FILE * fp __attribute__ ((__cleanup __ (cleanup_file)));
fp = fopen (TMP_FILE, "w +");

if (fp! = NULL)
fprintf (fp, "% s", "Alinewithnospaces");
fflush (fp);
fseek (fp, 0L, SEEK_SET);
fscanf (fp, "% s", buffer);
printf ("% s \ n", buffer) ;

return 0;
}

Compile and run:

Alinewithnospaces 
Closing file
Deleting the file
Freeing buffer

3.2 Simulating defer in C ++

In C ++, smart pointers can be used to perform simple simulations of defer, and their granularity can be controlled to the scope level, not the go defer function level. The defer implemented in C ++ simulation is also relatively elegant.

#include <iostream> 
#include <memory>
#include <funtional>
using namespace std;
using defer = shared_ptr <void>;
int main () {
defer _ (nullptr, bind ([] {cout << ", world"; }));
cout << "hello"
}

You can also remove the bind and write the lambda expression directly.

#include <iostream> 
#include <memory>
using namespace std;
using defer = shared_ptr <void>;
int main () {
defer _ (nullptr, [] (...) {cout << ", world";}) ;
cout << "hello"
}

Output when said code is executed: hello, world. The smart pointer variable is _. The scope ends when the main function exits. When the smart pointer is destroyed, the lambda expression bound to b is automatically called. The program outputs hello first, and then world. The sample code here approximates go defer.

3.3 Simulating defer in Java

Try-catch-finally sample code in Java, here is simply provided in the form of pseudo code:

InputStream fin = null; try { 
// open file and read
fin = new FileInputStream (...);
String line = fin.readLine ();
System.out.println (line);
} catch (Exception e) {
// handle exception
} finally {
fin.close ();
}

This try-catch-finally method in Java can only be regarded as a resource release method, and cannot be regarded as a simulated defer. Java doesn’t seem to provide any ability to sense the end of a scope or a function and trigger a callback function. I didn’t figure out how to emulate defer gracefully in Java.

3.4 defer in Go

I think defer in Go is currently the most elegant implementation in various programming languages, simple and easy to use, in line with everyone’s habits, and the code is readable. defer in Go is so widely used that even an example is redundant, so the sample code is omitted here :).

Resource release needs to be carefully considered. Resource leaks are common mistakes made by novice programmers. This article triggers from the idea of ​​go defer and compares the ways in which defer semantics are implemented in different languages ​​c, cpp, and java.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store