Grundlegendes zu init in Go

Einführung

In Go löst die vordefinierteinit()-Funktion einen Code aus, der vor jedem anderen Teil Ihres Pakets ausgeführt werden soll. Dieser Code wird ausgeführt, sobaldpackage is imported erreicht ist, und kann verwendet werden, wenn Ihre Anwendung in einem bestimmten Status initialisiert werden muss, z. B. wenn Sie über eine bestimmte Konfiguration oder einen bestimmten Satz von Ressourcen verfügen, mit denen Ihre Anwendung gestartet werden muss. Es wird auch verwendet, wennimporting a side effect eine Technik ist, mit der der Status eines Programms durch Importieren eines bestimmten Pakets festgelegt wird. Dies wird häufig verwendet, umregisterein Paket mit einem anderen zu verbinden, um sicherzustellen, dass das Programm den richtigen Code für die Aufgabe berücksichtigt.

Obwohlinit() ein nützliches Werkzeug ist, kann es manchmal schwierig sein, Code zu lesen, da eine schwer zu findendeinit()-Instanz die Reihenfolge, in der der Code ausgeführt wird, stark beeinflusst. Aus diesem Grund ist es für Entwickler, die noch keine Erfahrung mit Go haben, wichtig, die Facetten dieser Funktion zu verstehen, damit sie beim Schreiben von Code sicherstellen können, dassinit() leserlich verwendet werden.

In diesem Lernprogramm erfahren Sie, wieinit() für die Einrichtung und Initialisierung bestimmter Paketvariablen, einmalige Berechnungen und die Registrierung eines Pakets zur Verwendung mit einem anderen Paket verwendet wird.

Voraussetzungen

Für einige Beispiele in diesem Artikel benötigen Sie:

.
├── bin
│
└── src
    └── github.com
        └── gopherguides

init() deklarieren

Jedes Mal, wenn Sie eineinit()-Funktion deklarieren, wird sie von Go vor allen anderen Elementen in diesem Paket geladen und ausgeführt. Um dies zu demonstrieren, wird in diesem Abschnitt beschrieben, wie Sie eineinit()-Funktion definieren und die Auswirkungen auf die Ausführung des Pakets zeigen.

Nehmen wir zunächst Folgendes als Beispiel für Code ohne die Funktioninit():

main.go

package main

import "fmt"

var weekday string

func main() {
    fmt.Printf("Today is %s", weekday)
}

In diesem Programm haben wir ein globalesvariable namensweekday deklariert. Standardmäßig ist der Wert vonweekday eine leere Zeichenfolge.

Lassen Sie uns diesen Code ausführen:

go run main.go

Da der Wert vonweekday leer ist, erhalten wir beim Ausführen des Programms die folgende Ausgabe:

OutputToday is

Wir können die leere Variable ausfüllen, indem wir eineinit()-Funktion einführen, die den Wert vonweekday auf den aktuellen Tag initialisiert. Fügen Siemain.go die folgenden hervorgehobenen Zeilen hinzu:

main.go

package main

import (
    "fmt"
    "time"
)

var weekday string

func init() {
    weekday = time.Now().Weekday().String()
}

func main() {
    fmt.Printf("Today is %s", weekday)
}

In diesem Code haben wir das Pakettime importiert und verwendet, um den aktuellen Wochentag (Now().Weekday().String()) abzurufen, und danninit() verwendet, umweekday mit diesem Wert zu initialisieren.

Wenn wir das Programm jetzt starten, wird der aktuelle Wochentag ausgegeben:

OutputToday is Monday

Während dies zeigt, wieinit() funktioniert, besteht ein viel typischerer Anwendungsfall fürinit() darin, es beim Importieren eines Pakets zu verwenden. Dies kann hilfreich sein, wenn Sie bestimmte Setup-Aufgaben in einem Paket ausführen müssen, bevor Sie das Paket verwenden möchten. Um dies zu demonstrieren, erstellen wir ein Programm, für das eine bestimmte Initialisierung erforderlich ist, damit das Paket wie vorgesehen funktioniert.

