Можно ли перехватить сигнал Ctrl+C (SIGINT) и выполнить функцию очистки, как при defer?
Решение: перехват сигналов через os/signal и синхронизация с defer
Да, это возможно. Стандартная библиотека Go предоставляет пакет os/signal, который позволяет перехватывать сигналы ОС, включая SIGINT (Ctrl+C). Комбинируя его с каналами и горутиной, можно добиться поведения, аналогичного defer — cleanup выполнится в любом случае: при нормальном завершении, при панике или при получении сигнала.
Ключевая идея
Мы создаём канал для сигналов, подписываемся на SIGINT (и, возможно, SIGTERM), а затем в отдельной горутине ждём сигнал. Когда сигнал приходит, мы вызываем функцию очистки и завершаем программу. Чтобы сохранить идиому defer, мы оборачиваем всю логику в функцию run(), где defer сработает при любом выходе.
Пример реализации
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// Создаём контекст с отменой для graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Запускаем основную логику
if err := run(ctx); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) error {
// Открываем файл — cleanup выполнится через defer
f, err := os.Create("server.log")
if err != nil {
return fmt.Errorf("create file: %w", err)
}
defer f.Close()
// HTTP сервер
srv := &http.Server{Addr: ":8080"}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(f, "Request received at", time.Now())
w.Write([]byte("OK"))
})
// Канал для ошибок от сервера
errCh := make(chan error, 1)
// Запускаем сервер в горутине
go func() {
log.Println("Server started on :8080")
errCh <- srv.ListenAndServe()
}()
// Канал для сигналов ОС
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
// Ожидаем либо сигнал, либо ошибку сервера
select {
case sig := <-sigCh:
log.Printf("Received signal: %v. Starting cleanup...", sig)
case err := <-errCh:
if err != nil && err != http.ErrServerClosed {
return fmt.Errorf("server error: %w", err)
}
case <-ctx.Done():
log.Println("Context cancelled")
}
// Graceful shutdown сервера (ждём до 10 секунд)
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("Shutdown error: %v", err)
}
// Здесь сработает defer f.Close() — файл будет закрыт корректно
log.Println("Cleanup complete. Exiting.")
return nil
}
Как это работает
- Сигналы:
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)подписывается на SIGINT (Ctrl+C) и SIGTERM (kill). - Блокировка:
selectждёт одно из трёх событий — сигнал, ошибка сервера или отмена контекста. - Cleanup: После выхода из
selectмы выполняем graceful shutdown сервера, а затем срабатываетdefer f.Close(). - Контекст: Позволяет отменить операцию извне (например, через
cancel()в тестах).
Важные моменты
- Не забудьте указать буфер 1 для канала сигналов, чтобы не потерять сигнал, если горутина занята.
- Если вы используете
log.Fatal(который вызываетos.Exit(1)),deferне сработает. Вместо этого возвращайте ошибку изrun(). - Для полной идиомы
deferможно использовать пакетdeferred.SignalHandlerиз сторонних библиотек, но стандартное решение — предпочтительнее.
Альтернативный подход: использование defer с флагом завершения
Если вам не нужен graceful shutdown HTTP-сервера, а только выполнение cleanup-функции при SIGINT, можно использовать более простой подход с флагом и блокирующим чтением из канала сигналов.
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
cleanupDone := make(chan struct{})
// Перехватываем сигналы
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
// Запускаем горутину, которая ждёт сигнал
go func() {
<-sigCh
fmt.Println("Received signal, cleaning up...")
// Здесь ваша логика очистки
close(cleanupDone)
}()
// Ваша основная работа
fmt.Println("Working... Press Ctrl+C to stop.")
// Блокируем main до завершения cleanup
<-cleanupDone
fmt.Println("Cleanup done. Exiting.")
}
Недостатки этого подхода:
- Не интегрируется с
defer— cleanup вызывается вручную. - Не подходит для серверов с длительными запросами — программа завершается сразу после cleanup.
- Нет graceful shutdown — открытые соединения могут быть разорваны.
Этот вариант хорош для простых скриптов или CLI-утилит, где не требуется сложное управление жизненным циклом. Для production-серверов лучше использовать первый подход с контекстом и Shutdown.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги
Ошибки и исключения в Python: Расширенное и углубленное руководство