Begrijpen van pakketoverheid in Go

Inleiding

Bij het maken van een pakket in Go is het uiteindelijke doel meestal om het pakket beschikbaar te maken voor andere ontwikkelaars om te gebruiken, hetzij in pakketten van een hoger niveau of in gehele programma’s. Door het pakket te importeren, kan je stuk code dienen als bouwsteen voor andere, complexere tools. Echter, slechts bepaalde pakketten zijn beschikbaar voor importeren. Dit wordt bepaald door de zichtbaarheid van het pakket.

Zichtbaarheid in deze context betekent de bestandsruimte waarbinnen een pakket of ander construct kan worden aangeroepen. Bijvoorbeeld, als we een variabele definiëren in een functie, is de zichtbaarheid (scope) van die variabele alleen binnen de functie waarin deze is gedefinieerd. Op dezelfde manier, als je een variabele definieert in een pakket, kun je deze zichtbaar maken voor alleen dat pakket, of toestaan dat deze ook buiten het pakket zichtbaar is.

Zorgvuldig beheersen van pakketzichtbaarheid is belangrijk bij het schrijven van ergonomisch code, vooral wanneer rekening wordt gehouden met toekomstige wijzigingen die je misschien wilt aanbrengen in je pakket. Als je een bug moet oplossen, de prestaties moet verbeteren of functionaliteit moet wijzigen, wil je deze wijziging op een manier aanbrengen die de code van iedereen die je pakket gebruikt niet zal breken. Een manier om brekende wijzigingen te minimaliseren is om alleen toegang te verlenen tot de delen van je pakket die nodig zijn om het correct te kunnen gebruiken. Door toegang te beperken, kun je interne wijzigingen aanbrengen in je pakket met minder kans om te beïnvloeden hoe andere ontwikkelaars je pakket gebruiken.

In dit artikel leer je hoe je pakketzichtbaarheid kunt beheren, evenals hoe je delen van je code kunt beschermen die alleen binnen je pakket gebruikt moeten worden. Hiervoor zullen we een basislogger maken om log- en debugberichten te registreren, met behulp van pakketten met verschillende mate van itemzichtbaarheid.

Vereisten

Om de voorbeelden in dit artikel te volgen, heb je het volgende nodig:

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

Geëxporteerde en Niet-geëxporteerde Items

In tegenstelling tot andere programmeertalen zoals Java en Python die toegangsmodificatoren zoals public, private, of protected gebruiken om het bereik aan te geven, bepaalt Go of een item geëxporteerd en niet-geëxporteerd is door hoe het wordt gedeclareerd. Het exporteren van een item maakt het zichtbaar buiten het huidige pakket. Als het niet geëxporteerd is, is het alleen zichtbaar en bruikbaar binnen het pakket waarin het is gedefinieerd.

Deze externe zichtbaarheid wordt bepaald door de eerste letter van het gedeclareerde item te kapitaliseren. Alle declaraties, zoals Types, Variables, Constants, Functions, enz., die met een hoofdletter beginnen, zijn zichtbaar buiten het huidige pakket.

Laten we naar de volgende code kijken, met aandacht voor de kapitalisatie:

greet.go
package greet

import "fmt"

var Greeting string

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

Deze code declareert dat het zich in het greet pakket bevindt. Vervolgens declareert het twee symbolen, een variabele genaamd Greeting en een functie genaamd Hello. Omdat ze allebei met een hoofdletter beginnen, zijn ze beide geëxporteerd en beschikbaar voor elke externe programma. Zoals eerder vermeld, het ontwerpen van een pakket dat toegang beperkt, zal leiden tot een beter API-ontwerp en het zal eenvoudiger zijn om uw pakket intern bij te werken zonder code van iemand anders te breken die afhankelijk is van uw pakket.

Definiëren van Pakket Zichtbaarheid

Laten we eens nader kijken hoe pakket zichtbaarheid werkt in een programma door een logging pakket te creëren, terwijl we in gedachten houden wat we buiten ons pakket zichtbaar willen maken en wat we niet zichtbaar zullen maken. Dit logging pakket zal verantwoordelijk zijn voor het loggen van alle programmaberichten naar de console. Het zal ook bekijken op welk niveau we loggen. Een niveau beschrijft het type log en zal een van de drie statussen zijn: info, waarschuwing of fout.