Pakete beim Import initialisieren

Zuerst schreiben wir einen Code, der eine zufällige Kreatur ausslice auswählt und ausdruckt. In unserem ursprünglichen Programm werden wir jedoch keineinit() verwenden. Dies zeigt besser das Problem, das wir haben, und wieinit() unser Problem lösen wird.

Erstellen Sie aus Ihremsrc/github.com/gopherguides/-Verzeichnis einen Ordner mit dem Namencreature mit dem folgenden Befehl:

mkdir creature

Erstellen Sie im Ordnercreature eine Datei mit dem Namencreature.go:

nano creature/creature.go

Fügen Sie dieser Datei den folgenden Inhalt hinzu:

creature.go

package creature

import (
    "math/rand"
)

var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}

func Random() string {
    i := rand.Intn(len(creatures))
    return creatures[i]
}

Diese Datei definiert eine Variable namenscreatures, die eine Reihe von Meerestieren als Werte initialisiert hat. Es hat auch eineexportedRandom-Funktion, die einen zufälligen Wert aus dercreatures-Variablen zurückgibt.

Speichern und beenden Sie diese Datei.

Als nächstes erstellen wir eincmd-Paket, mit dem wir unseremain()-Funktion schreiben und dascreature-Paket aufrufen.

Erstellen Sie auf derselben Dateiebene, aus der wir den Ordnercreatureerstellt haben, einen Ordnercmdmit dem folgenden Befehl:

mkdir cmd

Erstellen Sie im Ordnercmd eine Datei mit dem Namenmain.go:

nano cmd/main.go

Fügen Sie der Datei den folgenden Inhalt hinzu:

cmd/main.go

package main

import (
    "fmt"

    "github.com/gopherguides/creature"
)

func main() {
    fmt.Println(creature.Random())
    fmt.Println(creature.Random())
    fmt.Println(creature.Random())
    fmt.Println(creature.Random())
}

Hier haben wir das Paketcreature importiert und dann in der Funktionmain() die Funktioncreature.Random() verwendet, um eine zufällige Kreatur abzurufen und viermal auszudrucken.

Speichern und beenden Siemain.go.

Wir haben jetzt unser gesamtes Programm geschrieben. Bevor wir dieses Programm ausführen können, müssen wir jedoch einige Konfigurationsdateien erstellen, damit unser Code ordnungsgemäß funktioniert. Go verwendetGo Modules, um Paketabhängigkeiten für den Import von Ressourcen zu konfigurieren. Diese Module sind Konfigurationsdateien in Ihrem Paketverzeichnis, die dem Compiler mitteilen, woher Pakete importiert werden sollen. Während das Erlernen von Modulen den Rahmen dieses Artikels sprengt, können wir nur einige Konfigurationszeilen schreiben, damit dieses Beispiel lokal funktioniert.

Erstellen Sie im Verzeichniscmd eine Datei mit dem Namengo.mod:

nano cmd/go.mod

Sobald die Datei geöffnet ist, legen Sie sie in den folgenden Inhalt:

cmd/go.mod

module github.com/gopherguides/cmd
 replace github.com/gopherguides/creature => ../creature

Die erste Zeile dieser Datei teilt dem Compiler mit, dass das von uns erstellte Paketcmdtatsächlichgithub.com/gopherguides/cmd ist. Die zweite Zeile teilt dem Compiler mit, dassgithub.com/gopherguides/creature lokal auf der Festplatte im Verzeichnis../creature gefunden werden kann.

Speichern und schließen Sie die Datei. Erstellen Sie als Nächstes einego.mod-Datei im Verzeichniscreature:

nano creature/go.mod

Fügen Sie der Datei die folgende Codezeile hinzu:

creature/go.mod

 module github.com/gopherguides/creature

