Comprendre la visibilité des paquets en Go

introduction

Lors de la création d'unpackage in Go, l'objectif final est généralement de rendre le package accessible à d'autres développeurs, que ce soit dans des packages d'ordre supérieur ou des programmes entiers. Parimporting the 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. Ceci est déterminé par la visibilité du package.

Visibility dans ce contexte signifie l'espace 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 dans 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 à ce package ou lui permettre d'être également visible à l'extérieur du package.

Il est important de contrôler soigneusement la visibilité des packages lors de la rédaction de code ergonomique, en particulier lors de la prise en compte des modifications que vous souhaitez éventuellement apporter à votre package. Si vous devez corriger un bogue, améliorer les performances ou modifier les fonctionnalités, vous souhaiterez effectuer la modification de manière à ne pas casser le code des utilisateurs de votre package. Une façon de minimiser les changements de dernière minute consiste à autoriser l'accès uniquement aux parties de votre package nécessaires à son utilisation correcte. En limitant l'accès, vous pouvez apporter des modifications internes à votre package sans que cela affecte la manière dont les autres développeurs utilisent votre package.

Dans cet article, vous apprendrez à contrôler la visibilité des packages et à protéger les parties de votre code qui ne doivent être utilisées que dans votre package. Pour ce faire, nous allons créer un enregistreur de base pour enregistrer et déboguer les messages, à l'aide de packages offrant un degré de visibilité différent pour les éléments.

Conditions préalables

Pour suivre les exemples de cet article, vous aurez besoin de:

.
├── bin
│
└── src
    └── github.com
        └── gopherguides

Articles exportés et non exportés

Contrairement à d'autres langages de programme comme Java etPython qui utilisentaccess modifiers tels quepublic,private ouprotected pour spécifier la portée, Go détermine si un élément estexported etunexported par la manière dont il est déclaré. L'exportation d'un élément dans ce cas le rendvisibleen dehors du package courant. S'il n'est pas exporté, il est uniquement visible et utilisable dans 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'article déclaré. Toutes les déclarations, telles queTypes,Variables,Constants,Functions, etc., qui commencent par une majuscule sont visibles en dehors du package actuel.

Examinons le code suivant en portant 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 est dans le packagegreet. Il déclare ensuite deux symboles, une variable appeléeGreeting et une fonction appeléeHello. Parce qu'ils commencent tous les deux par une majuscule, ils sont tous les deuxexported et disponibles pour tout programme extérieur. Comme indiqué précédemment, la création d’un paquetage qui limite l’accès permettra une meilleure conception des API et facilitera la mise à jour de votre paquet en interne sans casser le code de quiconque dépendant de votre paquet.

Définir la visibilité du paquet

Pour examiner de plus près le fonctionnement de la visibilité des packages dans un programme, créons un packagelogging, en gardant à l'esprit ce que nous voulons rendre visible en dehors de notre package et ce que nous ne rendrons pas visible. Ce paquet de journalisation sera responsable de la journalisation de tous les messages de notre programme sur la console. Il regardera également à quelslevel nous nous connectons. Un niveau décrit le type de journal et sera l'un des trois états suivants:info,warning ouerror.

Tout d'abord, dans votre répertoiresrc, créons un répertoire appelélogging dans lequel placer nos fichiers de journalisation:

mkdir logging

Allez dans ce répertoire ensuite:

cd logging

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

nano logging.go

Placez le code suivant dans le fichierlogging.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 a déclaré un package appelélogging. Dans ce package, il y a deux fonctionsexported:Debug etLog. Ces fonctions peuvent être appelées par tout autre package qui importe le packagelogging. Il existe également une variable privée appeléedebug. Cette variable n'est accessible qu'à partir du packagelogging. Il est important de noter que si la fonctionDebug et la variabledebug ont toutes deux la même orthographe, la fonction est en majuscule et la variable ne l'est pas. Cela en fait des déclarations distinctes avec des portées différentes.

Enregistrez et quittez le fichier.

Pour utiliser ce package dans d'autres zones de notre code, nous pouvonsimport it into a new package. Nous allons créer ce nouveau paquet, mais nous aurons besoin d’un nouveau répertoire pour stocker ces fichiers source en premier.

Sortons du répertoirelogging, créons un nouveau répertoire appelécmd et entrons dans ce nouveau répertoire:

