Grundlegendes zur Paketsichtbarkeit in Go

Einführung

Wenn Sie ein package in Go erstellen, besteht das Endziel normalerweise darin, das Paket für andere Entwickler zugänglich zu machen. entweder in Paketen höherer Ordnung oder ganzen Programmen. Unter importing the package kann Ihr Code als Baustein für andere, komplexere Tools dienen. Es sind jedoch nur bestimmte Pakete zum Importieren verfügbar. Dies wird durch die Sichtbarkeit des Pakets bestimmt.

Sichtbarkeit bedeutet in diesem Zusammenhang den Dateibereich, aus dem auf ein Paket oder ein anderes Konstrukt verwiesen werden kann. Wenn wir beispielsweise eine Variable in einer Funktion definieren, liegt die Sichtbarkeit (der Gültigkeitsbereich) dieser Variablen nur innerhalb der Funktion, in der sie definiert wurde. Wenn Sie eine Variable in einem Paket definieren, können Sie sie auch nur für dieses Paket sichtbar machen oder zulassen, dass sie auch außerhalb des Pakets sichtbar ist.

Die sorgfältige Kontrolle der Sichtbarkeit von Paketen ist wichtig, wenn Sie ergonomischen Code schreiben, insbesondere wenn Sie zukünftige Änderungen berücksichtigen, die Sie möglicherweise an Ihrem Paket vornehmen möchten. Wenn Sie einen Fehler beheben, die Leistung verbessern oder die Funktionalität ändern müssen, möchten Sie die Änderung so vornehmen, dass der Code von niemandem, der Ihr Paket verwendet, verletzt wird. Eine Möglichkeit, Änderungen so gering wie möglich zu halten, besteht darin, nur den Zugriff auf die Teile Ihres Pakets zuzulassen, die für eine ordnungsgemäße Verwendung erforderlich sind. Indem Sie den Zugriff einschränken, können Sie Änderungen an Ihrem Paket intern vornehmen, ohne dass sich dies auf die Verwendung Ihres Pakets durch andere Entwickler auswirkt.

In diesem Artikel erfahren Sie, wie Sie die Sichtbarkeit von Paketen steuern und Teile Ihres Codes schützen, die nur in Ihrem Paket verwendet werden sollten. Zu diesem Zweck erstellen wir einen grundlegenden Protokollierer zum Protokollieren und Debuggen von Nachrichten mit Paketen mit unterschiedlichem Grad an Sichtbarkeit der Elemente.

Voraussetzungen

Um den Beispielen in diesem Artikel zu folgen, benötigen Sie:

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

Exportierte und nicht exportierte Artikel

Im Gegensatz zu anderen Programmiersprachen wie Java und Python, die access-Modifikatoren wie + public +, + private + verwenden , oder "+ protected ", um den Gültigkeitsbereich anzugeben. Mit "Go" wird bestimmt, ob ein Element " exportiert" und "+ nicht exportiert" über die Deklaration bestimmt wird. Wenn Sie in diesem Fall einen Artikel exportieren, wird dieser außerhalb des aktuellen Pakets "+ sichtbar +". Wenn es nicht exportiert wird, ist es nur in dem Paket sichtbar und verwendbar, in dem es definiert wurde.

Diese Sichtbarkeit nach außen wird durch Großschreibung des ersten Buchstabens des deklarierten Elements gesteuert. Alle Deklarationen, wie z. B. "+ Types", "+ Variables", "+ Constants", "+ Functions" usw., die mit einem Großbuchstaben beginnen, werden außerhalb des aktuellen Pakets angezeigt.

Sehen wir uns den folgenden Code an und achten Sie dabei genau auf die Groß- und Kleinschreibung:

greet.go

package greet

import "fmt"

var reeting string

func ello(name string) string {
   return fmt.Sprintf(Greeting, name)
}

Dieser Code deklariert, dass es sich um das Paket "+ greet " handelt. Es deklariert dann zwei Symbole, eine Variable mit dem Namen " Begrüßung " und eine Funktion mit dem Namen " Hallo ". Da beide mit einem Großbuchstaben beginnen, sind 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 die interne Aktualisierung Ihres Pakets, ohne dass der Code eines anderen, der von Ihrem Paket abhängt, beschädigt wird.

Sichtbarkeit von Paketen definieren