Dies teilt dem Compiler mit, dass das von uns erstellte Paketcreaturetatsächlich das Paketgithub.com/gopherguides/creatureist. Ohne dies würde das Paketcmdnicht wissen, von wo dieses Paket importiert werden soll.

Speichern und beenden Sie die Datei.

Sie sollten jetzt die folgende Verzeichnisstruktur und das folgende Dateilayout haben:

├── cmd
│   ├── go.mod
│   └── main.go
└── creature
    ├── go.mod
    └── creature.go

Nachdem wir alle Konfigurationen abgeschlossen haben, können wir das Programmmainmit dem folgenden Befehl ausführen:

go run cmd/main.go

Dies wird geben:

Outputjellyfish
squid
squid
dolphin

Als wir dieses Programm ausführten, erhielten wir vier Werte und druckten sie aus. Wenn wir das Programm mehrmals ausführen, werden wir feststellen, dassalways dieselbe Ausgabe erhalten und nicht wie erwartet ein zufälliges Ergebnis. Dies liegt daran, dass das PaketrandPseudozufallszahlen erstellt, die konsistent dieselbe Ausgabe für einen einzelnen Anfangszustand generieren. Um eine zufälligere Zahl zu erhalten, können wir das Paketseed oder eine sich ändernde Quelle festlegen, sodass der Anfangszustand jedes Mal anders ist, wenn wir das Programm ausführen. In Go wird häufig die aktuelle Zeit verwendet, um das Paketrandzu setzen.

Öffnen Sie diese Datei, da das Paketcreaturedie Zufallsfunktionalität verarbeiten soll:

nano creature/creature.go

Fügen Sie der Dateicreature.godie folgenden hervorgehobenen Zeilen hinzu:

creature/creature.go

package creature

import (
    "math/rand"
    "time"
)

var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}

func Random() string {
    rand.Seed(time.Now().UnixNano())
    i := rand.Intn(len(creatures))
    return creatures[i]
}

In diesem Code haben wir das Pakettimeimportiert undSeed() verwendet, um die aktuelle Zeit zu setzen. Speichern und schließen Sie die Datei.

Wenn wir nun das Programm ausführen, erhalten wir ein zufälliges Ergebnis:

go run cmd/main.go
Outputjellyfish
octopus
shark
jellyfish

Wenn Sie das Programm immer wieder ausführen, erhalten Sie weiterhin zufällige Ergebnisse. Dies ist jedoch noch keine ideale Implementierung unseres Codes, da jedes Mal, wenncreature.Random() aufgerufen wird, das Paketranddurch erneutes Aufrufen vonrand.Seed(time.Now().UnixNano()) neu gesetzt wird. Erneutes Seeding erhöht die Chance, dass die interne Uhr nicht geändert wird und denselben Anfangswert annimmt. Dies kann zu Wiederholungen des Zufallsmusters führen oder die CPU-Verarbeitungszeit verlängern, indem Sie Ihr Programm warten lassen, bis sich die Uhr ändert.

Um dies zu beheben, können wir eineinit()-Funktion verwenden. Aktualisieren wir diecreature.go-Datei:

nano creature/creature.go

Fügen Sie die folgenden Codezeilen hinzu:

creature/creature.go

package creature

import (
    "math/rand"
    "time"
)

var creatures = []string{"shark", "jellyfish", "squid", "octopus", "dolphin"}

func init() {
    rand.Seed(time.Now().UnixNano())
}

func Random() string {
    i := rand.Intn(len(creatures))
    return creatures[i]
}

Durch Hinzufügen der Funktioninit() wird dem Compiler mitgeteilt, dass beim Importieren des Paketscreature die Funktioninit() einmal ausgeführt werden soll, um einen einzelnen Startwert für die Zufallszahlengenerierung bereitzustellen. Dies stellt sicher, dass wir nicht mehr Code ausführen, als wir müssen. Wenn wir das Programm jetzt ausführen, erhalten wir weiterhin zufällige Ergebnisse:

go run cmd/main.go
Outputdolphin
squid
dolphin
octopus

