Potential risks of defer Close() in Golang

Keywords: Python Java Go

As a Gopher, we can easily form a programming convention: whenever there is an object x that implements the io.Closer interface, after getting the object and checking the error, we will immediately use defer X. close() to ensure that the X object is closed when the function returns. Here are two examples of idiomatic writing.

  • HTTP request
resp, err := http.Get("https://golang.google.cn/")
if err != nil {
    return err
}
defer resp.Body.Close()
// The following code: handle resp
  • access files
f, err := os.Open("/home/golangshare/gopher.txt")
if err != nil {
    return err
}
defer f.Close()
// The following code: handle f

Existing problems

In fact, there are potential problems with this writing. defer x.Close() ignores its return value, but when executing x.Close(), we can't guarantee that x will close normally. What should we do if it returns an error? This way of writing makes it possible for the program to make errors that are very difficult to check.

So what error does the close () method return? In POSIX Operating Systems, such as Linux or maxOS, the close() function of closing files finally calls the system method close(). We can check what errors close() may return through the man close manual

ERRORS
     The close() system call will fail if:

     [EBADF]            fildes is not a valid, active file descriptor.

     [EINTR]            Its execution was interrupted by a signal.

     [EIO]              A previously-uncommitted write(2) encountered an
                        input/output error.

Error EBADF indicates an invalid file descriptor fd, which is independent of the situation in this article; EINTR refers to Unix signal interruption; Then the possible error in this article is EIO.

EIO errors refer to uncommitted reads. What is this error?

EIO error means that the close() method is called before the write() read of the file is committed.

The figure above is a classic computer memory hierarchy. In this hierarchy, from top to bottom, the access speed of devices becomes slower and slower, and the capacity becomes larger and larger. The main idea of memory hierarchy is that the upper layer memory is used as the cache of the lower layer memory.

The CPU accesses registers very fast. In contrast, accessing RAM is very slow. Accessing disk or network means wasting time. If each write() call synchronously submits data to disk, the overall performance of the system will be extremely reduced, and our computer will not work like this. When we call write(), the data is not immediately written to the target carrier. Each carrier of the computer memory is caching the data. At the right time, brush the data to the next carrier, which changes the synchronous, slow and blocked synchronization of the write call into a fast and asynchronous process.

In this way, the EIO error is indeed a mistake we need to guard against. This means that if we try to save the data to the disk, the operating system has not brushed the data to the disk when defer x.Close() is executed, and we should get the error prompt (as long as the data has not been recorded, the data will not be persistent successfully, and it may be lost. For example, in case of power failure, this part of the data will disappear forever, and we will not know it). However, according to the above conventional writing method, our program gets nil error.

Solution

We discuss several feasible transformation schemes according to the situation of closing documents

  • The first option is not to use defer
func solution01() error {
    f, err := os.Create("/home/golangshare/gopher.txt")
    if err != nil {
        return err
    }

    if _, err = io.WriteString(f, "hello gopher"); err != nil {
        f.Close()
        return err
    }

    return f.Close()
}

In this way, we need to explicitly call f.Close() to close when the io.WriteString fails to execute. However, in this scheme, we need to add the closing statement f.Close() to every error. If there are many write case s to F, it is easy to miss the closed file.

  • The second option is to handle this by naming the return value err and closure
func solution02() (err error) {
    f, err := os.Create("/home/golangshare/gopher.txt")
    if err != nil {
        return
    }

    defer func() {
        closeErr := f.Close()
        if err == nil {
            err = closeErr
        }
    }()

    _, err = io.WriteString(f, "hello gopher")
    return
}

This solution solves the risk of forgetting to close the file in solution 1. If there are more conditional branches of if err! = nil, this mode can effectively reduce the number of lines of code.

  • The third option is to display a call to f.Close() before the last return statement of the function
func solution03() error {
    f, err := os.Create("/home/golangshare/gopher.txt")
    if err != nil {
        return err
    }
    defer f.Close()

    if _, err := io.WriteString(f, "hello gopher"); err != nil {
        return err
    }

    if err := f.Close(); err != nil {
        return err
    }
    return nil
}

This solution can get the close call due to the existence of defer f.Close() when io.WriteString has an error. It can also get the error of err: = f.Close() when io.WriteString has no error but the cache has not been flushed to the disk. Because defer f.Close() does not return an error, it is not worried that two Close() calls will overwrite the error.

  • The last option is to execute f.Sync() when the function return s
func solution04() error {
    f, err := os.Create("/home/golangshare/gopher.txt")
    if err != nil {
        return err
    }
    defer f.Close()

    if _, err = io.WriteString(f, "hello world"); err != nil {
        return err
    }

    return f.Sync()
}

Because calling close() is the last chance to get the error returned by the operating system, but when we close the file, the cache will not necessarily be flushed to the disk. Then, we can call f.Sync() (which calls the system function fsync internally) to force the kernel to persist the cache to the disk.

// Sync commits the current contents of the file to stable storage.
// Typically, this means flushing the file system's in-memory copy
// of recently written data to disk.
func (f *File) Sync() error {
    if err := f.checkValid("sync"); err != nil {
        return err
    }
    if e := f.pfd.Fsync(); e != nil {
        return f.wrapErr("sync", e)
    }
    return nil
}

Due to the call of fsync, this mode can well avoid EIO in close. It can be predicted that this scheme can ensure data security, but its execution efficiency will be greatly reduced due to mandatory disk brushing.

Posted by cdc5205 on Mon, 22 Nov 2021 18:52:21 -0800