Um einen genaueren Blick auf die Funktionsweise der Paketsichtbarkeit in einem Programm zu werfen, erstellen wir ein "+ logging " - Paket, wobei wir berücksichtigen, was außerhalb unseres Pakets sichtbar sein soll und was nicht. Dieses Protokollierungspaket ist für die Protokollierung aller Programmnachrichten in der Konsole verantwortlich. Es wird auch geprüft, auf welcher Ebene wir uns anmelden. Eine Ebene beschreibt den Typ des Protokolls und wird einen von drei Status haben: " info ", " warning " oder " error +".

Erstellen Sie zunächst in Ihrem "+ src " - Verzeichnis ein Verzeichnis mit dem Namen " logging +", um unsere Protokolldateien in folgende Ordner zu verschieben:

mkdir logging

Als nächstes in dieses Verzeichnis verschieben:

cd logging

Erstellen Sie dann mit einem Editor wie nano eine Datei mit dem Namen "+ logging.go +":

nano logging.go

Fügen Sie den folgenden Code in die soeben erstellte Datei "+ logging.go +" ein:

logging / logging.go

package logging

import (
   "fmt"
   "time"
)

var debug bool

func Debug(b bool) {
   debug = b
}

func Log(statement string) {
   if !debug {
       return
   }

   fmt.Printf("%s %s\n", time.Now().Format(time.RFC3339), statement)
}

Die erste Zeile dieses Codes deklarierte ein Paket mit dem Namen "+ logging ". In diesem Paket gibt es zwei " exportierte" Funktionen: "+ Debug" und "+ Log". Diese Funktionen können von jedem anderen Paket aufgerufen werden, das das Paket + logging + importiert. Es gibt auch eine private Variable mit dem Namen "+ debug ". Auf diese Variable kann nur innerhalb des Pakets " logging " zugegriffen werden. Es ist wichtig zu beachten, dass während die Funktion " Debug " und die Variable " Debug +" beide die gleiche Schreibweise haben, die Funktion groß geschrieben wird und die Variable nicht. Dies macht sie zu unterschiedlichen Deklarationen mit unterschiedlichen Geltungsbereichen.

Speichern und beenden Sie die Datei.

Um dieses Paket in anderen Bereichen unseres Codes zu verwenden, können wir https://www.digitalocean.com/community/tutorials/importing-packages-in-go [+ importieren + es in ein neues Paket]. Wir erstellen dieses neue Paket, benötigen jedoch ein neues Verzeichnis, in dem diese Quelldateien zuerst gespeichert werden.

Verlassen wir das Verzeichnis "+ logging ", erstellen ein neues Verzeichnis mit dem Namen " cmd +" und verschieben es in dieses neue Verzeichnis:

cd ..
mkdir cmd
cd cmd

Erstellen Sie eine Datei mit dem Namen "+ main.go " in dem soeben erstellten " cmd +" -Verzeichnis:

nano main.go

Jetzt können wir den folgenden Code hinzufügen:

cmd / main.go

package main

import "github.com/gopherguides/logging"

func main() {
   logging.Debug(true)

   logging.Log("This is a debug statement...")
}

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 verwendet Go Modules, um Paketabhängigkeiten für das Importieren von Ressourcen zu konfigurieren. Go-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.

Öffne die folgende + go.mod + Datei im + cmd + Verzeichnis:

nano go.mod

Fügen Sie dann den folgenden Inhalt in die Datei ein:

go.mod

module github.com/gopherguides/cmd

replace github.com/gopherguides/logging => ../logging

Die erste Zeile dieser Datei teilt dem Compiler mit, dass das "+ cmd " - Paket den Dateipfad " github.com / gopherguides / cmd " hat. Die zweite Zeile teilt dem Compiler mit, dass sich das Paket " github.com / gopherguides / logging " lokal auf der Festplatte im Verzeichnis " .. / logging +" befindet.

Wir benötigen außerdem eine "+ go.mod " - Datei für unser " logging " - Paket. Gehen wir zurück in das Verzeichnis " logging " und erstellen eine " go.mod +" -Datei:

cd ../logging
nano go.mod

Fügen Sie der Datei den folgenden Inhalt hinzu:

go.mod

module github.com/gopherguides/logging