In diesem Abschnitt haben wir gesehen, wie die Verwendung voninit() sicherstellen kann, dass die entsprechenden Berechnungen oder Initialisierungen durchgeführt werden, bevor das Paket verwendet wird. Als nächstes werden wir sehen, wie mehrereinit()-Anweisungen in einem Paket verwendet werden.

Mehrere Instanzen voninit()

Im Gegensatz zurmain()-Funktion, die nur einmal deklariert werden kann, kann dieinit()-Funktion in einem Paket mehrmals deklariert werden. Allerdings mehrereinit()+`s can make it difficult to know which one has priority over the others. In this section, we will show how to maintain control over multiple `+init() Anweisungen.

In den meisten Fällen werden die Funktionen voninit()in der Reihenfolge ausgeführt, in der Sie auf sie stoßen. Nehmen wir als Beispiel den folgenden Code:

main.go

package main

import "fmt"

func init() {
    fmt.Println("First init")
}

func init() {
    fmt.Println("Second init")
}

func init() {
    fmt.Println("Third init")
}

func init() {
    fmt.Println("Fourth init")
}

func main() {}

Wenn wir das Programm mit dem folgenden Befehl ausführen:

go run main.go

Wir erhalten folgende Ausgabe:

OutputFirst init
Second init
Third init
Fourth init

Beachten Sie, dass jedesinit() in der Reihenfolge ausgeführt wird, in der der Compiler darauf trifft. Es ist jedoch möglicherweise nicht immer so einfach, die Reihenfolge zu bestimmen, in der dieinit()-Funktion aufgerufen wird.

Schauen wir uns eine kompliziertere Paketstruktur an, in der mehrere Dateien mit jeweils einer eigeneninit()-Funktion deklariert sind. Um dies zu veranschaulichen, erstellen wir ein Programm, das eine Variable namensmessage gemeinsam nutzt, und drucken sie aus.

Löschen Sie die Verzeichnissecreature undcmd und deren Inhalt aus dem vorherigen Abschnitt und ersetzen Sie sie durch die folgenden Verzeichnisse und Dateistrukturen:

├── cmd
│   ├── a.go
│   ├── b.go
│   └── main.go
└── message
    └── message.go

Fügen wir nun den Inhalt jeder Datei hinzu. Fügen Sie ina.go die folgenden Zeilen hinzu:

cmd/a.go

package main

import (
    "fmt"

    "github.com/gopherguides/message"
)

func init() {
    fmt.Println("a ->", message.Message)
}

Diese Datei enthält eine einzelneinit()-Funktion, die den Wert vonmessage.Message aus demmessage-Paket druckt.

Fügen Sie als Nächstes den folgenden Inhalt zub.go hinzu:

cmd/b.go

package main

import (
    "fmt"

    "github.com/gopherguides/message"
)

func init() {
    message.Message = "Hello"
    fmt.Println("b ->", message.Message)
}

Inb.go haben wir eine einzelneinit()-Funktion, die den Wert vonmessage.Message aufHello setzt und ausdruckt.

Erstellen Sie als Nächstesmain.go, um wie folgt auszusehen:

cmd/main.go

package main

func main() {}

Diese Datei führt nichts aus, bietet jedoch einen Einstiegspunkt für die Ausführung des Programms.

Erstellen Sie abschließend die Dateimessage.gowie folgt:

message/message.go

package message

var Message string

Unsermessage-Paket deklariert die exportierteMessage-Variable.

Führen Sie den folgenden Befehl aus dem Verzeichniscmdaus, um das Programm auszuführen:

go run *.go

Da der Ordnercmdmehrere Go-Dateien enthält, aus denen das Paketmainbesteht, müssen wir dem Compiler mitteilen, dass alle Dateien.goim Ordnercmdenthalten sein sollen zusammengestellt. Die Verwendung von*.go weist den Compiler an, alle Dateien in den Ordnercmd zu laden, die mit.go enden. Wenn wir den Befehlgo run main.go ausgeben, kann das Programm nicht kompiliert werden, da der Code in den Dateiena.go undb.go nicht angezeigt wird.

