Go in a nutshell
  • Go in a nutshell
  • Введение
  • Родословная или Should I Go?
  • Hello, world!
  • Базовые типы данных
  • Go - установка, сборка
  • Алфавит и пакеты
  • Синтаксис
  • Приведение типов
  • Операторы
  • Константы
  • Управление потоком
  • Массивы, срезы, отображения
  • Именованные типы и структуры
  • Указатели и оператор new
  • Функции
  • Работа с параметрами командной строки
  • Завершение работы программы
  • Методы
Powered by GitBook
On this page
  • Линейное выполнение программы
  • os.Exit()
  • Методы пакета log
  • Паника (panic)
  • Небольшая хитрость

Was this helpful?

Завершение работы программы

Всё хорошо, что хорошо кончается

Всё хорошо, что хорошо кончается, как известно. И, хотя, безусловно, существует множество программ-серверов, ориентированных (при соблюдении штатного режима работы) на цикличное, бесконечное выполнение, нередкой ситуацией является также и останов выполнения программного кода, по той или иной причине. Не очень часто, но программисту приходится управлять этим процессом. Предлагаю рассмотреть данный вопрос под увеличительным стеклом.

Линейное выполнение программы

Если программа не написана намеренно с целью бесконечного циклического выполнения (это может быть сетевой сервер или служба, выполняющая некую периодическую работу), то программный код (конкретнее, тело функции main) будет выполняться линейно, "сверху вниз", и, по достижении конца программы, произойдёт её самостоятельное завершение. Точка входа - функция main, в отличие, скажем, от того же языка С, может иметь возвращаемый тип только void.

package main

func main() int {
    return 0
}
//Неверно!
//Результат сборки:
//func main must have no arguments and no return values

Как видите, мы не можем вернуть значение из функции main подобным образом. Поэтому, нам стоит разобраться, как же теперь быть с кодом возврата! Вспомним же, для начала, что это такое. Истину говорят: дай определение - и это уже половина понимания. Итак, в многозадачных системах, процессы могут порождать другие процессы. В этой ситуации, процессы, один по отношению к другому, называются соответственно родительским и дочерним. Дочерний процесс, по своему завершению, автоматически (либо явно по указанию программиста и эту ситуацию мы рассмотрим ниже) совершает системный вызов exit, передавая ему целое число. В результате вызова exit целое число передаётся родительскому процессу, который может получить его с помощью системного вызова wait. Данное целочисленное значение - результат системного вызова exit - и называется кодом возврата процесса (приложения). Единой трактовки значений кодов возврата нет, тем более с учётом кроссплатформенности. Что, пожалуй, в этой системе координат является общим, так это представление о том, что код возврата "0" свидетельствует о штатном завершении работы программы. Коды со значением > 0 свидетельствуют о той или иной возникшей runtime-проблеме. Предлагаю для начала рассмотреть поведение функции main в языке Go, как оно есть, то есть без вмешательства программиста. Для этого напишем программу, которая может произвести потенциальную ошибку времени выполнения.

package main

import "fmt"

func main() {
	var i int
	fmt.Println("Enter a number, please:")
	fmt.Scanf("%d", &i)
	fmt.Println(5 / i)
}

Понятно, ввод числа "0" очевидно приведёт нас к ошибке "runtime error: integer divide by zero". Ввод же допустимого значения позволит программе завершиться штатно. Итак, выполняем go build и программа готова. Далее, в зависимости от используемой ОС, научимся "забирать" код ошибки. Для windows это будет переменная %errorlevel% (выполняем echo %errorlevel% в консоли cmd), для никсовой оболочки  bash этоо соответственно будет переменная $?. Теперь давайте выполним нашу программу без ошибки деления на "0". Затем, выведем код возврата. Он будет равен нулю, как мы и ожидали. Теперь, заставим нашу программу завершиться с ошибкой и вновь выведем код. В этом случае код будет равен двум. В качестве удовлетворения любопытства, предлагаю вам придумать разнообразные сценарии программ, выполнение которых могло бы привести к ошибке, собрать и выполнить данные программы, каждый раз извлекая код возврата. Вы сможете убедиться, что каждый раз это будет код "2".

