Grundlegendes zu Defer in Go

Einführung

Go enthält viele der in anderen Programmiersprachen üblichen Schlüsselwörter für den Kontrollfluss, z. B. "+ if ", " switch ", " for " usw. Ein Schlüsselwort, das in den meisten anderen Programmiersprachen nicht gefunden wird, ist " defer +". Auch wenn es seltener vorkommt, werden Sie schnell erkennen, wie nützlich es in Ihren Programmen sein kann.

Eine der Hauptanwendungen einer "+ defer " - Anweisung ist das Bereinigen von Ressourcen wie offenen Dateien, Netzwerkverbindungen und https://en.wikipedia.org/wiki/Handle_(computing)[database-Handles]. Wenn Ihr Programm mit diesen Ressourcen fertig ist, ist es wichtig, sie zu schließen, um die Programmgrenzen nicht zu erschöpfen und anderen Programmen den Zugriff auf diese Ressourcen zu ermöglichen. ` defer +` macht unseren Code sauberer und weniger fehleranfällig, indem die Aufrufe beibehalten werden, um die Datei / Ressource in der Nähe des offenen Aufrufs zu schließen.

In diesem Artikel erfahren Sie, wie Sie die Anweisung "+ defer " zum Bereinigen von Ressourcen verwenden und wie Sie einige häufige Fehler machen, die bei der Verwendung von " defer +" auftreten.

Was ist eine + Defer + -Anweisung?

Eine "+ Defer " - Anweisung fügt den Aufruf "https://www.digitalocean.com/community/tutorials/how-to-define-and-call-functions-ingo[function]" hinzu, der auf das Schlüsselwort " Defer +" folgt Stapel. Alle Aufrufe dieses Stacks werden aufgerufen, wenn die Funktion, zu der sie hinzugefügt wurden, zurückkehrt. Da die Aufrufe auf einem Stapel abgelegt sind, werden sie in der Reihenfolge des letzten Eingangs und des ersten Ausgangs aufgerufen.

Sehen wir uns an, wie "+ aufschieben +" funktioniert, indem wir einen Text ausdrucken:

main.go

package main

import "fmt"

func main() {
   defer fmt.Println("Bye")
   fmt.Println("Hi")
}

In der Funktion "+ main " haben wir zwei Anweisungen. Die erste Anweisung beginnt mit dem Schlüsselwort " defer ", gefolgt von einer Anweisung " print ", die " Bye " ausgibt. Die nächste Zeile gibt ` Hi +` aus.

Wenn wir das Programm ausführen, sehen wir die folgende Ausgabe:

OutputHi
Bye

Beachten Sie, dass "+ Hi " zuerst gedruckt wurde. Dies liegt daran, dass alle Anweisungen, denen das Schlüsselwort " defer " vorangestellt ist, erst am Ende der Funktion aufgerufen werden, in der " defer +" verwendet wurde.

Schauen wir uns das Programm noch einmal genauer an. Dieses Mal fügen wir einige Kommentare hinzu, um zu veranschaulichen, was passiert:

main.go

package main

import "fmt"

func main() {
   // defer statement is executed, and places
   // fmt.Println("Bye") on a list to be executed prior to the function returning
   defer fmt.Println("Bye")

   // The next line is executed immediately
   fmt.Println("Hi")

   // fmt.Println*("Bye") is now invoked, as we are at the end of the function scope
}

Der Schlüssel zum Verständnis von "+ defer " ist, dass die Argumente für die zurückgestellte Funktion sofort ausgewertet werden, wenn die Anweisung " defer " ausgeführt wird. Wenn ein ` defer +` ausgeführt wird, platziert es die darauf folgende Anweisung in einer Liste, die aufgerufen werden soll, bevor die Funktion zurückkehrt.

Obwohl dieser Code die Reihenfolge darstellt, in der "+ defer " ausgeführt wird, ist dies keine typische Methode, um ein Go-Programm zu schreiben. Es ist wahrscheinlicher, dass wir ` defer +` verwenden, um eine Ressource wie ein Dateihandle zu bereinigen. Schauen wir uns als nächstes an, wie das geht.