Dies ergibt die folgende Ausgabe:

Outputa ->
b -> Hello

Gemäß der Go-Sprachspezifikation fürPackage Initialization werden mehrere Dateien, die in einem Paket gefunden werden, alphabetisch verarbeitet. Aus diesem Grund war der Wert beim ersten Ausdruck vonmessage.Message ausa.go leer. Der Wert wurde erst initialisiert, als die Funktioninit() vonb.go ausgeführt wurde.

Wenn wir den Dateinamen vona.go inc.go ändern würden, würden wir ein anderes Ergebnis erhalten:

Outputb -> Hello
a -> Hello

Jetzt trifft der Compiler zuerst aufb.go, und als solches wird der Wert vonmessage.Message bereits mitHello initialisiert, wenn die Funktioninit() inc.go angetroffen wird.

Dieses Verhalten kann zu einem möglichen Problem in Ihrem Code führen. In der Softwareentwicklung ist es üblich, Dateinamen zu ändern. Aufgrund der Verarbeitung voninit() kann das Ändern von Dateinamen die Reihenfolge ändern, in derinit() verarbeitet wird. Dies könnte den unerwünschten Effekt haben, dass die Ausgabe Ihres Programms geändert wird. Um ein reproduzierbares Initialisierungsverhalten zu gewährleisten, sollten Build-Systeme einem Compiler mehrere Dateien desselben Pakets in lexikalischer Dateinamenreihenfolge präsentieren. Eine Möglichkeit, um sicherzustellen, dass alle Funktionen voninit()geladen werden, besteht darin, sie alle in einer Datei zu deklarieren. Dadurch wird verhindert, dass sich die Reihenfolge ändert, auch wenn Dateinamen geändert werden.

Zusätzlich dazu, dass sich die Reihenfolge Ihrerinit()-Funktionen nicht ändert, sollten Sie auch versuchen, die Verwaltung des Status in Ihrem Paket zu vermeiden, indem Sieglobal variables verwenden, d. H. Variablen, auf die von überall im Paket aus zugegriffen werden kann. Im vorhergehenden Programm war die Variablemessage.Message für das gesamte Paket verfügbar und behielt den Status des Programms bei. Aufgrund dieses Zugriffs konnten die Anweisungen voninit()die Variable ändern und die Vorhersagbarkeit Ihres Programms beeinträchtigen. Um dies zu vermeiden, versuchen Sie, mit Variablen in kontrollierten Bereichen zu arbeiten, die so wenig Zugriff wie möglich haben, während das Programm weiterhin arbeiten kann.

Wir haben gesehen, dass Sie mehrereinit()-Deklarationen in einem einzigen Paket haben können. Dies kann jedoch zu unerwünschten Effekten führen und das Lesen oder Vorhersagen Ihres Programms erschweren. Wenn Sie mehrereinit()-Anweisungen vermeiden oder alle in einer Datei speichern, wird sichergestellt, dass sich das Verhalten Ihres Programms nicht ändert, wenn Dateien verschoben oder Namen geändert werden.

Als nächstes werden wir untersuchen, wieinit() zum Importieren mit Nebenwirkungen verwendet wird.

Verwenden voninit() für Nebenwirkungen

In Go ist es manchmal wünschenswert, ein Paket nicht wegen seines Inhalts zu importieren, sondern wegen der Nebenwirkungen, die beim Importieren des Pakets auftreten. Dies bedeutet häufig, dass der importierte Code eineinit()-Anweisung enthält, die vor dem anderen Code ausgeführt wird, sodass der Entwickler den Status ändern kann, in dem sein Programm gestartet wird. Diese Technik heißtimporting for a side effect.