Итак, резюмируем. В языке Go функция main может иметь тип возвращаемого значения только void. Таким образом, мы не можем задать код возврата с помощью оператора return так, как это делается в языке C. Однако, программы на Go всё же формируют код возврата автоматически. При штатном завершении работы программы значение будет всегда равно нулю, а при нештатном - двойке.

os.Exit()

Итак, выше мы рассмотрели поведение программ, написанных на языке Go, при завершении их выполнения в тех случаях, когда программист не вмешивается явно в процесс формирования кода возврата. У любопытного читателя, конечно же, возникает резонный вопрос: а можно ли управлять? Хвалю любознательного читателя. И спешу на этот вопрос ответить. Хотя, как было разобрано выше, мы и не можем использовать оператор return, мы можем использовать метод Exit пакета os. В качестве аргумента, вышеуказанный метод принимает целочисленное значение, которое и будет являться кодом возврата. Диапазон допустимых значений, принимаемых методом os.Exit() варьируется в зависимости от операционной системы. Для windows это будет диапазон от -2147483648 до 2147483647. Опытный глаз угадает в этих числах диапазон значений, допустимых для знакового int32. Отрицательные значения не только допускаются но и воспринимаются системой. При выходе за рамки диапазона, происходит переполнение типа с характерным "перескоком" значений (2147483647 + 1 => -2147483648; -2147483648 - 1 => 2147483647). Для никсов всё выглядит несколько скромнее. Отрицательных значений система не интерпретирует. Допустимый диапазон ограничивается 8 битами - от 0 до 255. При попытке выйти за рамки данного диапазона поведение следующее: 256 => 1, 257 =>2 и т. д. , по достижении следующей степени двойки (512, 768, 1024...) значение снова интерпретируется системой как 0. Отрицательные значения принимаются, но интерпретируются системой как положительные по схеме -1 => 255, -2 => 254 и т. д., по достижении степени двойки (-512, -768, -1024...) мы получаем снова 0 и так далее. Таким образом, для соблюдения безопасной кроссплатформенности, рекомендую использовать значения os.Exit() в диапазоне от 0 до 255.

Важное замечание. При вызове os.Exit() в любом месте программы, происходит моментальный останов её выполнения. При этом никакие deferred функции выполнены не будут! Если безусловно необходимо выполнение всех отложенных функций, для останова программы можно воспользоваться вызовом паники (читайте об этом ниже).

package main

import (
	"fmt"
	"os"
)

func main() {
	defer func() {
		fmt.Println("In deferred func")
	}()

	fmt.Println("In main func")
	os.Exit(1)
}

//Результат:
//In main func
//Отложенная функция не была выполнена!
//Код возврата = 1

Если вы хотите связать код завершения с результатом выполнения некого программного блока, предлагаю следующий трюк:

package main

import "os"

func run() int {
	// here goes
	// the code

	return 1
}

func main() {
	os.Exit(run())
}

Методы пакета log

В языке Go существует весьма полезный пакет log, родственный по своим возможностям пакету fmt. Методы пакета log позволяют не только выводить информацию в консоль (или иной заданный источник), но и добавлять к ней сведения о текущем времени. Формат временной метки можно изменить при помощи специальных флагов. Вспомним, как это работает:

package main

import (
	"fmt"
	"log"
)

func main() {
	fmt.Println("Hello, world!")
	log.Println("Done!")

}
//Результат:
//Hello, world!
//2020/11/10 23:00:00 Done!

Итак, пакеты fmt и log содержат много аналогичных методов, с той лишь разницей, что использование log даст нам в дополнение временную метку. Однако, при всём сходстве, данные пакеты не совсем идентичны. И различие не только в указании времени. Пакет log содержит несколько методов, которые позволяют производить останов выполнения программы, сразу после выведения текстового сообщения с временной меткой. Это методы log.Fatal(), log.Fatalln() и log.Fatalf(). Набор этих методов аналогичен набору Print(), Println(), Printf() пакета всё того же пакета log (и fmt, если опустить временную метку). С той разницей, что вслед за выводом текстовой информации, сразу же следует вызов os.Exit(1). Выполнение программы завершается, передаётся код возврата 1, отложенные функции не выполняются. Чаще всего, данные методы пакета log используются в тех случаях, когда программист желает завершить выполнение программы по причине некой ошибки, сопроводив завершение определённым текстовым сообщением. Например:

package main