cd ..
mkdir cmd
cd cmd

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

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 tout notre programme écrit. Cependant, avant de pouvoir exécuter ce programme, nous devons également créer quelques fichiers de configuration pour que notre code fonctionne correctement. Go utiliseGo Modules pour configurer les dépendances de package pour l'importation de ressources. Les modules Go sont des fichiers de configuration placés dans votre répertoire de packages qui indiquent au compilateur d'où importer les packages. Bien que l’apprentissage des modules dépasse le cadre de cet article, nous ne pouvons écrire que quelques lignes de configuration pour que cet exemple fonctionne localement.

Ouvrez le fichiergo.mod suivant dans le répertoirecmd:

nano go.mod

Placez ensuite le contenu suivant 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 packagecmd a un chemin de fichier degithub.com/gopherguides/cmd. La deuxième ligne indique au compilateur que le packagegithub.com/gopherguides/logging peut être trouvé localement sur le disque dans le répertoire../logging.

Nous aurons également besoin d'un fichiergo.mod pour notre packagelogging. Revenons dans le répertoirelogging et créons un fichiergo.mod:

cd ../logging
nano go.mod

Ajoutez le contenu suivant au fichier:

go.mod

module github.com/gopherguides/logging

Cela indique au compilateur que le packagelogging que nous avons créé est en fait le packagegithub.com/gopherguides/logging. Cela permet d'importer le package dans notre packagemain avec la ligne suivante que nous avons écrite précédemment:

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

├── cmd
│   ├── go.mod
│   └── main.go
└── logging
    ├── go.mod
    └── logging.go

Maintenant que toute la configuration est terminée, nous pouvons exécuter le programmemain à partir du packagecmd avec les commandes suivantes:

cd ../cmd
go run main.go

Vous obtiendrez une sortie similaire à celle-ci:

Output2019-08-28T11:36:09-05:00 This is a debug statement...

Le programme imprimera l'heure actuelle au format RFC 3339 suivi de la déclaration que nous avons envoyée à l'enregistreur. RFC 3339 est un format d'heure conçu pour représenter l'heure sur Internet et couramment utilisé dans les fichiers journaux.

Étant donné que les fonctionsDebug etLog sont exportées depuis le package de journalisation, nous pouvons les utiliser dans notre packagemain. Cependant, la variabledebug du packagelogging n'est pas exportée. Essayer de référencer une déclaration non exportée provoquera une erreur lors de la 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 semblable à la suivante:

Output. . .
./main.go:10:14: cannot refer to unexported name logging.debug

Maintenant que nous avons vu comment se comportent les élémentsexported etunexported dans les packages, nous verrons ensuite commentfields etmethods peuvent être exportés depuisstructs.

Visibilité dans les structures

Bien que le schéma de visibilité dans l'enregistreur que nous avons construit dans la dernière section puisse fonctionner pour des programmes simples, il partage trop d'états pour être utile dans plusieurs packages. En effet, les variables exportées sont accessibles à plusieurs packages susceptibles de les modifier en états contradictoires. Permettre à l'état de votre paquet 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 variableDebug surtrue, et un autre pourrait la définir surfalse dans la même instance. Cela créerait un problème car les deux packages qui importent le packagelogging sont affectés.

Nous pouvons isoler l'enregistreur en créant une structure puis en suspendant les méthodes. Cela nous permettra de créer uninstance d'un enregistreur à utiliser indépendamment dans chaque paquet qui le consomme.

Modifiez le packagelogging comme suit pour refactoriser le code et isoler l'enregistreur:

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 structureLogger. Cette structure hébergera notre état non exporté, y compris le format de l'heure à imprimer et le paramètre de variabledebug detrue oufalse. La fonctionNew définit l'état initial avec lequel créer le journal, comme le format de l'heure et l'état de débogage. Il stocke ensuite les valeurs que nous lui avons données en interne aux variables non exportéestimeFormat etdebug. Nous avons également créé une méthode appeléeLog sur le typeLogger qui prend une instruction que nous voulons imprimer. Dans la méthodeLog se trouve une référence à sa variable de méthode localel pour accéder à ses champs internes tels quel.timeFormat etl.debug.

Cette approche nous permettra de créer unLogger dans de nombreux packages différents et de l'utiliser indépendamment de la façon dont les autres packages l'utilisent.

Pour l’utiliser dans un autre package, modifionscmd/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:

Output2019-08-28T11:56:49-05:00 This is a debug statement...

Dans ce code, nous avons créé une instance de l'enregistreur en appelant la fonction exportéeNew. Nous avons stocké la référence à cette instance dans la variablelogger. Nous pouvons maintenant appelerlogging.Log pour imprimer des instructions.