Bereinigen von Ressourcen mit + defer +

Die Verwendung von + defer + zum Bereinigen von Ressourcen ist in Go weit verbreitet. Schauen wir uns zunächst ein Programm an, das eine Zeichenfolge in eine Datei schreibt, jedoch nicht "+ defer +" verwendet, um die Ressourcenbereinigung durchzuführen:

main.go

package main

import (
   "io"
   "log"
   "os"
)

func main() {
   if err := write("readme.txt", "This is a readme file"); err != nil {
       log.Fatal("failed to write file:", err)
   }
}

func write(fileName string, text string) error {
   file, err := os.Create(fileName)
   if err != nil {
       return err
   }
   _, err = io.WriteString(file, text)
   if err != nil {
       return err
   }
   file.Close()
   return nil
}

In diesem Programm gibt es eine Funktion namens "+ write ", die zuerst versucht, eine Datei zu erstellen. Wenn ein Fehler vorliegt, wird der Fehler zurückgegeben und die Funktion beendet. Als nächstes wird versucht, den String " Dies ist eine Readme-Datei" in die angegebene Datei zu schreiben. Wenn ein Fehler auftritt, wird der Fehler zurückgegeben und die Funktion beendet. Anschließend versucht die Funktion, die Datei zu schließen und die Ressource wieder an das System freizugeben. Schließlich gibt die Funktion "+ nil +" zurück, um anzuzeigen, dass die Funktion fehlerfrei ausgeführt wurde.

Obwohl dieser Code funktioniert, gibt es einen subtilen Fehler. Wenn der Aufruf von "+ io.WriteString +" fehlschlägt, kehrt die Funktion zurück, ohne die Datei zu schließen und die Ressource an das System zurückzugeben.

Wir könnten das Problem beheben, indem wir eine weitere Anweisung + file.Close () + hinzufügen. So würden Sie dies wahrscheinlich in einer Sprache ohne + defer + lösen:

main.go

package main

import (
   "io"
   "log"
   "os"
)

func main() {
   if err := write("readme.txt", "This is a readme file"); err != nil {
       log.Fatal("failed to write file:", err)
   }
}

func write(fileName string, text string) error {
   file, err := os.Create(fileName)
   if err != nil {
       return err
   }
   _, err = io.WriteString(file, text)
   if err != nil {

       return err
   }
   file.Close()
   return nil
}

Auch wenn der Aufruf von "+ io.WriteString +" fehlschlägt, wird die Datei trotzdem geschlossen. Obwohl dies ein relativ einfach zu entdeckender und zu behebender Fehler war, der eine kompliziertere Funktion aufwies, wurde er möglicherweise übersehen.

Anstatt den zweiten Aufruf von "+ file.Close () " hinzuzufügen, können wir eine " defer " -Anweisung verwenden, um sicherzustellen, dass unabhängig davon, welche Verzweigungen während der Ausführung verwendet werden, immer " Close () +" aufgerufen wird.

Hier ist die Version, die das Schlüsselwort "+ defer +" verwendet:

main.go

package main

import (
   "io"
   "log"
   "os"
)

func main() {
   if err := write("readme.txt", "This is a readme file"); err != nil {
       log.Fatal("failed to write file:", err)
   }
}

func write(fileName string, text string) error {
   file, err := os.Create(fileName)
   if err != nil {
       return err
   }

   _, err = io.WriteString(file, text)
   if err != nil {
       return err
   }
   return nil
}

Diesmal haben wir die Codezeile hinzugefügt: + defer file.Close () +. Dies teilt dem Compiler mit, dass er die + Datei.Close + ausführen soll, bevor die Funktion + write + beendet wird.

Wir haben jetzt sichergestellt, dass wir die Datei auch dann bereinigen und schließen, wenn wir in Zukunft mehr Code hinzufügen und einen weiteren Zweig erstellen, der die Funktion verlässt.