import (
	"log"
	"os"
)

func main() {
	file, err := os.Open("conf.json")
	if err != nil {
		log.Fatalf("Cannot open config file, %s", err)
	}
	file.Close()
}
//Результат:
//2020/11/10 23:00:00 Cannot open config file, open conf.json:
//no such file or directory
//Program exited: status 1

Здесь мы хотели прочитать некий конфигурационный файл и предусмотрели обработку ошибки открытия файла. Поскольку запрошенный файл обнаружен не был, произошёл останов выполнения программы с кодом возврата 1 (нет смысла работать дальше, если отсутствует данный файл). Таков типичный случай применения "фатальных" методов пакета log. Никогда не за бывайте о порядке действий: сначала обработка ошибки, затем закрытие файла (дескриптора соединения, базы данных и т. д.)! Это важно.

Среди методов пакета log существуют также и такие, которые могут вызвать так называемую "панику". О ней мы и поговорим.

Паника (panic)

Многие ошибки времени выполнения, к примеру, попытка индексирования массива за пределами границ, вызывают панику во время выполнения (runtime panic).

Состояние паники существенно отличается от простого завершения выполнения программы. Паника останавливает нормальный поток выполнения и выходит из текущей функции. Любые отложенные вызовы будут выполняться до того, как управление будет передано следующей более высокой функции в стеке. Функция каждого стека будет выходить и запускать отложенные вызовы до тех пор, пока паника не будет обработана с использованием отложенного recover() , или пока паника не достигнет функции main() и не завершит программу. Если это произойдет, аргумент, предоставленный панике, и трассировка стека будут напечатаны на stderr . Код возврата при панике будет равен двум. Таким образом, при панике мы получаем выполнение всех отложенных функций, а также трассировку стека, которую можно анализировать. Использование recover() позволяет обработать панику и продолжить работу в штатном режиме, либо завершить программу с кодом возврата "0". Злоупотребление встроенной функцией recover(), вне сомнения, является дурным тоном программирования. Вы должны всячески стремиться к тому, чтобы выполнение ваших программ не приводило к ситуации паники. Однако, в особых случаях, если вам необходимо написать программу, останов выполнения которой нежелателен ни при каких условиях, а достижение этой цели при помощи одной лишь аккуратности программиста не видится достижимым - используйте recover(). Но не забывайте при этом хотя бы так или иначе сигнализировать об ошибках, чтобы не потерять контроль над реальной ситуацией. Приведём пример возникновения ситуации паники:

package main

import "fmt"

func printElement(slice []string, idx int) {
    defer func() {
        fmt.Println("Defer func in printElement")
    }()
        
    fmt.Println(slice[idx])
}

func main() {
    defer func() {
        fmt.Println("Defer func in main")
    }()
    
    slice := []string{"a", "b", "c"}
    printElement(slice, len(slice))//out of range
}
//Результат:
//Defer func in printElement
//Defer func in main
//panic: runtime error: index out of range [3] with length 3

//goroutine 1 [running]:
//main.printElement(0xc00006af40, 0x3, 0x3, 0x3)
//	/tmp/sandbox399073643/prog.go:10 +0xe8
//main.main()
//	/tmp/sandbox399073643/prog.go:19 +0xbd
//Program exited: status 2.

Этот пример является иллюстрацией факта выполнения отложенных функций, а также порядка их выполнения. Кроме того, вы видите и можете проанализировать трассу стека. Из трассы видно, что ошибку следует искать в строках 10 и 19. Код возврата - "2". Применение встроенной функции recover() изменит картину следующим образом:

package main

import "fmt"

func printElement(slice []string, idx int) {
    defer func() {
    if r := recover(); r != nil {
        //Не забыли просигнализировать об ошибке!
        fmt.Println("Recovered in printElement func, continue: ", r) }
    }()
    
    fmt.Println(slice[idx])
}

func main() {
    defer func() {
        fmt.Println("Defer func in main")
    }()
    
    slice := []string{"a", "b", "c"}
    printElement(slice, len(slice)) //out of range
}
//Результат:
//Recovered in printElement func, continue:
//runtime error: index out of range [3] with length 3
//Defer func in main
//Код возврата - "0"!

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

panic("Just a test") // string
panic(1) //int
err := errors.New("Test exception")
panic(err) // error