Dies teilt dem Compiler mit, dass das von uns erstellte Paket "+ logging " das Paket " github.com / gopherguides / logging " ist. Dadurch ist es möglich, das Paket in unser " main +" - Paket mit der folgenden Zeile zu importieren, die wir zuvor geschrieben haben:

cmd / main.go

package main



func main() {
   logging.Debug(true)

   logging.Log("This is a debug statement...")
}

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

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

Nachdem wir die gesamte Konfiguration abgeschlossen haben, können wir das Programm "+ main " aus dem Paket " cmd +" mit den folgenden Befehlen ausführen:

cd ../cmd
go run main.go

Sie erhalten eine Ausgabe ähnlich der folgenden:

Output This is a debug statement...

Das Programm druckt die aktuelle Uhrzeit im RFC 3339-Format aus, gefolgt von der Anweisung, die wir an den Logger gesendet haben. RFC 3339 ist ein Zeitformat, das für die Darstellung der Uhrzeit im Internet entwickelt wurde und häufig in Protokolldateien verwendet wird.

Da die Funktionen "+ Debug " und " Log " aus dem Protokollierungspaket exportiert werden, können wir sie in unserem " main " - Paket verwenden. Die Variable " debug " im Paket " logging +" wird jedoch nicht exportiert. Der Versuch, auf eine nicht exportierte Deklaration zu verweisen, führt zu einem Fehler beim Kompilieren.

Fügen Sie die folgende hervorgehobene Zeile zu + main.go + hinzu:

cmd / main.go

package main

import "github.com/gopherguides/logging"

func main() {
   logging.Debug(true)

   logging.Log("This is a debug statement...")


}

Speichern Sie die Datei und führen Sie sie aus. Sie erhalten eine Fehlermeldung ähnlich der folgenden:

Output. . .
./main.go:10:14: cannot refer to unexported name logging.debug

Nachdem wir nun gesehen haben, wie sich exportierte und nicht exportierte Elemente in Paketen verhalten, werden wir uns als nächstes ansehen, wie Felder und Methoden aus Strukturen exportiert werden können.

Sichtbarkeit innerhalb von Strukturen

Während das Sichtbarkeitsschema in der im letzten Abschnitt erstellten Protokollfunktion möglicherweise für einfache Programme funktioniert, weist es einen zu großen Status auf, um in mehreren Paketen verwendet werden zu können. Dies liegt daran, dass auf die exportierten Variablen mehrere Pakete zugreifen können, die die Variablen in widersprüchliche Zustände ändern können. Wenn Sie zulassen, dass der Status Ihres Pakets auf diese Weise geändert wird, können Sie nur schwer vorhersagen, wie sich Ihr Programm verhält. Beim aktuellen Design könnte beispielsweise ein Paket die Variable "+ Debug " auf " true " und ein anderes auf " false " setzen. Dies würde ein Problem verursachen, da beide Pakete, die das " logging +" - Paket importieren, betroffen sind.

Wir können den Logger isolieren, indem wir eine Struktur erstellen und dann Methoden davon abhängen. Auf diese Weise können wir eine "+" - Instanz eines Loggers erstellen, die unabhängig in jedem Paket verwendet wird, das diesen verwendet.

Ändern Sie das Paket "+ logging +" in das folgende, um den Code umzugestalten und den Logger zu isolieren:

logging / logging.go

package logging

import (
   "fmt"
   "time"
)

type Logger struct {
   timeFormat string
   debug      bool
}

func New(timeFormat string, debug bool) *Logger {
   return &Logger{
       timeFormat: timeFormat,
       debug:      debug,
   }
}

func (l *Logger) Log(s string) {
   if !l.debug {
       return
   }
   fmt.Printf("%s %s\n", time.Now().Format(l.timeFormat), s)
}

In diesem Code haben wir eine "+ Logger " - Struktur erstellt. Diese Struktur enthält unseren nicht exportierten Status, einschließlich des auszudruckenden Zeitformats und der Variableneinstellung " debug " von " true " oder " false ". Die Funktion ` Neu ` legt den Anfangszustand fest, mit dem der Logger erstellt werden soll, z. B. das Zeitformat und der Debug-Zustand. Es speichert dann die Werte, die wir ihm intern gegeben haben, in den nicht exportierten Variablen " timeFormat " und " debug ". Wir haben auch eine Methode mit dem Namen " Log " für den Typ " Logger " erstellt, die eine Anweisung übernimmt, die wir ausdrucken möchten. In der Methode " Log " befindet sich eine Referenz auf die lokale Methodenvariable " l ", um wieder auf die internen Felder wie " l.timeFormat " und " l.debug +" zuzugreifen.