Wir haben jedoch noch einen weiteren Fehler eingeführt, indem wir den Defer hinzugefügt haben. Wir überprüfen nicht mehr den möglichen Fehler, der von der Methode "+ Close " zurückgegeben werden kann. Dies liegt daran, dass es bei Verwendung von " defer +" keine Möglichkeit gibt, einen Rückgabewert an unsere Funktion zurückzugeben.

In Go wird es als sichere und akzeptierte Methode angesehen, + Close () + mehrmals aufzurufen, ohne das Verhalten Ihres Programms zu beeinflussen. Wenn + Close () + einen Fehler zurückgibt, wird dies beim ersten Aufruf durchgeführt. Dies ermöglicht es uns, es in unserer Funktion explizit auf dem erfolgreichen Ausführungsweg aufzurufen.

Schauen wir uns an, wie wir den Aufruf von "+ Close +" + zurückstellen und trotzdem über einen Fehler berichten können, wenn wir auf einen stoßen.

main.go

package main

import (
   "io"
   "log"
   "os"
)

func main() {
   if err := write("readme.txt", "This is a readme file"); err != nil {
       log.Fatal("failed to write file:", err)
   }
}

func write(fileName string, text string) error {
   file, err := os.Create(fileName)
   if err != nil {
       return err
   }
   defer file.Close()
   _, err = io.WriteString(file, text)
   if err != nil {
       return err
   }

   return
}

Die einzige Änderung in diesem Programm ist die letzte Zeile, in der wir + file.Close () + zurückgeben. Wenn der Aufruf von + Close + zu einem Fehler führt, wird dieser nun wie erwartet an die aufrufende Funktion zurückgegeben. Beachten Sie, dass die Anweisung + defer file.Close () + auch nach der Anweisung + return ausgeführt wird. Dies bedeutet, dass + file.Close () + möglicherweise zweimal aufgerufen wird. Dies ist zwar nicht ideal, aber eine akzeptable Vorgehensweise, da es keine Nebenwirkungen für Ihr Programm verursachen sollte.

Wenn jedoch früher in der Funktion ein Fehler auftritt, z. B. beim Aufrufen von "+ WriteString ", gibt die Funktion diesen Fehler zurück und versucht auch, " file.Close " aufzurufen, da er zurückgestellt wurde. Obwohl ` file.Close +` möglicherweise (und wahrscheinlich auch) einen Fehler zurückgibt, interessiert uns dies nicht mehr, da wir einen Fehler erhalten haben, der uns eher mitteilt, was anfangs schief gelaufen ist.

Bisher haben wir gesehen, wie wir mit einem einzigen Aufschub sicherstellen können, dass wir unsere Ressourcen ordnungsgemäß bereinigen. Als nächstes werden wir sehen, wie wir mehrere + defer + -Anweisungen verwenden können, um mehr als eine Ressource zu bereinigen.

Mehrere + defer + Anweisungen

Es ist normal, dass eine Funktion mehr als eine "+ defer " - Anweisung enthält. Erstellen wir ein Programm, das nur die Anweisungen " defer +" enthält, um zu sehen, was passiert, wenn wir mehrere Defer einführen:

main.go

package main

import "fmt"

func main() {
   defer fmt.Println("one")
   defer fmt.Println("two")
   defer fmt.Println("three")
}

Wenn wir das Programm ausführen, erhalten wir die folgende Ausgabe:

Outputthree
two
one

Beachten Sie, dass die Reihenfolge umgekehrt ist, in der wir die "+ defer +" - Anweisungen aufgerufen haben. Dies liegt daran, dass jede aufgerufene verzögerte Anweisung über der vorherigen gestapelt und dann umgekehrt aufgerufen wird, wenn die Funktion den Gültigkeitsbereich verlässt (Last In, First Out).

In einer Funktion können beliebig viele verzögerte Aufrufe erfolgen. Beachten Sie jedoch, dass alle Aufrufe in der umgekehrten Reihenfolge erfolgen, in der sie ausgeführt wurden.