Si nous essayons de référencer un champ non exporté à partir duLogger tel que le champtimeFormat, nous recevrons une erreur de compilation. Essayez d'ajouter la ligne en surbrillance suivante et d'exécutercmd/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 quelogger.timeFormat n’est pas exporté et ne peut donc pas être récupéré à partir du packagelogging.

Visibilité dans les méthodes

De la même manière que les champs de structure, les méthodes peuvent également être exportées ou non exportées.

Pour illustrer cela, ajoutons la journalisationleveled à notre enregistreur. La journalisation à niveau est un moyen de catégoriser vos journaux afin que vous puissiez rechercher dans ces journaux des types d'événements spécifiques. Les niveaux que nous allons mettre dans notre enregistreur sont:

  • Le niveauinfo, qui représente les événements de type information qui informent l'utilisateur d'une action, tels queProgram started ouEmail sent. Cela nous aide à déboguer et à suivre certaines parties de notre programme pour voir si le comportement attendu se produit.

  • Le niveauwarning. Ces types d'événements identifient quand quelque chose d'inattendu se produit qui n'est pas une erreur, commeEmail failed to send, retrying. Ils nous aident à voir les parties de notre programme qui ne se passent pas aussi bien que nous le pensions.

  • Le niveauerror, ce qui signifie que le programme a rencontré un problème, commeFile not found. Cela entraînera souvent l’échec du fonctionnement du programme.

Vous pouvez également souhaiter activer et désactiver certains niveaux de journalisation, en particulier si votre programme ne fonctionne pas comme prévu et si vous souhaitez le déboguer. Nous ajouterons cette fonctionnalité en modifiant le programme de sorte que lorsquedebug est défini surtrue, il imprime tous les niveaux de messages. Sinon, s'il s'agit defalse, il n'imprimera que les messages d'erreur.

Ajoutez une journalisation nivelée 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 dans la méthodeLog. Nous pouvons maintenant passer leslevel du message du journal. La méthodeLog détermine de quel niveau de message il s'agit. S'il s'agit d'un messageinfo ouwarning et que le champdebug esttrue, il écrit le message. Sinon, le message est ignoré. S'il s'agit d'un autre niveau, commeerror, il écrira le message malgré tout.

La plupart de la logique pour déterminer si le message est imprimé existe dans la méthodeLog. Nous avons également introduit une méthode non exportée appeléewrite. La méthodewrite est ce qui génère réellement le message du journal.

Nous pouvons maintenant utiliser cette journalisation nivelée dans notre autre package en modifiantcmd/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")

}

Courir cela 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éthodeLog exportée.

On peut maintenant passer leslevel de chaque message en commutantdebug enfalse:

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

}

Nous allons maintenant voir que seuls les messages de niveauerror s'impriment:

Output[error] 2019-08-28T13:58:52-05:00 exiting: no work performed

Si nous essayons d'appeler la méthodewrite depuis l'extérieur du packagelogging, 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...")
}
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 faire référence à quelque chose d'un autre package commençant par une lettre minuscule, il sait qu'il n'est pas exporté et génère donc une erreur du compilateur.

Le consignateur de ce didacticiel montre comment écrire du code exposant uniquement les parties que nous voulons que les autres packages utilisent. Étant donné que nous contrôlons les parties du package visibles à l'extérieur de celui-ci, nous sommes désormais en mesure d'apporter des modifications ultérieures sans affecter le code dépendant de notre package. Par exemple, si nous voulions désactiver les messages de niveauinfo uniquement lorsquedebug est faux, vous pouvez effectuer cette modification sans affecter aucune autre partie de votre API. Nous pourrions également modifier en toute sécurité le message de journal pour inclure davantage d'informations, telles que le répertoire à partir duquel le programme était exécuté.

Conclusion

Cet article a montré comment partager du code entre des packages tout en protégeant les détails d'implémentation de votre package. Cela vous permet d'exporter une API simple dont la compatibilité avec les versions antérieures sera rarement modifiée, tout en permettant des modifications privées dans votre package, selon les besoins, pour améliorer son fonctionnement à l'avenir. Ceci est considéré comme une pratique recommandée lors de la création de packages et de leurs API correspondantes.

Pour en savoir plus sur les packages dans Go, consultez nos articles surImporting Packages in Go etHow To Write Packages in Go, ou explorez l'intégralité de nosHow To Code in Go series.