`defer` в цикле — что будет лучше?

Мне нужно сделать SQL-запросы к базе данных в цикле:

for rows.Next() {

   fields, err := db.Query(.....)
   if err != nil {
      // ...
   }
   defer fields.Close()

   // do something with `fields`

}

Что будет лучше: оставить все как есть или переместить defer после цикла:

for rows.Next() {

   fields, err := db.Query(.....)
   if err != nil {
      // ...
   }

   // do something with `fields`
}

defer fields.Close()

Или что-то другое ?


person demas    schedule 10.08.2017    source источник


Ответы (2)


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

for rows.Next() {

   fields, err := db.Query(.....)
   if err != nil {
      // ...
   }

   // do something with `fields`

   fields.Close()
}
person Adrian    schedule 10.08.2017
comment
В дополнение к этому, в этом случае defer даже не будет работать так, как ожидает OP, так как он закроет только последний fields из цикла (для правильной работы требуется закрытие). Между прочим, обертывание внутреннего тела цикла в анонимный func с defer может быть хорошим решением. - person Martin Tournoij; 10.08.2017
comment
Верно, но даже если замыкание работает правильно, оно все равно не будет работать хорошо. - person Adrian; 10.08.2017
comment
Это по-другому. Если вы используете закрытие для отсрочки, будет вызван только последний. Для defer fields.Close() каждый вызов будет правильно указывать на другой указатель, конечно, это все равно неправильно, так как все будут вызываться после завершения функции. - person Kamil Dziedzic; 08.08.2018
comment
Означает ли это, что если вы выделяете несколько ресурсов в каждой итерации цикла, и возникает ошибка, и вы паникуете внутри предложения if, не закрывая сначала каждый открытый ресурс, ресурсы, выделенные во время последней итерации, не будут должным образом закрыты? т.е. в циклах for нельзя полагаться на автоматическую очистку ресурсов и приходится вручную очищать все ресурсы, выделенные в этой итерации цикла, в случае ошибки? - person Ben Usman; 07.09.2018
comment
Если не откладывать закрытие и восстанавливать панику, не закрывая ресурсы в рекавери, да, у вас может утечка ресурсов, независимо от всего остального. Паника должна быть редкой и, как правило, должна быть фатальной. Если вы собираетесь избавиться от паники, вы должны четко осознавать последствия. - person Adrian; 07.09.2018

Выполнение отложенной функции не только откладывается, откладывается до момента возврата окружающей функции, но также выполняется, даже если объемлющая функция внезапно завершается, например. паника. Спецификация: операторы отсрочки:

Оператор defer вызывает функцию, выполнение которой отложено до момента возврата из окружающей функции, либо потому, что окружающая функция выполнила оператор return достиг конца своего тела функции, или потому что соответствующая горутина паникует.

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

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

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

Пример:

for rows.Next() {
    func() {
        fields, err := db.Query(...)
        if err != nil {
            // Handle error and return
            return
        }
        defer fields.Close()

        // do something with `fields`
    }()
}

Или, если поставить именованную функцию:

func foo(rs *db.Rows) {
    fields, err := db.Query(...)
    if err != nil {
        // Handle error and return
        return
    }
    defer fields.Close()

    // do something with `fields`
}

И вызов его:

for rows.Next() {
    foo(rs)
}

Также, если вы хотите завершить работу при первой ошибке, вы можете вернуть ошибку из foo():

func foo(rs *db.Rows) error {
    fields, err := db.Query(...)
    if err != nil {
        return fmt.Errorf("db.Query error: %w", err)
    }
    defer fields.Close()

    // do something with `fields`
    return nil
}

И вызов его:

for rows.Next() {
    if err := foo(rs); err != nil {
        // Handle error and return
        return
    }
}

Также обратите внимание, что Rows.Close() возвращает ошибку, которая при вызове с использованием defer отбрасывается. Если мы хотим проверить возвращенную ошибку, мы можем использовать такую ​​анонимную функцию:

func foo(rs *db.Rows) (err error) {
    fields, err := db.Query(...)
    if err != nil {
        return fmt.Errorf("db.Query error: %w", err)
    }
    defer func() {
        if err = fields.Close(); err != nil {
            err = fmt.Errorf("Rows.Close() error: %w", err)
        }
    }()

    // do something with `fields`
    return nil
}
person icza    schedule 10.08.2017