Ein häufiger Anwendungsfall für den Import auf Nebenwirkungen ist die Funktionalität vonregisterin Ihrem Code, mit der ein Paket weiß, welchen Teil des Codes Ihr Programm verwenden muss. Inimage package muss beispielsweise die Funktionimage.Decode wissen, welches Bildformat sie zu dekodieren versucht (jpg,png,gif usw. .) bevor es ausgeführt werden kann. Sie können dies erreichen, indem Sie zuerst ein bestimmtes Programm importieren, das einen Nebeneffekt der Anweisunginit()hat.

Angenommen, Sie versuchen,image.Decode für eine.png-Datei mit dem folgenden Codeausschnitt zu verwenden:

Beispiel für ein Decodierungs-Snippet

. . .
func decode(reader io.Reader) image.Rectangle {
    m, _, err := image.Decode(reader)
    if err != nil {
        log.Fatal(err)
    }
    return m.Bounds()
}
. . .

Ein Programm mit diesem Code wird weiterhin kompiliert, aber jedes Mal, wenn wir versuchen, einpng-Bild zu dekodieren, wird eine Fehlermeldung angezeigt.

Um dies zu beheben, müssten wir zuerst ein Bildformat fürimage.Decode registrieren. Glücklicherweise enthält das Paketimage/pngdie folgende Anweisunginit():

image/png/reader.go

func init() {
    image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}

Wenn wir alsoimage/png in unser Decodierungs-Snippet importieren, wird die Funktionimage.RegisterFormat() inimage/png vor einem unserer Codes ausgeführt:

Beispiel für ein Decodierungs-Snippet

. . .
import _ "image/png"
. . .

func decode(reader io.Reader) image.Rectangle {
    m, _, err := image.Decode(reader)
    if err != nil {
        log.Fatal(err)
    }
    return m.Bounds()
}

Dadurch wird der Status festgelegt und registriert, dass wir diepng-Version vonimage.Decode() benötigen. Diese Registrierung erfolgt als Nebeneffekt beim Importieren vonimage/png.

Möglicherweise haben Sieblank identifier (_) vor"image/png" bemerkt. Dies ist erforderlich, da Sie mit Go keine Pakete importieren können, die nicht im gesamten Programm verwendet werden. Durch das Einfügen des Leerzeichens wird der Wert des Imports selbst verworfen, so dass nur die Nebenwirkung des Imports auftritt. Dies bedeutet, dass wir das Paketimage/pngin unserem Code zwar nie aufrufen, es aber dennoch als Nebeneffekt importieren können.

Es ist wichtig zu wissen, wann Sie ein Paket wegen seiner Nebenwirkung importieren müssen. Ohne die richtige Registrierung wird Ihr Programm wahrscheinlich kompiliert, funktioniert aber nicht richtig, wenn es ausgeführt wird. Die Pakete in der Standardbibliothek geben in ihrer Dokumentation an, dass diese Art des Imports erforderlich ist. Wenn Sie ein Paket schreiben, das wegen Nebenwirkungen importiert werden muss, sollten Sie auch sicherstellen, dass die von Ihnen verwendete Anweisunginit()dokumentiert ist, damit Benutzer, die Ihr Paket importieren, es ordnungsgemäß verwenden können.

Fazit

In diesem Tutorial haben wir erfahren, dass die Funktioninit()geladen wird, bevor der Rest des Codes in Ihrem Paket geladen wird, und dass sie bestimmte Aufgaben für ein Paket ausführen kann, z. B. das Initialisieren eines gewünschten Status. Wir haben auch erfahren, dass die Reihenfolge, in der der Compiler mehrereinit()-Anweisungen ausführt, von der Reihenfolge abhängt, in der der Compiler die Quelldateien lädt. Wenn Sie mehr überinit() erfahren möchten, lesen Sie die offiziellenGolang documentation oder lesen Siethe discussion in the Go community about the function durch.

Weitere Informationen zu Funktionen finden Sie in unserem Artikel zuHow To Define and Call Functions in Gooder zuthe entire How To Code in Go series.