In Go, error is often returned by the function. If an ERR is returned when a function is called, it must be processed. Once more Err is processed, it is easy to make errors if resources are not handled properly. Let’s take a look at how defer can be used in a real application to simplify our application.

First, let’s look at an example of closing a resource without using the defer statement. We implemented a function to copy files. We will also manage the closure of the File descriptor, because once an * os.file is opened and ready for reading and writing, it must be closed using the Close function. Finally, at the end of the function, we will use the Sync method to flush the buffer of the file system to force the content to disk and make the copy persistent. Here is the concrete implementation:

func CopyFile(srcName, dstName string) error{SRC, err := os.Open(srcName) ①iferr ! =nil {
        return err
    }
		
    stat, err := src.Stat()
    iferr ! =nil {
        src.Close()
	return err
    }
		
    ifStat. IsDir () {(2) the SRC. The Close ()return fmt.Errorf("file %q is a directory", srcName)
    }
    
    dst, err := os.Create(dstName) ③
    iferr ! =nil {
        src.Close()
	returnErr} _, err = IO.Copy(DST, SRC) ④iferr ! =nil {
        src.Close()
        dst.Close()
        returnErr} err = dst.sync () ⑤ src.close () dst.close ()return err
}
Copy the code

① Open the source file

② Check whether it is a directory

③ Create the target file

4 Copy the source file to the target file

⑤ Refresh the file system buffer

Note: Closing * os.file will return an error. However, in this case the error can be safely ignored because we force the buffer flush of the file system. Otherwise, we should log at least one error if it occurs. In the error Management chapter, we’ll see how errors can be handled gracefully in the defer statement.

The implementation works. We open the source file, check if it is a directory, and then handle the copy logic. However, we noticed some template code:

  • Src.close () is repeated five times
  • Dst.close () is repeated twice

Having to consider both source and object files closing at the end of the code makes our code very error-prone. Fortunately, Go provides a solution to this problem through the defer keyword, as shown in Figure 2.1:

The defer function is called when the function returns. The defer function is guaranteed to be executed even if the main function panics or unexpectedly terminates. The call to defer is pushed onto the stack. When the main function returns, the defer function pops up from the stack (first in, last out). Here, c() will be called first, then b (), and finally a ().

Note that the time for a defer call is when the function returns, not when the block in which it is called exits. As follows:

func main(a) {
    fmt.Println("a")
    if true {
        defer fmt.Print("b")
    }
    fmt.Print("c"} This code prints acB instead of ABC.Copy the code

Let’s go back to the example of the CopyFile function and implement another version using the defer keyword:

func CopyFile(srcName, dstName string) error {
    src, err := os.Open(srcName)
    iferr ! =nil {
	return err 
    }
    deferSrc.close () ① stat, err := src.stat ()iferr ! =nil {
        return err 
    }
		
    if stat.IsDir() {
        return fmt.Errorf("file %q is a directory", srcName)
    }
		
    dst, err := os.Create(dstName)
    iferr ! =nil {
        return err 
    }
    deferDst. Close() ② _, err = IO.Copy(DST, SRC)iferr ! =nil {
	return err
    }
		
    return dst.Sync()
}
Copy the code

① Delay src.close ()

Delay dst.close ()

In this version of the implementation, we removed the repeated close calls by using the defer keyword. This makes functions lighter and more readable. We don’t have to turn SRC and DST off at the end of every code path to make it less error-prone.

Defer statements are often paired with operating functions are used together, like the open/close, connect/disconnect, and lock/unlock function to ensure that resources are in all circumstances can be released.

Here’s another example of using sync.mutex:

type Store struct {
    mutex sync.Mutex
    data map[string]int
}

func (s *Store) Set(key string, value int){s.m utex. The Lock () (1)deferS.mutex.unlock () ② s.ata [key] = value}Copy the code

1 the Mutex lock

② Unlock in the defer statement

We locked mutex using the s.mutex.lock function and called the match operation of s.mutex.unlock () in defer.

Note: If we had to implement a pre and POST operation, such as a mutex lock/unlock that does not return any value, we could also implement this:

func (s *Store) Set(key string, value int) {
    deferS.lockunlock ()() ① s.data[key] = value}func (s *Store) lockUnlock(a) func(a) {
    s.mutex.Lock()
    return func(a) {
        s.mutex.Unlock()
    }
}
Copy the code

(1) s.lockunlock () is executed immediately, but s.lockunlock () is delayed

Delayed function calls are s.lockunlock ()(), not s.lockunlock (). Therefore, the s.lockunlock () part is executed immediately (s.mutex.lock), but the returned closure operation is delayed (s.mutex.unlock ()). It adds some syntactic sugar and handles the pre/post operations in functions in a single line of code, which is sometimes handy. If you use this pattern, also be aware that facing s.Lockunlock ()() with two sets of parentheses can be very confusing, depending on your team’s seniority.

When refactoring code, we also need to be aware of possible impacts. For example, suppose we need to split a main function containing the call to defer into multiple functions. In this case, the defer statement will not execute once the application has finished executing, but the defer call will be called when the subfunction has finished executing:

// Before
func main(a) {
    consumer := createConsumer()
    deferConsumer. The Close () (1)// ...
}

// After
func main(a) {
    consumer := handleConsumer()
    // ...
}

func handleConsumer(a) Consumer {
    consumer := createConsumer()
    deferConsumer. The Close () (2)return consumer
}
Copy the code

① Once the program ends, the defer call will be executed

The call to defer will only be executed when the handleConsumer function ends

Here, we introduced a bug by refactoring consumer. As soon as the handleConsumer function ends, consumer.close () is executed. It looks like a simple comment, but when you have to refactor a lot of code, it’s sometimes easy to ignore the defer statement.

Also note that prior to Go 1.14, the defer statement was not inline. Inlining is an optimization technique by which the compiler saves a function call directly in the calling function. This is why in some projects where performance is a key factor, the defer keyword is rarely used. However, after Go 1.14, the defer statement can be optimized by inlining.

In summary, defer avoids rigid code and reduces the risk of forgetting to release resources, such as releasing resources, disconnecting, mutex unlocking, and so on.