Introduzione
Quando si crea un pacchetto in Go, l’obiettivo finale è solitamente rendere il pacchetto accessibile ad altri sviluppatori per utilizzarlo, sia in pacchetti di ordine superiore che in programmi completi. Importando il pacchetto, il tuo pezzo di codice può servire come mattone costruttivo per altri strumenti più complessi. Tuttavia, solo alcuni pacchetti sono disponibili per l’importazione. Questo è determinato dalla visibilità del pacchetto.
Visibilità in questo contesto significa lo spazio dei file da cui un pacchetto o un altro costrutto può essere referenziato. Ad esempio, se definiamo una variabile in una funzione, la visibilità (ambito) di quella variabile è solo all’interno della funzione in cui è stata definita. Allo stesso modo, se definisci una variabile in un pacchetto, puoi renderla visibile solo a quel pacchetto, o permetterle di essere visibile anche al di fuori del pacchetto.
Controllare attentamente la visibilità dei pacchetti è importante quando si scrive codice ergonomico, soprattutto quando si prendono in considerazione i cambiamenti futuri che si potrebbero desiderare di apportare al proprio pacchetto. Se è necessario correggere un bug, migliorare le prestazioni o modificare la funzionalità, si desidera apportare la modifica in modo che non interrompa il codice di chi utilizza il proprio pacchetto. Un modo per minimizzare le modifiche che causano interruzioni è consentire l’accesso solo alle parti del proprio pacchetto necessarie per un corretto utilizzo. Limitando l’accesso, è possibile apportare modifiche interne al proprio pacchetto con meno probabilità di influenzare il modo in cui altri sviluppatori utilizzano il proprio pacchetto.
In questo articolo, imparerai come controllare la visibilità dei pacchetti, nonché come proteggere parti del tuo codice che dovrebbero essere utilizzate solo all’interno del tuo pacchetto. Per fare ciò, creeremo un logger di base per registrare e debuggare messaggi, utilizzando pacchetti con diversi gradi di visibilità degli elementi.
Prerequisiti
Per seguire gli esempi in questo articolo, avrai bisogno di:
- Un workspace Go configurato seguendo Come Installare Go e Configurare un Ambiente di Programmazione Locale. Questa guida utilizzerà la seguente struttura di file:
.
├── bin
│
└── src
└── github.com
└── gopherguides
Elementi Esportati e Non Esportati
A differenza di altri linguaggi di programmazione come Java e Python che utilizzano modificatori di accesso come public
, private
, o protected
per specificare l’ambito, Go determina se un elemento è esportato
e non esportato
attraverso il modo in cui viene dichiarato. Esportare un elemento in questo caso lo rende visibile
al di fuori del pacchetto corrente. Se non è esportato, è visibile e utilizzabile solo all’interno del pacchetto in cui è stato definito.
Questa visibilità esterna è controllata dalla capitalizzazione della prima lettera dell’elemento dichiarato. Tutte le dichiarazioni, come Tipi
, Variabili
, Costanti
, Funzioni
, ecc., che iniziano con una lettera maiuscola sono visibili al di fuori del pacchetto corrente.
Diamo un’occhiata al seguente codice, prestando attenzione alla capitalizzazione:
package greet
import "fmt"
var Greeting string
func Hello(name string) string {
return fmt.Sprintf(Greeting, name)
}
Questo codice dichiara che si trova nel pacchetto greet
. Dichiara quindi due simboli, una variabile chiamata Greeting
e una funzione chiamata Hello
. Poiché entrambi iniziano con una lettera maiuscola, sono entrambi esportati
e disponibili a qualsiasi programma esterno. Come detto in precedenza, creare un pacchetto che limita l’accesso permetterà una migliore progettazione dell’API e renderà più facile aggiornare il proprio pacchetto internamente senza interrompere il codice di chiunque dipenda dal proprio pacchetto.
Definizione della Visibilità del Pacchetto
Per dare un’occhiata più approfondita a come funziona la visibilità del pacchetto in un programma, creiamo un pacchetto logging
, tenendo a mente cosa vogliamo rendere visibile al di fuori del nostro pacchetto e cosa non renderemo visibile. Questo pacchetto di logging sarà responsabile di registrare qualsiasi messaggio del nostro programma sulla console. Esaminerà anche a quale livello stiamo registrando. Un livello descrive il tipo di log e sarà uno dei tre stati: info
, warning
o error
.
Prima, all’interno della directory src
, creiamo una directory chiamata logging
per inserire i nostri file di logging:
Passiamo quindi in quella directory:
Poi, utilizzando un editor come nano, creiamo un file chiamato logging.go
:
Inserisci il seguente codice nel file logging.go
che abbiamo appena creato:
La prima riga di questo codice dichiara un pacchetto chiamato logging
. In questo pacchetto, ci sono due funzioni esportate
: Debug
e Log
. Queste funzioni possono essere chiamate da qualsiasi altro pacchetto che importa il pacchetto logging
. C’è anche una variabile privata chiamata debug
. Questa variabile è accessibile solo dall’interno del pacchetto logging
. È importante notare che mentre la funzione Debug
e la variabile debug
hanno la stessa ortografia, la funzione è in maiuscolo e la variabile no. Questo le rende dichiarazioni distinte con diversi ambiti.
Salva e esci dal file.
Per utilizzare questo pacchetto in altre aree del nostro codice, possiamo importarlo
in un nuovo pacchetto. Creeremo questo nuovo pacchetto, ma avremo bisogno di una nuova directory per archiviare quei file sorgente prima.
Usciamo dalla directory logging
, creiamo una nuova directory chiamata cmd
e entriamo in quella nuova directory:
Crea un file chiamato main.go
nella directory cmd
che abbiamo appena creato:
Ora possiamo aggiungere il seguente codice:
Ora abbiamo scritto l’intero programma. Tuttavia, prima di poter eseguire questo programma, dovremo anche creare un paio di file di configurazione per far sì che il nostro codice funzioni correttamente. Go utilizza Go Modules per configurare le dipendenze dei pacchetti per l’importazione delle risorse. I moduli Go sono file di configurazione posizionati nella directory del pacchetto che indicano al compilatore da dove importare i pacchetti. Sebbene l’apprendimento dei moduli sia al di fuori dell’ambito di questo articolo, possiamo scrivere solo un paio di righe di configurazione per far funzionare questo esempio localmente.
Apri il seguente file go.mod
nella directory cmd
:
Quindi inserisci i seguenti contenuti nel file:
module github.com/gopherguides/cmd
replace github.com/gopherguides/logging => ../logging
La prima riga di questo file indica al compilatore che il pacchetto cmd
ha un percorso di file di github.com/gopherguides/cmd
. La seconda riga dice al compilatore che il pacchetto github.com/gopherguides/logging
può essere trovato localmente su disco nella directory ../logging
.
Avremo anche bisogno di un file go.mod
per il nostro pacchetto logging
. Torniamo nella directory logging
e creiamo un file go.mod
:
Aggiungi i seguenti contenuti al file:
module github.com/gopherguides/logging
Questo dice al compilatore che il pacchetto logging
che abbiamo creato è in realtà il pacchetto github.com/gopherguides/logging
. Questo rende possibile importare il pacchetto nel nostro pacchetto main
con la seguente riga che abbiamo scritto in precedenza:
package main
import "github.com/gopherguides/logging"
func main() {
logging.Debug(true)
logging.Log("This is a debug statement...")
}
Ora dovresti avere la seguente struttura di directory e layout dei file:
├── cmd
│ ├── go.mod
│ └── main.go
└── logging
├── go.mod
└── logging.go
Ora che abbiamo completato tutta la configurazione, possiamo eseguire il programma main
dal pacchetto cmd
con i seguenti comandi:
Otterrai un output simile al seguente:
Output2019-08-28T11:36:09-05:00 This is a debug statement...
Il programma stamperà l’ora corrente in formato RFC 3339 seguita da qualsiasi dichiarazione che abbiamo inviato al logger. RFC 3339 è un formato orario progettato per rappresentare l’ora su internet ed è comunemente utilizzato nei file di log.
Poiché le funzioni Debug
e Log
sono esportate dal pacchetto di logging, possiamo utilizzarle nel nostro pacchetto main
. Tuttavia, la variabile debug
nel pacchetto logging
non è esportata. Tentare di fare riferimento a una dichiarazione non esportata comporterà un errore in fase di compilazione.
Aggiungi la seguente riga evidenziata a 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)
}
Salva ed esegui il file. Riceverai un errore simile al seguente:
Output. . .
./main.go:10:14: cannot refer to unexported name logging.debug
Ora che abbiamo visto come si comportano gli elementi esportati
e non esportati
nei pacchetti, esamineremo successivamente come campi
e metodi
possono essere esportati dalle strutture
.
Visibilità All’interno Delle Strutture
Sebbene lo schema di visibilità nel logger che abbiamo costruito nell’ultima sezione possa funzionare per programmi semplici, condivide troppo stato per essere utile da più pacchetti. Questo perché le variabili esportate sono accessibili a più pacchetti che potrebbero modificare le variabili in stati contraddittori. Permettere che il stato del tuo pacchetto venga cambiato in questo modo rende difficile predire come il tuo programma si comporterà. Con il design attuale, per esempio, un pacchetto potrebbe impostare la variabile Debug
a true
, mentre un altro potrebbe impostarla a false
nello stesso istanza. Ciò creerebbe un problema poiché entrambi i pacchetti che importano il pacchetto logging
sono interessati.
Potremmo rendere il logger isolato creando un struct e poi aggiungendo metodi ad esso. Questo permetterà di creare un’instance
di un logger da utilizzare in modo indipendente in ogni pacchetto che lo utilizza.
Modifica il pacchetto logging
come segue per riformulare il codice e isolare il logger:
In questo codice, abbiamo creato una struttura Logger
. Questa struttura ospiterà il nostro stato non esportato, inclusa la formattazione del tempo da stampare e la variabile debug
impostata su true
o false
. La funzione New
imposta lo stato iniziale per creare il logger, come la formattazione del tempo e lo stato di debug. Poi memorizza i valori che gli abbiamo dato internamente nelle variabili non esportate timeFormat
e debug
. Abbiamo anche creato un metodo chiamato Log
sul tipo Logger
che accetta un’istruzione che vogliamo stampare. All’interno del metodo Log
c’è un riferimento alla sua variabile locale l
per accedere di nuovo ai suoi campi interni come l.timeFormat
e l.debug
.
Questo approccio ci permetterà di creare un Logger
in molti pacchetti diversi e utilizzarlo indipendentemente da come gli altri pacchetti lo stanno utilizzando.
Per utilizzarlo in un altro pacchetto, modifichiamo cmd/main.go
in modo che assomigli al seguente:
Eseguendo questo programma otterrete il seguente output:
Output2019-08-28T11:56:49-05:00 This is a debug statement...
In questo codice, abbiamo creato un’istanza del logger chiamando la funzione esportata New
. Abbiamo memorizzato il riferimento a questa istanza nella variabile logger
. Ora possiamo chiamare logging.Log
per stampare le istruzioni.
Se proviamo a fare riferimento a un campo non esportato dal Logger
come il campo timeFormat
, riceveremo un errore in fase di compilazione. Provate ad aggiungere la seguente riga evidenziata ed eseguite 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)
}
Questo produrrà il seguente errore:
Output. . .
cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)
Il compilatore riconosce che logger.timeFormat
non è esportato e, pertanto, non può essere recuperato dal pacchetto logging
.
Visibilità All’interno Dei Metodi
Allo stesso modo dei campi delle strutture, anche i metodi possono essere esportati o non esportati.
Per illustrare questo concetto, aggiungiamo la registrazione a livelli al nostro logger. La registrazione a livelli è un modo di categorizzare i log in modo da poter cercare nei log specifici tipi di eventi. I livelli che inseriremo nel nostro logger sono:
-
Il livello
info
, che rappresenta eventi di tipo informativo che informano l’utente di un’azione, comeProgramma avviato
oEmail inviata
. Questi ci aiutano a eseguire il debug e tracciare parti del nostro programma per verificare se si sta verificando il comportamento atteso. -
Il livello
warning
. Questi tipi di eventi identificano quando sta accadendo qualcosa di inaspettato che non è un errore, comeInvio email fallito, ritentando
. Ci aiutano a vedere parti del nostro programma che non stanno procedendo così fluidamente come ci aspettavamo. -
Il livello
error
, che significa che il programma ha incontrato un problema, comeFile not found
. Questo spesso si tradurrà nel fallimento dell’operazione del programma.
Potresti anche desiderare di attivare e disattivare determinati livelli di logging, specialmente se il tuo programma non sta performando come previsto e desideri eseguire il debug del programma. Aggiungeremo questa funzionalità cambiando il programma in modo che, quando debug
è impostato su true
, stamperà tutti i livelli di messaggi. Altrimenti, se è false
, stamperà solo i messaggi di errore.
Aggiungi il logging a livelli apportando le seguenti modifiche a logging/logging.go
:
In questo esempio, abbiamo introdotto un nuovo argomento al metodo Log
. Ora possiamo passare il level
del messaggio di log. Il metodo Log
determina a quale livello di messaggio appartiene. Se è un messaggio info
o warning
, e il campo debug
è true
, allora scrive il messaggio. Altrimenti ignora il messaggio. Se è di qualsiasi altro livello, come error
, scriverà comunque il messaggio.
La maggior parte della logica per determinare se il messaggio viene stampato esiste nel metodo Log
. Abbiamo anche introdotto un metodo non esportato chiamato write
. Il metodo write
è quello che effettivamente emette il messaggio di log.
Ora possiamo utilizzare questo logging a livelli nel nostro altro pacchetto modificando cmd/main.go
in modo che assomigli al seguente:
Eseguendo questo otterremo:
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 questo esempio, cmd/main.go
ha utilizzato con successo il metodo esportato Log
.
Ora possiamo passare il livello
di ogni messaggio cambiando debug
a false
:
Ora vedremo che vengono stampati solo i messaggi di livello errore
:
Output[error] 2019-08-28T13:58:52-05:00 exiting: no work performed
Se proviamo a chiamare il metodo write
dall’esterno del pacchetto logging
, riceveremo un errore in fase di compilazione:
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)
Quando il compilatore vede che stai cercando di fare riferimento a qualcosa da un altro pacchetto che inizia con una lettera minuscola, sa che non è esportato e quindi genera un errore del compilatore.
Il logger in questo tutorial illustra come possiamo scrivere codice che espone solo le parti che vogliamo consumare da altri pacchetti. Poiché controlliamo quali parti del pacchetto sono visibili all’esterno del pacchetto, ora siamo in grado di apportare modifiche future senza influenzare alcun codice che dipende dal nostro pacchetto. Ad esempio, se volessimo disattivare solo i messaggi di livello info
quando debug
è falso, potresti apportare questa modifica senza influenzare nessun’altra parte della tua API. Potremmo anche apportare modifiche sicure ai messaggi di log per includere più informazioni, come la directory da cui il programma era in esecuzione.
Conclusione
Questo articolo ha mostrato come condividere codice tra pacchetti proteggendo al contempo i dettagli di implementazione del tuo pacchetto. Questo ti permette di esportare una semplice API che raramente cambierà per mantenere la compatibilità con le versioni precedenti, ma consentirà modifiche private nel tuo pacchetto secondo necessità per migliorare il suo funzionamento in futuro. Questa è considerata una buona pratica nella creazione di pacchetti e delle loro API corrispondenti.
Per saperne più sui pacchetti in Go, consultate le nostre
Source:
https://www.digitalocean.com/community/tutorials/understanding-package-visibility-in-go