Nachdem wir nun die Reihenfolge verstanden haben, in der mehrere Verzögerungen ausgeführt werden, wollen wir sehen, wie wir mehrere Verzögerungen verwenden, um mehrere Ressourcen zu bereinigen. Wir erstellen ein Programm, mit dem eine Datei geöffnet, darauf geschrieben und anschließend erneut geöffnet wird, um den Inhalt in eine andere Datei zu kopieren.

main.go

package main

import (
   "fmt"
   "io"
   "log"
   "os"
)

func main() {
   if err := write("sample.txt", "This file contains some sample text."); err != nil {
       log.Fatal("failed to create file")
   }

   if err := fileCopy("sample.txt", "sample-copy.txt"); err != nil {
       log.Fatal("failed to copy file: %s")
   }
}

func write(fileName string, text string) error {
   file, err := os.Create(fileName)
   if err != nil {
       return err
   }
   defer file.Close()
   _, err = io.WriteString(file, text)
   if err != nil {
       return err
   }

   return file.Close()
}

func fileCopy(source string, destination string) error {
   src, err := os.Open(source)
   if err != nil {
       return err
   }
   defer src.Close()

   dst, err := os.Create(destination)
   if err != nil {
       return err
   }
   defer dst.Close()

   n, err := io.Copy(dst, src)
   if err != nil {
       return err
   }
   fmt.Printf("Copied %d bytes from %s to %s\n", n, source, destination)

   if err := src.Close(); err != nil {
       return err
   }

   return dst.Close()
}

Wir haben eine neue Funktion namens "+ fileCopy +" hinzugefügt. In dieser Funktion öffnen wir zuerst unsere Quelldatei, aus der wir kopieren wollen. Wir prüfen, ob beim Öffnen der Datei ein Fehler aufgetreten ist. Wenn ja, geben wir den Fehler zurück und verlassen die Funktion. Andernfalls verschieben wir das Schließen der gerade geöffneten Quelldatei.

Als nächstes erstellen wir die Zieldatei. Wir überprüfen erneut, ob beim Erstellen der Datei ein Fehler aufgetreten ist. Wenn ja, geben wir diesen Fehler zurück und verlassen die Funktion. Andernfalls verschieben wir auch das Zeichen + Close () + für die Zieldatei. Wir haben jetzt zwei "+ defer +" - Funktionen, die aufgerufen werden, wenn die Funktion ihren Gültigkeitsbereich verlässt.

Nachdem wir beide Dateien geöffnet haben, kopieren wir die Daten von der Quelldatei in die Zieldatei. Wenn das erfolgreich ist, werden wir versuchen, beide Dateien zu schließen. Wenn wir einen Fehler beim Schließen einer der beiden Dateien erhalten, geben wir den Fehler zurück und verlassen den Funktionsumfang.

Beachten Sie, dass wir explizit "+ Close () " für jede Datei aufrufen, obwohl " defer " auch " Close () +" aufruft. Dies soll sicherstellen, dass bei einem Fehler beim Schließen einer Datei der Fehler gemeldet wird. Es wird auch sichergestellt, dass, wenn die Funktion aus irgendeinem Grund mit einem Fehler vorzeitig beendet wird, z. B. wenn das Kopieren zwischen den beiden Dateien fehlgeschlagen ist, jede Datei weiterhin versucht, die zurückgestellten Aufrufe ordnungsgemäß zu schließen.

Fazit

In diesem Artikel haben wir etwas über die Anweisung "+ defer " erfahren und wie sie verwendet werden kann, um sicherzustellen, dass die Systemressourcen in unserem Programm ordnungsgemäß bereinigt werden. Wenn Sie die Systemressourcen ordnungsgemäß bereinigen, wird Ihr Programm weniger Arbeitsspeicher belegen und eine bessere Leistung erzielen. Um mehr darüber zu erfahren, wo ` defer +` verwendet wird, lesen Sie den Artikel zum Umgang mit Panics oder lesen Sie unsere gesamte How To Code in Go-Serie.