The Go Blog

Defer, Panic, and Recover

Andrew Gerrand
4 August 2010

Go имеет обычные механизмы управления потоком выполнения: if, for, switch, goto. Также имеется оператор go, который позволяет запускать код в отдельной горутине. В данном случае хотелось бы рассмотреть некоторые менее распространённые механизмы: defer, panic и recover.

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

Например, рассмотрим функцию, которая открывает два файла и копирует содержимое одного файла в другой:

<code>func CopyFile(dstName, srcName string) (written int64, err error) {
  src, err := os.Open(srcName)
  if err != nil {
    return
  }
  dst, err := os.Create(dstName)
  if err != nil {
    return
  }
  written, err = io.Copy(dst, src)
  dst.Close()
  src.Close()
  return
}
</code>

Это работает, но есть ошибка. Если вызов os.Create завершается неудачей, функция завершится без закрытия исходного файла. Эту проблему можно легко исправить, поместив вызов src.Close() перед вторым оператором return, но если бы функция была более сложной, проблема могла бы быть не так очевидной и её решение — не таким простым. Используя операторы defer, мы можем гарантировать, что файлы всегда будут закрыты:

<code>func CopyFile(dstName, srcName string) (written int64, err error) {
  src, err := os.Open(srcName)
  if err != nil {
    return
  }
  defer src.Close()
  dst, err := os.Create(dstName)
  if err != nil {
    return
  }
  defer dst.Close()
  return io.Copy(dst, src)
}
</code>

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

Поведение операторов defer простое и предсказуемое. Существует три простых правила:

  1. Аргументы отложенной функции вычисляются при выполнении оператора defer.

В этом примере выражение “i” вычисляется при отложении вызова Println. Отложенный вызов напечатает “0” после завершения функции.

<code>func a() {
  i := 0
  defer fmt.Println(i)
  i++
  return
}
</code>
  1. Отложенные вызовы функций выполняются в порядке Last In First Out (LIFO) после завершения окружающей функции.

Эта функция печатает “3210”:

<code>func b() {
  for i := 0; i < 4; i++ {
    defer fmt.Print(i)
  }
}
</code>
  1. Отложенные функции могут читать и присваивать значения именованным возвращаемым переменным окружающей функции.

В этом примере отложенная функция увеличивает возвращаемое значение i после завершения окружающей функции. Таким образом, эта функция возвращает 2:

<code>func c() (i int) {
  defer func() { i++ }()
  return 1
}
</code>

Это удобно для изменения возвращаемого значения ошибки функции; мы скоро увидим пример этого.

Panic — это встроенная функция, которая останавливает обычный поток управления и начинает паниковать. Когда функция F вызывает panic, выполнение F прекращается, все отложенные функции в F выполняются обычным образом, а затем F возвращает управление своему вызывающему. Для вызывающей функции F тогда ведёт себя как вызов panic. Процесс продолжается по стеку, пока все функции в текущей горутине не вернутся, в этот момент программа завершает выполнение с ошибкой. Паники могут быть вызваны напрямую с помощью функции panic. Они также могут быть вызваны ошибками времени выполнения, например, при выходе за пределы массива.

Recover — это встроенная функция, которая восстанавливает контроль над паникующей горутиной. Recover полезен только внутри отложенных функций. Во время обычного выполнения вызов recover вернёт nil и не окажет никакого другого эффекта. Если текущая горутина паникует, вызов recover захватит значение, переданное в panic, и возобновит нормальное выполнение.

Вот пример программы, демонстрирующей механизмы panic и defer:

<code>package main
import "fmt"
func main() {
  f()
  fmt.Println("Returned normally from f.")
}
func f() {
  defer func() {
    if r := recover(); r != nil {
      fmt.Println("Recovered in f", r)
    }
  }()
  fmt.Println("Calling g.")
  g(0)
  fmt.Println("Returned normally from g.")
}
func g(i int) {
  if i > 3 {
    fmt.Println("Panicking!")
    panic(fmt.Sprintf("%v", i))
  }
  defer fmt.Println("Defer in g", i)
  fmt.Println("Printing in g", i)
  g(i + 1)
}
</code>

Функция g принимает целое число i и паникует, если i больше 3, иначе она вызывает саму себя с аргументом i+1. Функция f откладывает функцию, которая вызывает recover и выводит восстановленное значение (если оно не nil). Попробуйте представить, какой будет вывод этой программы, прежде чем продолжить чтение.

Программа выведет:

<code>Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.
</code>

Если мы удалим отложенную функцию из f, паника не будет восстановлена и дойдёт до вершины стека вызовов горутины, завершив выполнение программы. Эта изменённая программа выведет:

<code>Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
panic: 4
panic PC=0x2a9cd8
[stack trace omitted]
</code>

Для реального примера использования panic и recover, смотрите пакет json из стандартной библиотеки Go. Он кодирует интерфейс с помощью набора рекурсивных функций. Если возникает ошибка при обходе значения, вызывается panic, чтобы раскрутить стек до вызова верхнего уровня, который восстанавливает панику и возвращает соответствующее значение ошибки (см. методы 'error' и 'marshal' типа encodeState в encode.go).

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

Другие способы использования defer (помимо примера file.Close, приведённого выше) включают освобождение мьютекса:

<code>mu.Lock()
defer mu.Unlock()
</code>

печать нижнего колонтитула:

<code>printHeader()
defer printFooter()
</code>

и другие.

В заключение, инструкция defer (со или без panic и recover) предоставляет необычный и мощный механизм управления потоком выполнения. Она может использоваться для моделирования целого ряда возможностей, реализованных специализированными структурами в других языках программирования. Попробуйте это применить.

Следующая статья: Go Wins 2010 Bossie Award
Предыдущая статья: Share Memory By Communicating
Blog Index

GoRu.dev Golang на русском

На сайте представлена адаптированная под русский язык документация языка программирования Golang