Verstehen Sie die Paket-Sichtbarkeit in Go

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:

.
├── 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:

greet.go
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:

  1. mkdir logging

Gehen Sie als Nächstes in dieses Verzeichnis:

  1. cd logging

Erstellen Sie dann mit einem Editor wie nano eine Datei namens logging.go:

  1. nano logging.go

Fügen Sie den folgenden Code in die gerade 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 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:

  1. cd ..
  2. mkdir cmd
  3. cd cmd

Erstellen Sie eine Datei namens main.go im neu erstellten cmd-Verzeichnis:

  1. 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 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:

  1. 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 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:

  1. cd ../logging
  2. nano go.mod

Fügen Sie den folgenden Inhalt zur Datei hinzu:

go.mod
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:

cmd/main.go
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:

  1. cd ../cmd
  2. go run main.go

Sie erhalten eine Ausgabe ähnlich der folgenden:

Output
2019-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:

cmd/main.go
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:

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 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:

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

Die Ausführung dieses Programms liefert folgende Ausgabe:

Output
2019-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:

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

	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 oder E-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:

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 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:

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

}

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:

main.go
package main

import (
	"time"

	"github.com/gopherguides/logging"
)

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

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

}

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:

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

	logger.write("error", "log this message...")
}
Output
cmd/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