Introduction
Lors de la création d’un package en Go, l’objectif final est généralement de rendre le package accessible à d’autres développeurs pour les utiliser, que ce soit dans des packages de niveau supérieur ou des programmes complets. En important le package, votre morceau de code peut servir de bloc de construction pour d’autres outils plus complexes. Cependant, seuls certains packages sont disponibles pour l’importation. Cela est déterminé par la visibilité du package.
Visibilité dans ce contexte signifie l’espace de fichier à partir duquel un package ou une autre construction peut être référencé. Par exemple, si nous définissons une variable dans une fonction, la visibilité (portée) de cette variable est uniquement au sein de la fonction dans laquelle elle a été définie. De même, si vous définissez une variable dans un package, vous pouvez la rendre visible uniquement pour ce package, ou l’autoriser à être visible en dehors du package également.
Il est important de contrôler attentivement la visibilité des packages lors de l’écriture de code ergonomique, en particulier lorsque l’on prend en compte les futurs changements que vous pourriez souhaiter apporter à votre package. Si vous devez corriger un bogue, améliorer les performances ou modifier les fonctionnalités, vous voudrez apporter ces modifications de manière à ne pas casser le code de ceux qui utilisent votre package. Une façon de minimiser les changements cassants est de permettre l’accès uniquement aux parties de votre package nécessaires à son bon usage. En limitant l’accès, vous pouvez apporter des modifications internes à votre package avec moins de chances d’affecter la manière dont les autres développeurs utilisent votre package.
Dans cet article, vous apprendrez à contrôler la visibilité des packages, ainsi qu’à protéger les parties de votre code qui ne doivent être utilisées qu’à l’intérieur de votre package. Pour ce faire, nous allons créer un enregistreur de base pour enregistrer et déboguer les messages, en utilisant des packages avec différents degrés de visibilité des éléments.
Prérequis
Pour suivre les exemples de cet article, vous aurez besoin de :
- Un espace de travail Go configuré en suivant Comment installer Go et configurer un environnement de programmation local. Ce tutoriel utilisera la structure de fichiers suivante :
.
├── bin
│
└── src
└── github.com
└── gopherguides
Éléments Exportés et Non Exportés
Contrairement à d’autres langages de programmation comme Java et Python qui utilisent des modificateurs d’accès tels que public
, private
, ou protected
pour spécifier la portée, Go détermine si un élément est exporté
ou non exporté
en fonction de la manière dont il est déclaré. Exporter un élément dans ce cas le rend visible
en dehors du package courant. S’il n’est pas exporté, il est uniquement visible et utilisable depuis le package dans lequel il a été défini.
Cette visibilité externe est contrôlée en mettant en majuscule la première lettre de l’élément déclaré. Toutes les déclarations, telles que Types
, Variables
, Constantes
, Fonctions
, etc., qui commencent par une lettre majuscule sont visibles en dehors du package courant.
Examinons le code suivant, en prêtant une attention particulière à la capitalisation :
package greet
import "fmt"
var Greeting string
func Hello(name string) string {
return fmt.Sprintf(Greeting, name)
}
Ce code déclare qu’il se trouve dans le package greet
. Il déclare ensuite deux symboles, une variable appelée Greeting
et une fonction appelée Hello
. Comme ils commencent tous les deux par une lettre majuscule, ils sont tous les deux exportés
et disponibles pour tout programme externe. Comme mentionné précédemment, créer un package qui limite l’accès permettra une meilleure conception de l’API et facilitera la mise à jour de votre package en interne sans rompre le code de quiconque dépend de votre package.
Définir la visibilité des packages
Pour examiner de plus près comment la visibilité des packages fonctionne dans un programme, créons un package logging
, en gardant à l’esprit ce que nous voulons rendre visible en dehors de notre package et ce que nous ne rendrons pas visible. Ce package de journalisation sera responsable de la journalisation de tous les messages de notre programme dans la console. Il examinera également le niveau auquel nous nous connectons. Un niveau décrit le type de journal et sera l’un des trois statuts : info
, warning
ou error
.
Tout d’abord, dans votre répertoire src
, créons un répertoire appelé logging
pour y placer nos fichiers de journalisation :
Passez ensuite dans ce répertoire :
Ensuite, en utilisant un éditeur comme nano, créez un fichier appelé logging.go
:
Placez le code suivant dans le fichier logging.go
que nous venons de créer :
La première ligne de ce code déclare un package appelé logging
. Dans ce package, il y a deux fonctions exportées
: Debug
et Log
. Ces fonctions peuvent être appelées par n’importe quel autre package qui importe le package logging
. Il y a également une variable privée appelée debug
. Cette variable n’est accessible que depuis l’intérieur du package logging
. Il est important de noter que bien que la fonction Debug
et la variable debug
aient la même orthographe, la fonction est en majuscule et la variable ne l’est pas. Cela les rend distinctes avec des portées différentes.
Enregistrez et quittez le fichier.
Pour utiliser ce package dans d’autres parties de notre code, nous pouvons l'importer
dans un nouveau package. Nous allons créer ce nouveau package, mais nous aurons besoin d’un nouveau répertoire pour stocker ces fichiers source en premier.
Sortons du répertoire logging
, créons un nouveau répertoire appelé cmd
, et entrons dans ce nouveau répertoire :
Créez un fichier appelé main.go
dans le répertoire cmd
que nous venons de créer :
Maintenant, nous pouvons ajouter le code suivant :
Nous avons maintenant notre programme entièrement écrit. Cependant, avant de pouvoir exécuter ce programme, nous devrons également créer quelques fichiers de configuration pour que notre code fonctionne correctement. Go utilise Go Modules pour configurer les dépendances de packages pour l’importation de ressources. Les modules Go sont des fichiers de configuration placés dans le répertoire de votre package qui indiquent au compilateur où importer les packages. Bien que l’apprentissage des modules dépasse le cadre de cet article, nous pouvons écrire quelques lignes de configuration pour faire fonctionner cet exemple localement.
Ouvrez le fichier go.mod
dans le répertoire cmd
:
Puis placez les contenus suivants dans le fichier:
module github.com/gopherguides/cmd
replace github.com/gopherguides/logging => ../logging
La première ligne de ce fichier indique au compilateur que le package cmd
a un chemin de fichier de github.com/gopherguides/cmd
. La deuxième ligne indique au compilateur que le package github.com/gopherguides/logging
peut être trouvé localement sur le disque dans le répertoire ../logging
.
Nous aurons également besoin d’un fichier go.mod
pour notre package logging
. Retournons dans le répertoire logging
et créons un fichier go.mod
:
Ajoutez les contenus suivants au fichier:
module github.com/gopherguides/logging
Cela indique au compilateur que le package logging
que nous avons créé est en fait le package github.com/gopherguides/logging
. Cela permet d’importer le package dans notre package main
avec la ligne suivante que nous avons écrite plus tôt:
package main
import "github.com/gopherguides/logging"
func main() {
logging.Debug(true)
logging.Log("This is a debug statement...")
}
Vous devriez maintenant avoir la structure de répertoire et la disposition de fichiers suivantes:
├── cmd
│ ├── go.mod
│ └── main.go
└── logging
├── go.mod
└── logging.go
Maintenant que nous avons terminé toutes les configurations, nous pouvons exécuter le programme main
à partir du package cmd
avec les commandes suivantes :
Vous obtiendrez une sortie similaire à la suivante :
Output2019-08-28T11:36:09-05:00 This is a debug statement...
Le programme affichera l’heure actuelle au format RFC 3339 suivie de toute déclaration que nous avons envoyée au logger. RFC 3339 est un format de temps conçu pour représenter l’heure sur Internet et est couramment utilisé dans les fichiers journaux.
Comme les fonctions Debug
et Log
sont exportées depuis le package de journalisation, nous pouvons les utiliser dans notre package main
. Cependant, la variable debug
dans le package logging
n’est pas exportée. Tenter de référencer une déclaration non exportée entraînera une erreur de compilation.
Ajoutez la ligne en surbrillance suivante à 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)
}
Enregistrez et exécutez le fichier. Vous recevrez une erreur similaire à la suivante :
Output. . .
./main.go:10:14: cannot refer to unexported name logging.debug
Maintenant que nous avons vu comment les éléments exportés
et non exportés
se comportent dans les packages, nous examinerons ensuite comment les champs
et les méthodes
peuvent être exportés à partir de structures
.
Visibilité dans les Structures
Bien que le schéma de visibilité dans le logger que nous avons construit dans la dernière section puisse fonctionner pour des programmes simples, il partage trop d’état pour être utile au sein de plusieurs packages. En effet, les variables exportées sont accessibles à plusieurs packages qui pourraient les modifier en états contradictoires. Permettre à l’état de votre package d’être modifié de cette manière rend difficile de prédire le comportement de votre programme. Avec la conception actuelle, par exemple, un package pourrait définir la variable Debug
à true
, et un autre pourrait la définir à false
dans la même instance. Cela créerait un problème puisque les deux packages qui importent le package logging
sont affectés.
Nous pouvons isoler le logger en créant une structure et en accrochant des méthodes à celle-ci. Cela nous permettra de créer une instance
de logger à utiliser indépendamment dans chaque package qui le consomme.
Modifiez le package logging
comme suit pour refactoriser le code et isoler le logger :
Dans ce code, nous avons créé une structure Logger
. Cette structure abritera notre état non exporté, y compris le format de temps à imprimer et la variable debug
définie sur true
ou false
. La fonction New
définit l’état initial pour créer le logger, comme le format de temps et l’état de débogage. Elle stocke ensuite les valeurs que nous lui avons données dans les variables non exportées timeFormat
et debug
. Nous avons également créé une méthode appelée Log
sur le type Logger
qui prend une déclaration que nous voulons imprimer. Dans la méthode Log
se trouve une référence à sa variable de méthode locale l
pour accéder à ses champs internes tels que l.timeFormat
et l.debug
.
Cette approche nous permettra de créer un Logger
dans de nombreux packages différents et de l’utiliser indépendamment de la manière dont les autres packages l’utilisent.
Pour l’utiliser dans un autre package, modifions cmd/main.go
pour qu’il ressemble à ce qui suit :
L’exécution de ce programme vous donnera la sortie suivante :
Output2019-08-28T11:56:49-05:00 This is a debug statement...
Dans ce code, nous avons créé une instance du logger en appelant la fonction exportée New
. Nous avons stocké la référence à cette instance dans la variable logger
. Nous pouvons maintenant appeler logging.Log
pour imprimer des déclarations.
Si nous essayons de référencer un champ non exporté de Logger
comme le champ timeFormat
, nous recevrons une erreur de compilation. Essayez d’ajouter la ligne en surbrillance suivante et exécutez 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)
}
Cela donnera l’erreur suivante :
Output. . .
cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)
Le compilateur reconnaît que logger.timeFormat
n’est pas exporté et ne peut donc pas être récupéré à partir du package logging
.
Visibilité au sein des méthodes
De la même manière que les champs de structures, les méthodes peuvent également être exportées ou non exportées.
Pour illustrer cela, ajoutons la journalisation par niveaux à notre logger. La journalisation par niveaux est un moyen de catégoriser vos journaux afin de pouvoir rechercher des types spécifiques d’événements dans vos journaux. Les niveaux que nous introduirons dans notre logger sont :
-
Le niveau
info
, qui représente des événements de type information qui informent l’utilisateur d’une action, commeProgramme démarré
ouEmail envoyé
. Ils nous aident à déboguer et à suivre des parties de notre programme pour voir si le comportement attendu se produit. -
Le niveau
warning
. Ces types d’événements identifient quand quelque chose d’inattendu se produit qui n’est pas une erreur, commeÉchec de l'envoi de l'email, nouvelle tentative
. Ils nous aident à voir des parties de notre programme qui ne se déroulent pas aussi bien que nous l’avions prévu. -
Le niveau
error
, ce qui signifie que le programme a rencontré un problème, commeFile not found
. Cela entraînera souvent l’échec de l’opération du programme.
Vous pourriez également souhaiter activer et désactiver certains niveaux de journalisation, surtout si votre programme ne fonctionne pas comme prévu et que vous aimeriez déboguer le programme. Nous ajouterons cette fonctionnalité en modifiant le programme de sorte que lorsque debug
est défini sur true
, il imprimera tous les niveaux de messages. Sinon, s’il est false
, il n’imprimera que les messages d’erreur.
Ajoutez la journalisation par niveaux en apportant les modifications suivantes à logging/logging.go
:
Dans cet exemple, nous avons introduit un nouvel argument à la méthode Log
. Nous pouvons maintenant passer le level
du message de log. La méthode Log
détermine quel niveau de message il s’agit. Si c’est un message info
ou warning
, et que le champ debug
est true
, alors il écrit le message. Sinon, il ignore le message. Si c’est un autre niveau, comme error
, il écrira le message quoi qu’il en soit.
La plupart de la logique pour déterminer si le message est imprimé se trouve dans la méthode Log
. Nous avons également introduit une méthode non exportée appelée write
. La méthode write
est ce qui produit réellement le message de log.
Nous pouvons maintenant utiliser ce logging par niveaux dans notre autre package en modifiant cmd/main.go
pour qu’il ressemble à ce qui suit:
L’exécution de ceci vous donnera:
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
Dans cet exemple, cmd/main.go
a utilisé avec succès la méthode exportée Log
.
Nous pouvons maintenant passer le level
de chaque message en passant debug
à false
:
Maintenant, nous verrons que seuls les messages de niveau error
s’impriment:
Output[error] 2019-08-28T13:58:52-05:00 exiting: no work performed
Si nous essayons d’appeler la méthode write
depuis l’extérieur du package logging
, nous recevrons une erreur de compilation:
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)
Lorsque le compilateur voit que vous essayez de référencer quelque chose d’un autre package qui commence par une lettre minuscule, il sait que ce n’est pas exporté, et donc génère une erreur de compilation.
Le logger dans ce tutoriel montre comment nous pouvons écrire du code qui expose uniquement les parties que nous souhaitons que d’autres packages consomment. Étant donné que nous contrôlons les parties du package visibles à l’extérieur du package, nous sommes désormais en mesure d’apporter des modifications futures sans affecter le moindre code dépendant de notre package. Par exemple, si nous voulions désactiver uniquement les messages de niveau info
lorsque debug
est faux, vous pourriez apporter cette modification sans affecter aucune autre partie de votre API. Nous pourrions également apporter en toute sécurité des modifications aux messages de log pour inclure plus d’informations, comme le répertoire dans lequel le programme était en cours d’exécution.
Conclusion
Cet article a montré comment partager du code entre les packages tout en protégeant les détails d’implémentation de votre package. Cela vous permet d’exporter une API simple qui changera rarement pour assurer la compatibilité descendante, mais permettra des modifications en privé dans votre package si nécessaire pour l’améliorer à l’avenir. Cette pratique est considérée comme la meilleure méthode lors de la création de packages et de leurs API correspondantes.
Pour en savoir plus sur les packages en Go, consultez nos articles Importer des Packages en Go et Comment Écrire des Packages en Go, ou explorez notre série complète Comment Coder en Go.
Source:
https://www.digitalocean.com/community/tutorials/understanding-package-visibility-in-go