Eerst, binnen uw src directory, laten we een directory genaamd logging aanmaken om onze logging bestanden in te plaatsen:

  1. mkdir logging

Ga vervolgens naar die directory:

  1. cd logging

Gebruik vervolgens een editor zoals nano om een bestand genaamd logging.go aan te maken:

  1. nano logging.go

Plaats de volgende code in het logging.go bestand dat we zojuist hebben aangemaakt:

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

De eerste regel van deze code declareert een pakket genaamd logging. In dit pakket zijn er twee geëxporteerde functies: Debug en Log. Deze functies kunnen worden aangeroepen door elk ander pakket dat het logging pakket importeert. Er is ook een private variabele genaamd debug. Deze variabele is alleen toegankelijk vanuit het logging pakket. Het is belangrijk op te merken dat hoewel de functie Debug en de variabele debug dezelfde spelling hebben, de functie met een hoofdletter is geschreven en de variabele niet. Dit maakt ze tot afzonderlijke declaraties met verschillende scopes.

Bewaar en sluit het bestand.

Om dit pakket in andere delen van onze code te gebruiken, kunnen we het importeren in een nieuw pakket. We zullen dit nieuwe pakket aanmaken, maar we hebben eerst een nieuwe map nodig om die bronbestanden in op te slaan.

Laten we uit de logging map stappen, een nieuwe map genaamd cmd aanmaken en in die nieuwe map gaan:

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

Maak een bestand genaamd main.go in de cmd map die we net hebben aangemaakt:

  1. nano main.go

Nu kunnen we de volgende code toevoegen:

cmd/main.go
package main

import "github.com/gopherguides/logging"

