golang

Как использовать defer в Go

  • воскресенье, 11 августа 2024 г. в 00:00:08
https://habr.com/ru/companies/otus/articles/833702/

Привет, Хабр!

Defer — это ключевое слово в Go, которое позволяет отложить выполнение функции до момента завершения выполнения текущей функции. Это относительно простой способ управлять ресурсами.

В этой статье мы и рассмотрим как использовать defer в Golang.

Основы

Чтобы понять, как defer работает в Go, нужно знать немного о его внутреннем устройстве. Когда вы используете defer, вы говорите компилятору Go отложить выполнение указанной функции до момента завершения окружающей функции. То естьdefer позволяет управлять ресурсами и гарантировать выполнение определённых действий независимо от того, как будет завершена текущая функция — будь то успешное выполнение или неожиданный выход из-за ошибки.

В Go каждый defer помещает вызов функции в стек отложенных вызовов. Этот стек работает по принципу LIFO, то есть последний добавленный вызов выполняется первым.

Пример кода:

package main

import "fmt"

func main() {
    fmt.Println("Начало функции main")

    defer fmt.Println("Первый defer")
    defer fmt.Println("Второй defer")
    defer fmt.Println("Третий defer")

    fmt.Println("Конец функции main")
}

Вывод:

Начало функции main
Конец функции main
Третий defer
Второй defer
Первый defer

Как видно из вывода, отложенные вызовы выполняются в обратном порядке по сравнению с их объявлением.

Когда есть несколько отложенных вызовов, важно понимать, что они будут выполнены в обратном порядке. Рассмотрим пример, который демонстрирует это поведение:

package main

import "fmt"

func main() {
    defer fmt.Println("Завершение программы")
    
    for i := 1; i <= 3; i++ {
        defer fmt.Println("Отложенный вызов номер", i)
    }

    fmt.Println("Выполнение основного кода")
}

Вывод:

Выполнение основного кода
Отложенный вызов номер 3
Отложенный вызов номер 2
Отложенный вызов номер 1
Завершение программы

defer позволяет структурировать выполнение кода в обратном порядке. Это может быть полезно, например, для поочерёдного закрытия открытых соединений или освобождения ресурсов в порядке, обратном их захвату.

Примеры использования

Управление ресурсами: закрытие файлов, соединений и т. д.

Одно из основных применений defer — это управление ресурсами, которые необходимо закрывать или освобождать после использования. Такие операции, как закрытие файлов или сетевых соединений, идеально подходят для defer, потому что они должны быть выполнены независимо от того, как завершится функция.

Допустим, нужно написать программу для обработки файлов. Вы открываете файл, читаете из него данные, а затем должны его закрыть. Defer позволяет гарантировать, что файл будет закрыт в любом случае:

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)
    }
}

Здесь defer file.Close() гарантирует, что файл будет закрыт, когда main завершит выполнение, независимо от того, будет ли это нормальный выход или завершение из-за ошибки.

Работа с сетевыми соединениями также иногда требует внимания к освобождению ресурсов:

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() обеспечивает закрытие HTTP-соединения после того, как работа с ним будет закончена.

Очистка ресурсов в panic-ситуациях

Defer также хорошо для обработки ситуаций, когда программа неожиданно "паникует".

Рассмотрим обработку panic-ситуации с defer и 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 используется для установки анонимной функции, которая вызывает recover в случае паники.

Логгирование и отладка с использованием defer

Defer также может быть использован для логгирования и отладки.

Иногда бывает полезно записывать в журнал время входа и выхода из функций для отладки или мониторинга:

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") используется для измерения времени выполнения функции processData.

Подсчёт времени выполнения нескольких функций:

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()
}

Вывод:

Выполнение функции A
Время выполнения: 1.0004321s
Выполнение функции B
Время выполнения: 500.6542ms

Каждый вызов функции оборачивается в defer, чтобы точно измерить и вывести время её выполнения.

Ресурсные менеджеры

defer может быть в роли "менеджера", который гарантирует, что все ресурсы будут освобождены корректно.

Пример с транзакциями в БД:

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 используется для автоматического отката транзакции в случае ошибки.

Обработка ошибок

Можно использовать defer для обработки ошибок:

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("Файл успешно открыт")
}

Доступ к возвращаемым значениям функции

Есть возможность получить доступ к возвращаемым значениям функции внутри defer. Это полезно, когда хочется изменить возвращаемые значения в зависимости от состояния выполнения функции:

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)
    }
}

При правильном использовании defer позволяет писать более чистый, безопасный и управляемый код. Главное — помнить о накладных расходах и избегать типичных ошибок, таких как изменение переменных после их захвата или избыточное использование defer в циклах.

Больше практических инструментов по разным языкам программирования эксперты OTUS рассматривают в рамках практических онлайн-курсов. Подробнее с каталогом курсов можно ознакомиться по ссылке.