Comprendre la visibilité des paquets en Go

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 :

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

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

  1. mkdir logging

Passez ensuite dans ce répertoire :

  1. cd logging

Ensuite, en utilisant un éditeur comme nano, créez un fichier appelé logging.go :

  1. nano logging.go

Placez le code suivant dans le fichier logging.go que nous venons de créer :

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

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

Créez un fichier appelé main.go dans le répertoire cmd que nous venons de créer :

  1. nano main.go

Maintenant, nous pouvons ajouter le code suivant :

cmd/main.go
package main

import "github.com/gopherguides/logging"

func main() {
	logging.Debug(true)

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

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:

  1. nano go.mod

Puis placez les contenus suivants dans le fichier:

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

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

Ajoutez les contenus suivants au fichier:

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

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

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

Vous obtiendrez une sortie similaire à la suivante :

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

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

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 :

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

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 :

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

L’exécution de ce programme vous donnera la sortie suivante :

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

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, comme Programme démarré ou Email 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, comme File 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:

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

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:

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

}

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:

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

}

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:

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)

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