Comprendere la visibilità dei pacchetti in Go

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:

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

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

  1. mkdir logging

Passiamo quindi in quella directory:

  1. cd logging

Poi, utilizzando un editor come nano, creiamo un file chiamato logging.go:

  1. nano logging.go

Inserisci il seguente codice nel file logging.go che abbiamo appena creato:

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

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:

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

Crea un file chiamato main.go nella directory cmd che abbiamo appena creato:

  1. nano main.go

Ora possiamo aggiungere il seguente codice:

cmd/main.go
package main

import "github.com/gopherguides/logging"

func main() {
	logging.Debug(true)

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

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:

  1. nano go.mod

Quindi inserisci i seguenti contenuti nel file:

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

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

Aggiungi i seguenti contenuti al file:

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

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

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

Otterrai un output simile al seguente:

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

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

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:

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

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

Eseguendo questo programma otterrete il seguente output:

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

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, come Programma avviato o Email 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, come Invio 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, come File 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:

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

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

}

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:

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

}

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:

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)

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 Guide sulle Importazioni di Pacchetti in Go e le Scrivere Pacchetti in Go. Puoi anche esplorare la nostra intera serie Come Codificare in Go.

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