Aller à la configuration logicielle

Gopher avec drapeau


Bonjour à tous! Après cinq ans de programmation sur Go, je me suis retrouvé assez
ardent défenseur d'une approche spécifique de la configuration des programmes. Dans ce
Je vais essayer de révéler ses principales idées dans l'article, ainsi que partager un petit
une bibliothèque qui met en œuvre ces idées.


De toute évidence, l'article est très subjectif et ne prétend pas être objectif
vérités. Cependant, j’espère que cela peut être utile à la communauté et aider à réduire
le temps consacré à une tâche aussi banale.


De quoi tu parles?


En général, la configuration, à mon avis, est la définition des variables de notre
des programmes dont nous pouvons obtenir les valeurs de l'extérieur dès l'exécution.
Il peut s'agir d'arguments ou de paramètres de ligne de commande, de variables d'environnement,
fichiers de configuration stockés sur disque ou n'importe où sur le réseau, tables de base de données
données et ainsi de suite.


Puisque Go est un langage typé fortement statique, nous aimerions
déterminer et obtenir des valeurs pour ces variables, en tenant compte de leur type.


Il existe un grand nombre de bibliothèques open source ou même de frameworks,
résoudre de tels problèmes. La plupart d'entre eux représentent leur propre vision.
comment le faire.


Je voudrais parler d'une approche moins courante de la configuration du programme.
De plus, cette approche me semble la plus simple.


Forfait flag


Oui, ce n'est pas une blague et je veux vraiment attirer votre attention sur tout
Le célèbre package de bibliothèque standard Go.


À première vue, flag est un outil pour travailler avec les paramètres de commande
lignes et pas plus. Mais ce package peut également être utilisé comme interface.
déterminer les paramètres de notre programme. Et dans le cadre de l'approche discutée
flag principalement utilisé de cette façon.


Comme mentionné ci-dessus, nous aimerions avoir des paramètres saisis.
Le package d' flag offre la possibilité de le faire pour la plupart des types de base.
- flag.String() , flag.Int() et même flag.Duration() .


Pour les types plus complexes, comme []string ou time.Time il existe une interface
flag.Value , qui vous permet de décrire flag.Value obtenir la valeur d'un paramètre
représentation de chaîne .


Par exemple, un paramètre de type time.Time peut être implémenté comme ceci:


 // TimeValue is an implementation of flag.Value interface. type TimeValue struct { P *time.Time Layout string } func (t *TimeValue) Set(s string) error { v, err := time.Parse(t.Layout, s) if err == nil { (*tP) = v } return err } func (t *TimeValue) String() string { return tPFormat(t.Layout) } 

Une propriété importante d'un paquet est sa présence dans la bibliothèque standard - le flag est
moyen standard de configurer des programmes , ce qui signifie sa probabilité
l'utilisation entre différents projets et bibliothèques est plus élevée que d'autres
les bibliothèques de la communauté.


Pourquoi ne pas utiliser le flag ?


Il me semble que d'autres bibliothèques sont utilisées et existent pour deux raisons:


  • Les paramètres sont lus non seulement à partir de la ligne de commande
  • Je veux structurer les paramètres

Si vous lisez des paramètres, par exemple à partir de fichiers, tout est plus ou moins clair (environ
plus tard), puis sur les paramètres structurels, il vaut la peine de dire quelques mots directement
maintenant.


Il n'y a, à mon avis, pas la meilleure façon de déterminer la configuration
programmes en tant que structures dont les domaines pourraient être d'autres structures et ainsi
en outre:


 type AppConfig struct { Port int Database struct { Endpoint string Timeout time.Duration } ... } 

Et il me semble que c'est pourquoi les bibliothèques sont utilisées et existent
des cadres qui vous permettent de travailler avec la configuration de cette façon.


Je pense que le flag ne devrait pas fournir de capacités de configuration structurelle.
Cela peut être facilement réalisé avec quelques lignes de code (ou une bibliothèque
flagutil , dont il est question ci-dessous).


De plus, si vous y réfléchissez, l’existence d’une telle structure conduit à une forte
connectivité entre les composants utilisés.


Configuration structurelle


L'idée est de définir des paramètres quelle que soit la structure
programmes et aussi près que possible de l'endroit où ils sont utilisés - c'est-à-dire
directement au niveau du package.


