Einführung
Bei der Erstellung eines Pakets in Go besteht das Hauptziel in der Regel darin, das Paket für andere Entwickler zugänglich zu machen, sei es in höheren Paketen oder ganzen Programmen. Durch Importieren des Pakets kann Ihr Code als Baustein für andere, komplexere Werkzeuge dienen. Allerdings sind nur bestimmte Pakete zum Importieren verfügbar. Dies wird durch die Sichtbarkeit des Pakets bestimmt.
Sichtbarkeit bezieht sich in diesem Zusammenhang auf den Dateiraum, aus dem ein Paket oder ein anderes Konstrukt referenziert werden kann. Wenn wir beispielsweise eine Variable in einer Funktion definieren, ist die Sichtbarkeit (der Gültigkeitsbereich) dieser Variable nur innerhalb der Funktion, in der sie definiert wurde. Ebenso können Sie, wenn Sie eine Variable in einem Paket definieren, sie nur für dieses Paket sichtbar machen oder auch außerhalb des Pakets sichtbar machen.
Sorgfältiges Steuern der Paketsichtbarkeit ist wichtig, wenn man ergonomischen Code schreibt, insbesondere wenn man zukünftige Änderungen berücksichtigt, die man an seinem Paket vornehmen möchte. Wenn man einen Fehler beheben, die Leistung verbessern oder die Funktionalität ändern muss, sollte man dies auf eine Weise tun, die den Code von Benutzern des Pakets nicht beeinträchtigt. Eine Möglichkeit, die Auswirkungen von Änderungen zu minimieren, besteht darin, nur den Teilen des Pakets Zugriff zu gewähren, die für dessen ordnungsgemäße Verwendung erforderlich sind. Durch die Beschränkung des Zugriffs kann man interne Änderungen am Paket vornehmen, ohne dass dies wahrscheinlich Auswirkungen auf die Verwendung des Pakets durch andere Entwickler hat.
In diesem Artikel erfahren Sie, wie Sie die Paketsichtbarkeit steuern und wie Sie Teile Ihres Codes schützen, die nur innerhalb Ihres Pakets verwendet werden sollen. Dazu erstellen wir einen einfachen Logger zum Protokollieren und Debuggen von Nachrichten, wobei Pakete mit unterschiedlichen Sichtbarkeitsgraden von Elementen verwendet werden.
Voraussetzungen
Um die Beispiele in diesem Artikel zu befolgen, benötigen Sie:
- Ein eingerichtetes Go-Arbeitsbereich, der durch Befolgen von How To Install Go and Set Up a Local Programming Environment eingerichtet wurde. Dieses Tutorial verwendet die folgende Dateistruktur:
.
├── bin
│
└── src
└── github.com
└── gopherguides
Exportierte und Nicht-Exportierte Elemente
Im Gegensatz zu anderen Programmiersprachen wie Java und Python, die Zugriffsmodifikatoren wie public
, private
oder protected
verwenden, um den Gültigkeitsbereich festzulegen, bestimmt Go, ob ein Element exportiert
oder nicht-exportiert
ist, anhand dessen, wie es deklariert wird. Das Exportieren eines Elements macht es sichtbar
außerhalb des aktuellen Pakets. Wenn es nicht exportiert wird, ist es nur innerhalb des Pakets sichtbar und verwendbar, in dem es definiert wurde.
Diese externe Sichtbarkeit wird durch die Verwendung eines Großbuchstabens am Anfang des deklarierten Elements gesteuert. Alle Deklarationen wie Typen
, Variablen
, Konstanten
, Funktionen
usw., die mit einem Großbuchstaben beginnen, sind außerhalb des aktuellen Pakets sichtbar.
Betrachten wir den folgenden Code, wobei wir besonders auf die Groß- und Kleinschreibung achten:
package greet
import "fmt"
var Greeting string
func Hello(name string) string {
return fmt.Sprintf(Greeting, name)
}
Dieser Code deklariert, dass er sich im greet
Paket befindet. Anschließend werden zwei Symbole deklariert, eine Variable namens Greeting
und eine Funktion namens Hello
. Da beide mit einem Großbuchstaben beginnen, sind sie beide exportiert
und für jedes externe Programm verfügbar. Wie bereits erwähnt, ermöglicht die Erstellung eines Pakets, das den Zugriff einschränkt, ein besseres API-Design und erleichtert es, Ihr Paket intern zu aktualisieren, ohne dass Code, der von Ihrem Paket abhängt, beschädigt wird.
Definition der Paketsichtbarkeit
Um genauer zu betrachten, wie die Paketsichtbarkeit in einem Programm funktioniert, erstellen wir ein logging
Paket, wobei wir im Auge behalten, was wir außerhalb unseres Pakets sichtbar machen möchten und was nicht. Dieses Logging-Paket ist verantwortlich für das Protokollieren aller Programmnachrichten auf der Konsole. Es wird auch betrachten, auf welcher Ebene wir protokollieren. Eine Ebene beschreibt die Art des Logs und wird einer von drei Status sein: info
, warning
oder error
.
Zuerst erstellen wir innerhalb des src
Verzeichnisses ein Verzeichnis namens logging
, um unsere Logging-Dateien darin zu speichern:
Gehen Sie als Nächstes in dieses Verzeichnis:
Erstellen Sie dann mit einem Editor wie nano eine Datei namens logging.go
:
Fügen Sie den folgenden Code in die gerade erstellte Datei logging.go
ein:
Die erste Zeile dieses Codes deklariert ein Paket namens logging
. In diesem Paket gibt es zwei exportierte
Funktionen: Debug
und Log
. Diese Funktionen können von jedem anderen Paket aufgerufen werden, das das logging
-Paket importiert. Es gibt auch eine private Variable namens debug
. Diese Variable ist nur innerhalb des logging
-Pakets zugänglich. Es ist wichtig zu beachten, dass die Funktion Debug
und die Variable debug
zwar gleich geschrieben sind, die Funktion jedoch groß geschrieben wird und die Variable nicht. Dies macht sie zu unterschiedlichen Deklarationen mit verschiedenen Gültigkeitsbereichen.
Speichern und beenden Sie die Datei.
Um dieses Paket in anderen Bereichen unseres Codes zu verwenden, können wir es importieren
in ein neues Paket. Wir werden dieses neue Paket erstellen, aber wir benötigen zunächst ein neues Verzeichnis, um diese Quelldateien darin zu speichern.
Lassen Sie uns aus dem logging
-Verzeichnis herausgehen, ein neues Verzeichnis namens cmd
erstellen und in dieses neue Verzeichnis wechseln:
Erstellen Sie eine Datei namens main.go
im neu erstellten cmd
-Verzeichnis:
Jetzt können wir den folgenden Code hinzufügen:
Wir haben jetzt unser gesamtes Programm geschrieben. Bevor wir dieses Programm jedoch ausführen können, müssen wir auch einige Konfigurationsdateien für unseren Code erstellen, damit er ordnungsgemäß funktioniert. Go verwendet Go Modules zur Konfiguration von Paketabhängigkeiten für den Import von Ressourcen. Go-Module sind Konfigurationsdateien, die in Ihrem Paketverzeichnis abgelegt werden und dem Compiler mitteilen, von wo Pakete importiert werden sollen. Obwohl das Erlernen von Modulen den Rahmen dieses Artikels sprengt, können wir ein paar Zeilen Konfiguration schreiben, um dieses Beispiel lokal zum Laufen zu bringen.
Öffnen Sie die folgende go.mod
-Datei im cmd
-Verzeichnis:
Fügen Sie dann den folgenden Inhalt in die Datei ein:
module github.com/gopherguides/cmd
replace github.com/gopherguides/logging => ../logging
Die erste Zeile dieser Datei teilt dem Compiler mit, dass das cmd
-Paket einen Dateipfad von github.com/gopherguides/cmd
hat. Die zweite Zeile teilt dem Compiler mit, dass das Paket github.com/gopherguides/logging
lokal auf der Festplatte im Verzeichnis ../logging
gefunden werden kann.
Wir benötigen auch eine go.mod
-Datei für unser logging
-Paket. Gehen wir zurück in das logging
-Verzeichnis und erstellen eine go.mod
-Datei:
Fügen Sie den folgenden Inhalt zur Datei hinzu:
module github.com/gopherguides/logging
Dies teilt dem Compiler mit, dass das von uns erstellte logging
-Paket tatsächlich das Paket github.com/gopherguides/logging
ist. Dies ermöglicht es, das Paket in unserem main
-Paket mit der folgenden Zeile zu importieren, die wir zuvor geschrieben haben:
package main
import "github.com/gopherguides/logging"
func main() {
logging.Debug(true)
logging.Log("This is a debug statement...")
}
Sie sollten nun die folgende Verzeichnisstruktur und Dateianordnung haben:
├── cmd
│ ├── go.mod
│ └── main.go
└── logging
├── go.mod
└── logging.go
Nachdem wir nun alle Konfigurationen abgeschlossen haben, können wir das main
-Programm aus dem cmd
-Paket mit den folgenden Befehlen ausführen:
Sie erhalten eine Ausgabe ähnlich der folgenden:
Output2019-08-28T11:36:09-05:00 This is a debug statement...
Das Programm gibt die aktuelle Zeit im RFC 3339-Format aus, gefolgt von der Aussage, die wir an den Logger gesendet haben. RFC 3339 ist ein Zeitformat, das zur Darstellung von Zeit im Internet entwickelt wurde und häufig in Protokolldateien verwendet wird.
Da die Funktionen Debug
und Log
aus dem Logging-Paket exportiert werden, können wir sie in unserem main
-Paket verwenden. Allerdings wird die Variable debug
im logging
-Paket nicht exportiert. Der Versuch, eine nicht exportierte Deklaration zu referenzieren, führt zu einem Compile-Time-Fehler.
Fügen Sie die folgende markierte Zeile zu main.go
hinzu:
package main
import "github.com/gopherguides/logging"
func main() {
logging.Debug(true)
logging.Log("This is a debug statement...")
fmt.Println(logging.debug)
}
Speichern und führen Sie die Datei aus. Sie erhalten einen Fehler ähnlich dem folgenden:
Output. . .
./main.go:10:14: cannot refer to unexported name logging.debug
Nachdem wir gesehen haben, wie exportierte
und nicht exportierte
Elemente in Paketen sich verhalten, werden wir als Nächstes untersuchen, wie Felder
und Methoden
aus Strukturen
exportiert werden können.
Sichtbarkeit innerhalb von Strukturen
Während das Sichtbarkeitsschema im Logger, den wir im letzten Abschnitt erstellt haben, für einfache Programme funktionieren mag, teilt es zu viel Zustand, um innerhalb mehrerer Pakete nützlich zu sein. Dies liegt daran, dass die exportierten Variablen von mehreren Paketen zugänglich sind, die die Variablen in widersprüchliche Zustände ändern könnten. Die Möglichkeit, dass der Zustand Ihres Pakets auf diese Weise geändert wird, erschwert es, das Verhalten Ihres Programms vorherzusagen. Bei dem aktuellen Design könnte beispielsweise ein Paket die Variable Debug
auf true
setzen und ein anderes könnte sie in derselben Instanz auf false
setzen. Dies würde ein Problem schaffen, da beide Pakete, die das logging
-Paket importieren, betroffen sind.
Wir können den Logger isolieren, indem wir eine Struktur erstellen und dann Methoden daran hängen. Dies ermöglicht uns, eine Instanz
eines Loggers zu erstellen, die unabhängig in jedem Paket verwendet werden kann, das ihn nutzt.
Ändern Sie das logging
-Paket wie folgt, um den Code umzugestalten und den Logger zu isolieren:
In diesem Code haben wir eine Logger
-Struktur erstellt. Diese Struktur wird unseren unexportierten Zustand beinhalten, einschließlich des Zeitformats zum Ausdrucken und der debug
-Variablen, die auf true
oder false
gesetzt ist. Die New
-Funktion setzt den Anfangszustand, um den Logger zu erstellen, wie zum Beispiel das Zeitformat und den Debug-Zustand. Dann speichert sie die von uns übergebenen Werte intern in den unexportierten Variablen timeFormat
und debug
. Wir haben auch eine Methode namens Log
auf dem Logger
-Typ erstellt, die eine Aussage übernimmt, die wir ausdrucken möchten. Innerhalb der Log
-Methode gibt es einen Verweis auf die lokale Methodenvariable l
, um Zugriff auf ihre internen Felder wie l.timeFormat
und l.debug
zu erhalten.
Dieser Ansatz ermöglicht es uns, einen Logger
in vielen verschiedenen Paketen zu erstellen und unabhängig davon zu verwenden, wie die anderen Pakete ihn verwenden.
Um ihn in einem anderen Paket zu verwenden, ändern wir cmd/main.go
wie folgt:
Die Ausführung dieses Programms liefert folgende Ausgabe:
Output2019-08-28T11:56:49-05:00 This is a debug statement...
In diesem Code haben wir eine Instanz des Loggers erstellt, indem wir die exportierte Funktion New
aufgerufen haben. Wir haben den Verweis auf diese Instanz in der logger
-Variable gespeichert. Jetzt können wir logging.Log
aufrufen, um Aussagen auszugeben.
Wenn wir versuchen, ein unexportiertes Feld aus dem Logger
wie das timeFormat
-Feld zu referenzieren, erhalten wir einen Compile-Zeit-Fehler. Probieren Sie die folgende markierte Zeile hinzuzufügen und cmd/main.go
auszuführen:
package main
import (
"time"
"github.com/gopherguides/logging"
)
func main() {
logger := logging.New(time.RFC3339, true)
logger.Log("This is a debug statement...")
fmt.Println(logger.timeFormat)
}
Dies wird den folgenden Fehler verursachen:
Output. . .
cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)
Der Compiler erkennt, dass logger.timeFormat
nicht exportiert ist und daher nicht aus dem logging
-Paket abgerufen werden kann.
Sichtbarkeit innerhalb von Methoden
Ähnlich wie bei Strukturfeldern können auch Methoden exportiert oder nicht exportiert sein.
Um dies zu veranschaulichen, fügen wir unserem Logger stufenweise Protokollierung hinzu. Stufenweise Protokollierung ist eine Methode zur Kategorisierung Ihrer Logs, damit Sie Ihre Logs nach bestimmten Ereignistypen durchsuchen können. Die Stufen, die wir in unseren Logger einbauen werden, sind:
-
Die
info
-Stufe, die Informationsart-Ereignisse repräsentiert, die den Benutzer über eine Aktion informieren, wie z.B.Programm gestartet
oderE-Mail gesendet
. Diese helfen uns beim Debuggen und Verfolgen von Teilen unseres Programms, um zu prüfen, ob das erwartete Verhalten auftritt. -
Die
warning
-Stufe. Diese Art von Ereignissen identifiziert, wenn etwas Unerwartetes passiert, was kein Fehler ist, wie z.B.E-Mail konnte nicht gesendet werden, erneuter Versuch
. Sie helfen uns zu sehen, welche Teile unseres Programms nicht so reibungslos ablaufen, wie wir es erwartet haben. -
Die
error
Ebene, die bedeutet, dass das Programm auf einen Problem stößt, wie z. B.Datei nicht gefunden
. Dies wird oft dazu führen, dass die Programmoperation fehlschlägt.
Sie können auch den Wunsch haben, bestimmte Level der Protokollierung ein- oder auszuschalten, insbesondere wenn Ihr Programm nicht wie erwartet funktioniert und Sie das Debugging des Programms unterstützen möchten. Wir schaffen diese Funktionalität, indem wir das Programm so ändern, dass es, wenn debug
als true
festgelegt ist, alle Level der Nachrichten protokolliert. Ansonsten, wenn es false
ist, protokollt es nur Fehlernachrichten.
Den folgenden Code in logging/logging.go
ändern:
In diesem Beispiel haben wir eine neue Argumentvariable für die Log
Methode hinzugefügt. Wir können jetzt die level
der Log-Nachricht übergeben. Die Log
Methode bestimmt, was für einen Level der Nachricht ist. Wenn es ein info
oder warning
ist und die debug
Feld ist true
, dann schreibt sie die Nachricht. Ansonsten ignoriere sie die Nachricht. Falls es jede andere Ebene ist, wie z. B. error
, schreibt sie die Nachricht unabhängig davon.
Die meisten Logik zur Bestimmung, ob die Nachricht ausgegeben wird, befinden sich in der Methode Log
. Wir haben auch eine nicht exportierte Methode namens write
eingeführt. Die Methode write
ist es, die die Log-Nachricht tatsächlich ausgibt.
Wir können diese stufenbasierte Protokollierung nun in unserem anderen Paket verwenden, indem wir cmd/main.go
wie folgt ändern:
Das Ausführen dieses Codes liefert:
Output[info] 2019-09-23T20:53:38Z starting up service
[warning] 2019-09-23T20:53:38Z no tasks found
[error] 2019-09-23T20:53:38Z exiting: no work performed
In diesem Beispiel hat cmd/main.go
die exportierte Methode Log
erfolgreich verwendet.
Wir können nun die level
jeder Nachricht übergeben, indem wir debug
auf false
setzen:
Jetzt werden wir feststellen, dass nur Nachrichten des error
-Levels ausgegeben werden:
Output[error] 2019-08-28T13:58:52-05:00 exiting: no work performed
Wenn wir versuchen, die Methode write
von außerhalb des logging
-Pakets aufzurufen, erhalten wir einen Compile-Fehler:
package main
import (
"time"
"github.com/gopherguides/logging"
)
func main() {
logger := logging.New(time.RFC3339, true)
logger.Log("info", "starting up service")
logger.Log("warning", "no tasks found")
logger.Log("error", "exiting: no work performed")
logger.write("error", "log this message...")
}
Outputcmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or method logging.(*Logger).write)
Wenn der Compiler erkennt, dass Sie versuchen, auf etwas aus einem anderen Paket zuzugreifen, das mit einem Kleinbuchstaben beginnt, weiß er, dass es nicht exportiert ist, und wirft daher einen Compiler-Fehler.
Der Logger in diesem Tutorial zeigt, wie wir Code schreiben können, der nur die Teile offenlegt, die wir anderen Paketen zur Verfügung stellen möchten. Da wir kontrollieren, welche Teile des Pakets außerhalb des Pakets sichtbar sind, können wir jetzt zukünftige Änderungen vornehmen, ohne Code, der von unserem Paket abhängt, zu beeinflussen. Wenn wir beispielsweise nur info
-Level-Nachrichten ausschalten möchten, wenn debug
falsch ist, könnten Sie diese Änderung vornehmen, ohne andere Teile Ihrer API zu beeinflussen. Wir könnten auch sicher Änderungen an der Protokollnachricht vornehmen, um weitere Informationen einzubeziehen, wie z.B. das Verzeichnis, in dem das Programm ausgeführt wurde.
Schlussfolgerung
Dieser Artikel zeigte, wie man Code zwischen Paketen teilt und gleichzeitig die Implementierungsdetails Ihres Pakets schützt. Dies ermöglicht es Ihnen, eine einfache API zu exportieren, die selten für die Abwärtskompatibilität geändert wird, aber es erlaubt auch private Änderungen in Ihrem Paket, um es bei Bedarf für die Zukunft besser funktionieren zu lassen. Dies gilt als Best Practice bei der Erstellung von Paketen und ihren entsprechenden APIs.
Um mehr über Pakete in Go zu erfahren, schau dir unsere Artikel Pakete in Go importieren und Pakete in Go schreiben an oder entdecke unsere gesamte Serie Programmieren in Go lernen.
Source:
https://www.digitalocean.com/community/tutorials/understanding-package-visibility-in-go