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:
- Een Go-werkruimte ingesteld door de instructies te volgen in How To Install Go and Set Up a Local Programming Environment. Deze tutorial zal de volgende bestandsstructuur gebruiken:
.
├── 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:
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:
Ga vervolgens naar die directory:
Gebruik vervolgens een editor zoals nano om een bestand genaamd logging.go
aan te maken:
Plaats de volgende code in het logging.go
bestand dat we zojuist hebben aangemaakt:
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:
Maak een bestand genaamd main.go
in de cmd
map die we net hebben aangemaakt:
Nu kunnen we de volgende code toevoegen:
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:
Plaats vervolgens de volgende inhoud in het bestand:
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:
Voeg de volgende inhoud toe aan het bestand:
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:
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:
Je krijgt uitvoer die vergelijkbaar is met het volgende:
Output2019-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
:
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:
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:
Het uitvoeren van dit programma geeft je het volgende resultaat:
Output2019-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:
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, zoalsProgramma gestart
ofE-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, zoalsE-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, zoalsBestand 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
:
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:
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:
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:
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)
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