Mit diesem Ansatz können wir einen "+ Logger +" in vielen verschiedenen Paketen erstellen und unabhängig davon verwenden, wie die anderen Pakete ihn verwenden.

Um es in einem anderen Paket zu verwenden, ändern wir "+ cmd / main.go +" so, dass es wie folgt aussieht:

cmd / main.go

package main

import (
   "time"

   "github.com/gopherguides/logging"
)

func main() {
   logger := logging.New(time.RFC3339, true)

   logger.Log("This is a debug statement...")
}

Wenn Sie dieses Programm ausführen, erhalten Sie die folgende Ausgabe:

Output 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 Variablen "+ logger " gespeichert. Wir können jetzt " logging.Log +" aufrufen, um Anweisungen auszudrucken.

Wenn wir versuchen, auf ein nicht exportiertes Feld aus dem Feld "+ Logger " zu verweisen, z. Fügen Sie die folgende hervorgehobene Zeile hinzu und führen Sie " cmd / main.go +" aus:

cmd / main.go

package main

import (
   "time"

   "github.com/gopherguides/logging"
)

func main() {
   logger := logging.New(time.RFC3339, true)

   logger.Log("This is a debug statement...")


}

Dies führt zu folgendem Fehler:

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 wird und daher nicht aus dem " logging +" - Paket abgerufen werden kann.

Sichtbarkeit innerhalb von Methoden

Ebenso wie Strukturfelder können auch Methoden exportiert oder nicht exportiert werden.

Um dies zu veranschaulichen, fügen wir unserem Logger leveled logging hinzu. Die abgestufte Protokollierung dient zum Kategorisieren Ihrer Protokolle, sodass Sie in Ihren Protokollen nach bestimmten Ereignistypen suchen können. Die Ebenen, die wir in unseren Logger einfügen, sind:

  • Die Ebene "+ info ", die Ereignisse vom Typ "Information" darstellt, die den Benutzer über eine Aktion informieren, z. B. " Programm gestartet " oder " E-Mail gesendet +". Diese helfen uns, Teile unseres Programms zu debuggen und nachzuverfolgen, um festzustellen, ob das erwartete Verhalten vorliegt.

  • Die Stufe "+ Warnung ". Diese Arten von Ereignissen identifizieren, wenn etwas Unerwartetes passiert, das kein Fehler ist, wie " E-Mail konnte nicht gesendet werden, + wird wiederholt". Sie helfen uns, Teile unseres Programms zu erkennen, die nicht so reibungslos verlaufen, wie wir es erwartet hatten.

  • Die Fehlerstufe "", dh, das Programm hat ein Problem festgestellt, z. B. " Datei nicht gefunden". Dies führt häufig dazu, dass die Ausführung des Programms fehlschlägt.

Möglicherweise möchten Sie auch bestimmte Protokollierungsebenen aktivieren und deaktivieren, insbesondere wenn Ihr Programm nicht die erwartete Leistung erbringt und Sie das Programm debuggen möchten. Wir werden diese Funktionalität hinzufügen, indem wir das Programm so ändern, dass, wenn "+ debug " auf " true " gesetzt ist, alle Nachrichtenebenen gedruckt werden. Andernfalls werden bei " false +" nur Fehlermeldungen ausgegeben.

Fügen Sie eine abgestufte Protokollierung hinzu, indem Sie die folgenden Änderungen an "+ logging / logging.go +" vornehmen:

logging / logging.go

package logging

import (
   "fmt"
   "strings"
   "time"
)

type Logger struct {
   timeFormat string
   debug      bool
}

func New(timeFormat string, debug bool) *Logger {
   return &Logger{
       timeFormat: timeFormat,
       debug:      debug,
   }
}

func (l *Logger) Log(level string, s string) {
   level = strings.ToLower(level)
   switch level {
   case "info", "warning":
       if l.debug {
           l.write(level, s)
       }
   default:
       l.write(level, s)
   }
}