func main() {
	logging.Debug(true)

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

We hebben nu ons hele programma geschreven. Voordat we dit programma echter kunnen uitvoeren, moeten we ook een paar configuratiebestanden aanmaken zodat onze code correct werkt. Go gebruikt Go Modules om pakketafhankelijkheden te configureren voor het importeren van bronnen. Go modules zijn configuratiebestanden die in de pakketdirectory worden geplaatst en de compiler vertellen waar pakketten vandaan moeten worden geïmporteerd. Hoewel het leren over modules buiten het bereik van dit artikel valt, kunnen we slechts een paar regels configuratie schrijven om dit voorbeeld lokaal te laten werken.

Open het volgende go.mod bestand in de cmd directory:

  1. nano go.mod

Plaats vervolgens de volgende inhoud in het bestand:

go.mod
module github.com/gopherguides/cmd

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

De eerste regel van dit bestand vertelt de compiler dat het cmd pakket een bestandspad heeft van github.com/gopherguides/cmd. De tweede regel vertelt de compiler dat het pakket github.com/gopherguides/logging lokaal op de schijf te vinden is in de ../logging directory.

We hebben ook een go.mod bestand nodig voor ons logging pakket. Laten we teruggaan naar de logging directory en een go.mod bestand aanmaken:

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

Voeg de volgende inhoud toe aan het bestand:

go.mod
module github.com/gopherguides/logging

Dit vertelt de compiler dat het logging pakket dat we hebben gemaakt eigenlijk het github.com/gopherguides/logging pakket is. Dit maakt het mogelijk om het pakket in ons main pakket te importeren met de volgende regel die we eerder hebben geschreven:

cmd/main.go
package main

import "github.com/gopherguides/logging"

func main() {
	logging.Debug(true)

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

Je zou nu de volgende directorystructuur en bestandsindeling moeten hebben:

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

Nu we alle configuratie hebben voltooid, kunnen we het main programma uit het cmd pakket uitvoeren met de volgende commando’s:

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

Je krijgt uitvoer die vergelijkbaar is met het volgende:

Output
2019-08-28T11:36:09-05:00 This is a debug statement...

Het programma zal de huidige tijd in RFC 3339 formaat afdrukken, gevolgd door welke uitspraak we ook naar de logger hebben gestuurd. RFC 3339 is een tijdformaat dat is ontworpen om tijd op internet weer te geven en wordt vaak gebruikt in logbestanden.

Omdat de Debug en Log functies worden geëxporteerd uit het logging pakket, kunnen we ze gebruiken in ons main pakket. Echter, de debug variabele in het logging pakket is niet geëxporteerd. Het proberen te verwijzen naar een niet-geëxporteerde declaratie zal resulteren in een compilatiefout.

Voeg de volgende gemarkeerde regel toe aan main.go:

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

Bewaar en voer het bestand uit. Je zult een foutmelding krijgen die vergelijkbaar is met het volgende:

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

Nu we hebben gezien hoe geëxporteerde en niet-geëxporteerde items in pakketten zich gedragen, zullen we vervolgens kijken hoe velden en methoden kunnen worden geëxporteerd uit structuren.

Zichtbaarheid Binnen Structuren

Hoewel het zichtbaarheidsschema in de logger die we in de vorige sectie hebben gebouwd misschien werkt voor eenvoudige programma’s, deelt het te veel staat om nuttig te zijn vanuit meerdere pakketten. Dit komt omdat de geëxporteerde variabelen toegankelijk zijn voor meerdere pakketten die de variabelen in tegenstrijdige toestanden kunnen wijzigen. Het toestaan dat de staat van uw pakket op deze manier wordt gewijzigd, maakt het moeilijk om te voorspellen hoe uw programma zal werken. Bij de huidige ontwerp, bijvoorbeeld, kan één pakket de variabele Debug instellen op true, en een ander pakket kan deze instellen op false in hetzelfde exemplaar. Dit zou een probleem creëren omdat beide pakketten die het logging pakket importeren, erdoor worden beïnvloed.

We kunnen de logger geïsoleerd maken door een struct te creëren en vervolgens methoden eraan te hangen. Dit stelt ons in staat om een exemplaar van een logger te maken dat onafhankelijk kan worden gebruikt in elk pakket dat het consumeert.

Wijzig het logging pakket naar het volgende om de code te herstructureren en de logger te isoleren:

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 deze code hebben we een Logger struct aangemaakt. Deze struct zal onze ongeëxporteerde staat bevatten, inclusief het tijdformaat om uit te printen en de debug variabele instelling van true of false. De New functie stelt de initiële staat in om de logger mee te maken, zoals het tijdformaat en de debug-staat. Het slaat dan de waarden die we eraan hebben gegeven intern op in de ongeëxporteerde variabelen timeFormat en debug. We hebben ook een methode genaamd Log aangemaakt op het Logger type dat een statement neemt dat we willen uitprinten. Binnen de Log methode is er een referentie naar de lokale methodevariabele l om toegang te krijgen tot zijn interne velden zoals l.timeFormat en l.debug.

Deze aanpak zal ons toestaan om een Logger in veel verschillende pakketten aan te maken en het onafhankelijk te gebruiken van hoe de andere pakketten het gebruiken.

Om het in een ander pakket te gebruiken, laten we cmd/main.go aanpassen om er als volgt uit te zien:

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

Het uitvoeren van dit programma geeft je het volgende resultaat:

Output
2019-08-28T11:56:49-05:00 This is a debug statement...

In deze code hebben we een instantie van de logger aangemaakt door de geëxporteerde functie New aan te roepen. We hebben de referentie naar deze instantie opgeslagen in de logger variabele. We kunnen nu logging.Log aanroepen om statements uit te printen.

Als we proberen om een ongeëxporteerd veld van de Logger te refereren, zoals het timeFormat veld, krijgen we een compileerfout. Probeer de volgende gemarkeerde regel toe te voegen en cmd/main.go uit te voeren:

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

Dit zal de volgende fout geven:

Output
. . . cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)

De compiler herkent dat logger.timeFormat niet geëxporteerd is, en kan dus niet worden opgehaald uit het logging pakket.

Zichtbaarheid Binnen Methoden

Net zoals structvelden kunnen ook methoden geëxporteerd of niet-geëxporteerd zijn.

Om dit te illustreren, laten we geleidelijke logging toevoegen aan onze logger. Geleidelijke logging is een middel om je logs te categoriseren, zodat je je logs kunt doorzoeken op specifieke soorten gebeurtenissen. De niveaus die we in onze logger zullen plaatsen zijn:

  • Het info niveau, dat informatieve gebeurtenissen vertegenwoordigt die de gebruiker informeren over een actie, zoals Programma gestart of E-mail verzonden. Deze helpen ons te debuggen en delen van ons programma te volgen om te zien of het verwachte gedrag plaatsvindt.

  • Het waarschuwing niveau. Deze soort gebeurtenissen identificeren wanneer er iets onverwachts gebeurt dat geen fout is, zoals E-mail verzenden mislukt, opnieuw proberen. Ze helpen ons te zien welke delen van ons programma niet zo soepel verlopen als we hadden verwacht.

  • Het error niveau, wat betekent dat het programma een probleem tegenkwam, zoals Bestand niet gevonden. Dit zal vaak leiden tot het mislukken van de werking van het programma.

Je kunt ook willen dat bepaalde logniveaus in- en uitgeschakeld worden, vooral als je programma niet naar verwachting presteert en je het programma wilt debuggen. We zullen deze functionaliteit toevoegen door het programma zo te wijzigen dat wanneer debug is ingesteld op true, het alle niveaus van berichten zal afdrukken. Anders, als het false is, zal het alleen foutberichten afdrukken.

Voeg gelaagd loggen toe door de volgende wijzigingen aan te brengen in logging/logging.go:

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 dit voorbeeld hebben we een nieuw argument aan de Log methode toegevoegd. We kunnen nu het level van het logbericht doorgeven. De Log methode bepaalt welk niveau van bericht het is. Als het een info of warning bericht is, en het debug veld is true, dan schrijft het het bericht. Anders negeert het het bericht. Als het een ander niveau is, zoals error, zal het het bericht ongeacht afdrukken.

De meeste logica voor het bepalen of de berichten worden uitgeprint bestaat in de Log methode. We hebben ook een niet-exporteerde methode toegevoegd genaamd write. De write methode is wat daadwerkelijk de logberichten uitvoert.

U kunt nu deze geleveerde logging gebruiken in uw andere pakketten door cmd/main.go te wijzigen naar de volgende opmaak:

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

}

