Ctrl+C in Go: SIGINT abfangen und «defer»-Bereinigung unter der Haube

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Kann man Ctrl+C in Go abfangen und ein Cleanup wie bei defer durchführen? Analyse unter der Haube

\n\n

Haben Sie jemals einen Server in Go gestartet, Ctrl+C gedrückt und sich gefragt: „Was wäre, wenn ich vor dem Beenden noch ordentlich die DB-Verbindungen schließen, den Cache speichern oder einfach „Tschüss!“ sagen könnte?“ Diese Frage ist auf Stack Overflow so beliebt (tausende Aufrufe!), dass wir uns entschlossen haben, sie detailliert zu analysieren. Heute geben wir nicht nur ein fertiges Rezept, sondern schauen unter die Haube – wie Go Signale verarbeitet, was signal.Notify ist und warum defer hier nicht wirklich die Hauptrolle spielt.

\n\n

Bereit? Los geht's!

\n\n

Problem: Ctrl+C beendet das Programm sofort

\n\n

Standardmäßig sendet das Betriebssystem beim Drücken von Ctrl+C im Terminal das Signal SIGINT an den Prozess. Ein Go-Programm wird, wenn nichts unternommen wird, abrupt beendet – alle Goroutinen werden abgebrochen, defer-Funktionen werden nicht ausgeführt. Das ist, als würde man den Stecker ziehen: Der Computer schaltet sich aus, aber Daten können verloren gehen.

\n\n

Klassisches Beispiel: Sie schreiben einen Webserver, öffnen eine Verbindung zu PostgreSQL, und plötzlich – Ctrl+C. Wenn das Signal nicht verarbeitet wird, bleibt die Verbindung hängen und der Verbindungspool wird nicht freigegeben. Nach ein paar solchen Neustarts sagt die Datenbank: „Tut mir leid, ich kann nicht mehr.“

\n\n
package main\n\nimport (\n    "fmt"\n    "os"\n    "os/signal"\n    "syscall"\n)\n\nfunc main() {\n    // ... Ihr Code\n    fmt.Println("Drücken Sie Ctrl+C zum Beenden")\n    // Auf Signal warten\n    sig := make(chan os.Signal, 1)\n    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)\n    <-sig\n    fmt.Println("Signal empfangen, beende...")\n}
\n\n

Dieser Code fängt das Signal bereits ab, aber was ist, wenn wir eine komplexe Bereinigung durchführen müssen? Hier beginnt die Magie.

\n\n

Wie es unter der Haube funktioniert: signal.Notify und Channels

\n\n

Die Funktion signal.Notify registriert Ihren Channel für den Empfang bestimmter Signale. Wenn das Betriebssystem ein Signal sendet, legt die Go-Laufzeitumgebung es in den Channel. Wenn der Channel gepuffert ist (und wir haben einen Puffer von 1), geht das Signal nicht verloren, selbst wenn die empfangende Goroutine beschäftigt ist.

\n\n

Wichtiger Hinweis: signal.Notify fängt das Signal ab, blockiert aber nicht seine Ausbreitung. Wenn Sie das Signal im Channel nicht verarbeiten, wird das Programm trotzdem nach der Ausführung von defer (falls vorhanden in main) beendet. Wenn Sie jedoch ein Cleanup durchführen möchten, müssen Sie auf dessen Abschluss warten.

\n\n

Ablaufschema:

\n
    \n
  • Betriebssystem → sendet SIGINT an den Prozess
  • \n
  • Go-Laufzeitumgebung → legt das Signal in den über signal.Notify registrierten Channel
  • \n
  • Ihre Goroutine → liest aus dem Channel und startet das Cleanup
  • \n
  • Nach dem Cleanup → rufen Sie os.Exit(0) auf oder verlassen einfach main
  • \n
\n\n

Aber wie macht man ein Cleanup „im defer-Stil“? Denn defer wird bei der Rückkehr aus einer Funktion ausgeführt, nicht beim Empfang eines Signals. Antwort: Wir verwenden defer + Blockieren von main bis zum Empfang des Signals.

\n\n

Rezept: „Defer-ähnliche“ Bereinigung bei Ctrl+C

\n\n

Die Idee ist einfach: Wir schreiben Cleanup-Funktionen, die beim Beenden ausgeführt werden sollen, und „hängen“ sie mit defer ein. Dann blockieren wir main, bis das Signal eintrifft. Wenn das Signal kommt, wird main beendet und alle defer-Funktionen werden ausgeführt. Elegant, oder?

\n\n
package main\n\nimport (\n    "fmt"\n    "os"\n    "os/signal"\n    "syscall"\n    "time"\n)\n\nfunc cleanup() {\n    fmt.Println("Bereinigung: Schließe Verbindungen...")\n    time.Sleep(2 * time.Second) // Simulation der Arbeit\n    fmt.Println("Bereinigung abgeschlossen")\n}\n\nfunc main() {\n    // defer wird bei jedem Beenden von main ausgeführt\n    defer cleanup()\n\n    // Channel für das Signal\n    sig := make(chan os.Signal, 1)\n    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)\n\n    fmt.Println("Server gestartet. Drücken Sie Ctrl+C zum Stoppen.")\n    fmt.Println("Bereinigung wird durchgeführt...")\n\n    //                

Blogs

Buchempfehlungen