How to use defer in Go
Basics
To understand how defer
works in Go, you need to know a little about its internals. When you use defer
you tell the Go compiler to defer execution of the specified function until the enclosing function completes. That is,defer
allows you to manage resources and ensure that certain actions are performed regardless of how the current function completes – whether it is successful or an unexpected exit due to an error.
In Go, everyone defer
places the function call in deferred call stack. This stack works on the principle LIFOthat is, the last added call is executed first.
Code example:
package main
import "fmt"
func main() {
fmt.Println("Начало функции main")
defer fmt.Println("Первый defer")
defer fmt.Println("Второй defer")
defer fmt.Println("Третий defer")
fmt.Println("Конец функции main")
}
Conclusion:
Начало функции main
Конец функции main
Третий defer
Второй defer
Первый defer
As you can see from the output, deferred calls are executed in the reverse order of their declaration.
When there are multiple deferred calls, it is important to understand that they will be executed in reverse order. Let's look at an example that demonstrates this behavior:
package main
import "fmt"
func main() {
defer fmt.Println("Завершение программы")
for i := 1; i <= 3; i++ {
defer fmt.Println("Отложенный вызов номер", i)
}
fmt.Println("Выполнение основного кода")
}
Conclusion:
Выполнение основного кода
Отложенный вызов номер 3
Отложенный вызов номер 2
Отложенный вызов номер 1
Завершение программы
defer
allows you to structure code execution in reverse order. This can be useful, for example, for closing open connections one by one or releasing resources in the reverse order of their acquisition.
Examples of use
Resource management: closing files, connections, etc.
One of the main applications defer
— is the management of resources that need to be closed or released after use. Operations such as closing files or network connections are ideal for defer
because they must be executed regardless of how the function completes.
Let's say you need to write a program to process files. You open a file, read data from it, and then you have to close it. Defer
allows you to ensure that the file will be closed in any case:
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.Open("example.txt")
if err != nil {
fmt.Println("Ошибка при открытии файла:", err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Println("Ошибка чтения файла:", err)
}
}
Here defer file.Close()
ensures that the file will be closed when main
will terminate execution, regardless of whether it is a normal exit or an error exit.
Working with network connections also sometimes requires attention to releasing resources:
package main
import (
"fmt"
"net/http"
)
func fetchData(url string) error {
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("ошибка получения данных: %v", err)
}
defer resp.Body.Close()
// обработка данных из ответа
// ...
return nil
}
func main() {
err := fetchData("https://example.com")
if err != nil {
fmt.Println("Ошибка:", err)
}
}
defer resp.Body.Close()
Ensures that the HTTP connection is closed after work with it is finished.
Cleaning up resources in panic situations
Defer
also good for handling situations where the program unexpectedly “panicking“.
Let's consider handling a panic situation with defer
And recover
:
package main
import (
"fmt"
)
func safeDivide(a, b int) int {
defer func() {
if r := recover(); r != nil {
fmt.Println("Восстановление после паники:", r)
}
}()
return a / b
}
func main() {
fmt.Println("Результат деления:", safeDivide(10, 0))
}
defer
used to set an anonymous function that calls recover
in case of panic.
Logging and debugging using defer
Defer
can also be used for logging and debugging.
Sometimes it is useful to log the entry and exit times of functions for debugging or monitoring:
package main
import (
"fmt"
"time"
)
func logExecutionTime(start time.Time, name string) {
elapsed := time.Since(start)
fmt.Printf("%s заняла %s\n", name, elapsed)
}
func processData() {
defer logExecutionTime(time.Now(), "processData")
// типо обработка данных
time.Sleep(2 * time.Second)
fmt.Println("Данные обработаны")
}
func main() {
processData()
}
defer logExecutionTime(time.Now(), "processData")
used to measure the execution time of a function processData
.
Calculating the execution time of several functions:
package main
import (
"fmt"
"time"
)
func logFunctionTime() func() {
start := time.Now()
return func() {
fmt.Printf("Время выполнения: %v\n", time.Since(start))
}
}
func funcA() {
defer logFunctionTime()()
time.Sleep(1 * time.Second)
fmt.Println("Выполнение функции A")
}
func funcB() {
defer logFunctionTime()()
time.Sleep(500 * time.Millisecond)
fmt.Println("Выполнение функции B")
}
func main() {
funcA()
funcB()
}
Conclusion:
Выполнение функции A
Время выполнения: 1.0004321s
Выполнение функции B
Время выполнения: 500.6542ms
Each function call is wrapped in defer
to accurately measure and display the time of its execution.
Resource managers
defer
can be in the role of “manager“, which ensures that all resources are released correctly.
Example with transactions in the database:
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func executeTransaction(db *sql.DB) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
tx.Rollback()
fmt.Println("Транзакция откатилась")
} else {
tx.Commit()
fmt.Println("Транзакция завершена")
}
}()
_, err = tx.Exec("INSERT INTO users(name) VALUES('John')")
if err != nil {
return err
}
_, err = tx.Exec("INSERT INTO accounts(user_id) VALUES(LAST_INSERT_ID())")
return err
}
func main() {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
fmt.Println("Ошибка подключения к базе данных:", err)
return
}
defer db.Close()
err = executeTransaction(db)
if err != nil {
fmt.Println("Ошибка выполнения транзакции:", err)
}
}
defer
used to automatically roll back a transaction in case of an error.
Error handling
Can be used defer
for error handling:
package main
import (
"fmt"
"os"
)
func openFile(filename string) (f *os.File, err error) {
f, err = os.Open(filename)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
f.Close()
fmt.Println("Файл закрыт из-за ошибки")
}
}()
// выполнение операций с файлом
return f, nil
}
func main() {
file, err := openFile("example.txt")
if err != nil {
fmt.Println("Ошибка:", err)
return
}
defer file.Close()
fmt.Println("Файл успешно открыт")
}
Accessing Function Return Values
It is possible to access the return values of a function inside defer
. This is useful when you want to change the return values depending on the state of the function execution:
package main
import (
"fmt"
)
func calculateSum(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Восстановление после паники:", r)
err = fmt.Errorf("panic occurred")
}
}()
result = a + b
if result > 100 {
panic("Сумма слишком велика")
}
return result, nil
}
func main() {
sum, err := calculateSum(50, 60)
if err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Println("Сумма:", sum)
}
}
When used correctly defer
allows you to write cleaner, safer, and more manageable code. The key is to be mindful of overhead and avoid common mistakes such as modifying variables after they are captured or overusing defer
in cycles.
OTUS experts consider more practical tools for different programming languages within the framework of practical online courses. You can find more details with the course catalog see the link.