Dit zal je nu laten zien:

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 deze voorbeeld zijn cmd/main.go succesvol de Log methode gebruikt vanuit het exporteerde logging pakket.

We kunnen nu de level van elk bericht door debug te schakelen naar false overschakelen:

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

}

Nu zien we dat alleen de error niveau berichten worden geprint:

Output
[error] 2019-08-28T13:58:52-05:00 exiting: no work performed

Als je proberen om de write methode te调用 van buiten de logging pakket, dan zullen je een compile-time fout ontvangen:

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)

Wanneer de compiler ziet dat je probeert te refereren aan iets van een andere pakket die met een lage letter beginnt, weten ze dat het niet exporteerd is en vraagt ze je daarom een compile-time fout.

De logger in deze tutorial illustreert hoe we code kunnen schrijven die alleen de delen onthult die we willen dat andere pakketten gebruiken. Omdat we bepalen welke delen van het pakket zichtbaar zijn buiten het pakket, kunnen we nu toekomstige wijzigingen aanbrengen zonder enige code die afhankelijk is van ons pakket te beïnvloeden. Als voorbeeld, als we alleen info-niveau berichten willen uitschakelen wanneer debug false is, kun je deze wijziging aanbrengen zonder andere delen van je API te beïnvloeden. We konden ook veilig wijzigingen aanbrengen aan het logbericht om meer informatie toe te voegen, zoals de map waarin het programma werd uitgevoerd.

Conclusie

Dit artikel toonde hoe je code kunt delen tussen pakketten terwijl je ook de implementatiedetails van je pakket beschermt. Dit stelt je in staat om een eenvoudige API te exporteren die weinig zal veranderen voor achterwaartse compatibiliteit, maar die toelaat wijzigingen privé in je pakket aan te brengen als dat nodig is om het beter te laten functioneren in de toekomst. Dit wordt beschouwd als een beste praktijk bij het creëren van pakketten en hun bijbehorende API’s.

Om meer te weten te komen over pakketten in Go, bekijk onze artikelen Pakketten Importeren in Go en Hoe Pakketten Te Schrijven in Go, of verken onze hele Hoe Te Coderen in Go serie.

Source:
https://www.digitalocean.com/community/tutorials/understanding-package-visibility-in-go