func (l *Logger) write(level string, s string) {
   fmt.Printf("[%s] %s %s\n", level, time.Now().Format(l.timeFormat), s)
}

In diesem Beispiel haben wir der Methode "+ Log " ein neues Argument hinzugefügt. Wir können jetzt das " level " der Protokollnachricht übergeben. Die Methode " Log " bestimmt, auf welcher Ebene die Nachricht angezeigt wird. Wenn es sich um eine " info" - oder "+ warning" -Nachricht handelt und das "+ debug" -Feld "+ true" lautet, wird die Nachricht geschrieben. Andernfalls wird die Nachricht ignoriert. Wenn es sich um eine andere Ebene handelt, z. B. "+ error +", wird die Nachricht trotzdem ausgegeben.

Der größte Teil der Logik zur Feststellung, ob die Nachricht ausgedruckt wurde, befindet sich in der Methode "+ Log ". Wir haben auch eine nicht exportierte Methode namens " write " eingeführt. Die Methode " write +" gibt die Protokollnachricht tatsächlich aus.

Wir können diese abgestufte Protokollierung jetzt in unserem anderen Paket verwenden, indem wir "+ cmd / main.go +" so ändern, dass es wie folgt aussieht:

cmd / main.go

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")

}

Wenn Sie dies ausführen, erhalten Sie:

Output[info]  starting up service
[warning]  no tasks found
[error]  exiting: no work performed

In diesem Beispiel hat "+ cmd / main.go " die exportierte " Log +" - Methode erfolgreich verwendet.

Wir können jetzt die Stufe "" jeder Nachricht übergeben, indem wir " debug" auf "+ false" setzen:

main.go

package main

import (
   "time"

   "github.com/gopherguides/logging"
)

func main() {
   logger := logging.New(time.RFC3339, )

   logger.Log("info", "starting up service")
   logger.Log("warning", "no tasks found")
   logger.Log("error", "exiting: no work performed")

}

Jetzt werden wir sehen, dass nur die "+" Fehlermeldungen ausgegeben werden:

Output[error]  exiting: no work performed

Wenn wir versuchen, die Methode "+ write " außerhalb des Pakets " logging +" aufzurufen, wird beim Kompilieren ein Fehler ausgegeben:

main.go

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")


}
Outputcmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or method logging.(*Logger).write)

Wenn der Compiler feststellt, dass Sie versuchen, auf etwas aus einem anderen Paket zu verweisen, das mit einem Kleinbuchstaben beginnt, weiß er, dass es nicht exportiert wird, und gibt daher einen Compilerfehler aus.

Der Logger in diesem Tutorial zeigt, wie wir Code schreiben können, der nur die Teile freigibt, die andere Pakete benötigen sollen. Da wir steuern, welche Teile des Pakets außerhalb des Pakets sichtbar sind, können wir künftige Änderungen vornehmen, ohne den Code zu beeinflussen, der von unserem Paket abhängt. Wenn wir zum Beispiel Nachrichten der Ebene "+ info " nur deaktivieren wollten, wenn " debug +" falsch ist, könnten Sie diese Änderung vornehmen, ohne einen anderen Teil Ihrer API zu beeinflussen. Wir könnten auch sicher Änderungen an der Protokollnachricht vornehmen, um weitere Informationen aufzunehmen, z. B. das Verzeichnis, aus dem das Programm ausgeführt wurde.

Fazit

In diesem Artikel wurde gezeigt, wie Sie Code für mehrere Pakete freigeben und gleichzeitig die Implementierungsdetails Ihres Pakets schützen können. Auf diese Weise können Sie eine einfache API exportieren, die sich aus Gründen der Abwärtskompatibilität nur selten ändert, aber Änderungen in Ihrem Paket privat nach Bedarf zulässt, damit es in Zukunft besser funktioniert. Dies wird als bewährte Methode beim Erstellen von Paketen und den zugehörigen APIs angesehen.

Weitere Informationen zu Paketen in Go finden Sie unter Importing Packages in Go und https://www.digitalocean.com/community / tutorials / how-to-write-packages-in-go [So schreiben Sie Pakete in Go] oder lesen Sie unsere gesamte https://www.digitalocean.com/community/tutorial_series/how-to-code-in- go [How To Code in Go-Reihe].