Модифицируем приведенный ранее пример с чтением конфигурационного файла:

package main

import (
	"errors"
	"fmt"
	"os"
)

func main() {
	defer func() {
		fmt.Println("Some code in defer func")
	}()
	file, err := os.Open("conf.json")
	if err != nil {
		err = errors.New("Cannot open config file")
		panic(err)
	}
	file.Close()
}
//Результат:
//Some code in defer func
//panic: Cannot open config file

//goroutine 1 [running]:
//main.main()
//	/tmp/sandbox934454262/prog.go:16 +0xdf
//Program exited: status 2.

Как видите, использование вызова паники позволяет добиться выполнения отложенных функций, а также вывести трассу стека. Как мы помним, при использовании os.Exit(), останов происходит сразу; никакие отложенные функции не выполняются. Для верного понимания состояния паники, ещё раз перечитайте описание поведения потока выполнения программы в данном состоянии. Перечитали? Отлично!

Как упоминалось выше, существуют методы пакета log, предназначенные для вызова паники. Это методы log.Panic(), log.Panicf() и log.Panicln(). В принципе, их поведение близко к поведению аналогичных "фатальных" методов пакета log. Различие в том, что, после вывода текста и временной метки, вместо вызова os.Exit(1), вызывается panic().  Если разобрать точнее, то произойдёт следующее: сначала выведется временная метка и текст, соответствующий аргументу использованного "панического" метода пакета log, затем произойдёт "всплытие" по стеку с выполнением всех отложенных функций. В случае, если при этом будет достигнута функция recover(), то это станет выходом из состояния паники. В противном случае, далее будет выведен текст "panic: <текст, соответствующий аргументу использованного "панического" метода пакета log>", а также трасса стека. Как всегда при панике, код возврата - "2":

package main

import (
	"errors"
	"fmt"
	"log"
	"os"
)

func main() {
	defer func() {
		fmt.Println("Some code in defer func")
	}()
	file, err := os.Open("conf.json")
	if err != nil {
		err = errors.New("Cannot open config file")
		log.Panic(err)
	}
	file.Close()
}
//Результат:
//2020/11/10 23:00:00 Cannot open config file
//Some code in defer func
//panic: Cannot open config file

//goroutine 1 [running]:
//log.Panic(0xc00006af60, 0x1, 0x1)
//	/usr/local/go-faketime/src/log/log.go:351 +0xac
//main.main()
//	/tmp/sandbox624898865/prog.go:17 +0x108
//Program exited: status 2.

А теперь добавим recover():

package main

import (
	"errors"
	"fmt"
	"log"
	"os"
)

func main() {
	defer func() {
		recover()
	}()
	defer func() {
		fmt.Println("Some code in defer func")
	}()
	file, err := os.Open("conf.json")
	if err != nil {
		err = errors.New("Cannot open config file")
		log.Panic(err)
	}
	file.Close()
}
//Результат:
//2009/11/10 23:00:00 Cannot open config file
//Some code in defer func
//Ожидаемо, код возврата - "0"

Небольшая хитрость

В качестве завершения темы, расскажу небольшую хитрость, как обойти особенность использования os.Exit(), касающуюся запрета выполнения отложенных функций. Для выполнения таких функций, предлагаю вашему вниманию следующее решение:

package main

import (
	"fmt"
	"os"
)

func main() {
	exitCode := 0
	defer func() { os.Exit(exitCode) }()

	// Do whatever, including deferring more functions

	defer func() {
		fmt.Printf("Do some cleanup\n")
	}()

	func() {
		fmt.Printf("Do some work\n")
	}()

	// But let's say something went wrong
	exitCode = 1

	// Do even more work/cleanup if you want

	// At the end, os.Exit will be called with the last value of exitCode
}

Как становится ясно из данного примера, мы переносим сам по себе вызов os.Exit() в отложенную функцию, которая, по принципу LIFO, будет выполнена последней. Также, вводится переменная-флаг, которая хранит статус выполнения программы и может изменяться по ходу её работы. Так, будут выполнены все предусмотренные вами deferred функции, а метод os.Exit() будет в итоге вызван с последним значением кода возврата.

Раунд!

PreviousРабота с параметрами командной строкиNextМетоды

Last updated 4 years ago

Was this helpful?