Supposons que nous ayons une implémentation client pour certains services (base de données,
API ou autre) appelé yoogle :


 package yoogle type Config struct { Endpoint string Timeout time.Duration } func New(c *Config) *Client { // ... } 

Pour remplir la structure yoogle.Config , nous avons besoin d'une fonction qui
enregistre les champs de structure dans l' *flag.FlagSet .


Une telle fonction peut être déclarée au yoogle package yoogle ou dans un package
yooglecfg (dans le cas d'une bibliothèque tierce, on peut écrire une telle fonction
ailleurs):


 package yooglecfg import ( "flag" "app/yoogle" ) func Export(flag *flag.FlagSet) *yoogle.Config { var c yoogle.Config flag.StringVar(&c.Endpoint, "endpoint", "https://example.com", "endpoint for our API", ) flag.DurationVar(&c.Timeout, "timeout", time.Second, "timeout for operations", ) return &c } 

Pour éliminer la dépendance à l'égard du package d' flag , vous pouvez définir une interface avec
méthodes flag.FlagSet nécessaires:


 package yooglecfg import "app/yoogle" type FlagSet interface { StringVar(p *string, name, value, desc string) } func Export(flag FlagSet) *yoogle.Config { var c yoogle.Config flag.StringVar(&c.Endpoint, "endpoint", "https://example.com", "endpoint for our API", ) return &c } 

Et si la configuration dépend des valeurs des paramètres (par exemple, parmi les paramètres
l'algorithme de quelque chose est indiqué), la fonction yooglecfg.Export() peut retourner
fonction constructeur à appeler après l' analyse de toutes les valeurs
configurations:


 package yooglecfg import "app/yoogle" type FlagSet interface { StringVar(p *string, name, value, desc string) } func Export(flag FlagSet) func() *yoogle.Config { var algorithm string flag.StringVar(&algorithm, "algorithm", "quick", "algorithm used to do something", ) var c yoogle.Config return func() *yoogle.Config { switch algorithm { case "quick": c.Impl = quick.New() case "merge": c.Impl = merge.New() case "bubble": panic(...) } return c } } 

Les fonctions d'exportation vous permettent de définir des paramètres de package sans connaître la structure
configuration du programme et comment obtenir leurs valeurs.

github.com/gobwas/flagutil


Nous avons trouvé une grande structure de configuration et fait nos paramètres
indépendant, mais on ne sait pas encore comment les rassembler et obtenir
valeurs.


C'est pour résoudre ce problème que le package flagutil été écrit.


Assembler les paramètres


Tous les paramètres du programme, ses packages et les bibliothèques tierces reçoivent leur préfixe
et sont collectés au niveau du package main :


 package main import ( "flag" "app/yoogle" "app/yooglecfg" "github.com/gobwas/flagutil" ) func main() { flags := flag.NewFlagSet("my-app", flag.ExitOnError) var port int flag.IntVar(&port, "port", 4050, "port to bind to", ) var config *yoogle.Config flagutil.Subset(flags, "yoogle", func(sub *flag.FlagSet) { config = yooglecfg.Export(sub) }) } 

La fonction flagutil.Subset() fait une chose simple: elle ajoute un préfixe
( "yoogle" ) à tous les paramètres enregistrés dans sub à l'intérieur du rappel.


L'exécution du programme peut maintenant ressembler à ceci:


 app -port 4050 -yoogle.endpoint https://example.com -yoogle.timeout 10s 

Obtenir les valeurs des paramètres


Tous les paramètres à l'intérieur de flag.FlagSet contiennent l'implémentation de flag.Value ,
qui a une méthode d' Set(string) error - c'est-à-dire qu'elle fournit une opportunité
définition de la représentation sous forme de chaîne de la valeur .


Il reste à lire les valeurs de n'importe quelle source sous la forme de paires clé-valeur et
effectuer un flag.Set(key, value) .


Cela nous donne la possibilité de ne même pas utiliser la syntaxe des paramètres de commande
lignes décrites dans le package de flag . Vous pouvez analyser les arguments, de toute façon,
par exemple, comme les arguments du programme posix .

 package main func main() { flags := flag.NewFlagSet("my-app", flag.ExitOnError) // ... flags.String( "config", "/etc/app/config.json", "path to configuration file", ) flagutil.Parse(flags, // First, use posix arguments syntax instead of `flag`. // Just to illustrate that it is possible. flagutil.WithParser(&pargs.Parser{ Args: os.Args[1:], }), // Then lookup for "config" flag value and try to // parse its value as a json configuration file. flagutil.WithParser(&file.Parser{ PathFlag: "config", Syntax: &json.Syntax{}, }), ) } 

Par conséquent, le fichier config.json peut ressembler à ceci:


 { "port": 4050, "yoogle": { "endpoint": "https://example.com", "timeout": "10s" ... } } 

Conclusion


Bien sûr, je ne suis pas le premier à parler d'une telle approche. Beaucoup de
les idées décrites ci-dessus ont déjà été utilisées d'une manière ou d'une autre il y a plusieurs années,
J'ai travaillé chez MailRu.


Donc, pour simplifier la configuration de notre application et ne pas perdre de temps
étudier (ou même écrire) le prochain cadre de configuration est proposé
ce qui suit:


  • Utiliser l' flag comme interface de définition de paramètre
    le programme
  • Exportez les paramètres de chaque package séparément, sans connaître la structure et
    un moyen d'obtenir des valeurs plus tard
  • Définir comment lire les valeurs, les préfixes et la structure de configuration dans main

La bibliothèque flagutil a été flagutil ma connaissance de la bibliothèque
Peterbourgon / ff - et je n'écrirais pas flagutil , sinon pour certains
les écarts d'utilisation.


Merci de votre attention!


Les références


Source: https://habr.com/ru/post/fr479882/


All Articles