Gehen Sie zur Software-Konfiguration

Gopher mit Fahne


Hallo allerseits! Nach fünf Jahren des Programmierens auf Go fand ich mich ganz
ein begeisterter Befürworter einer bestimmten Herangehensweise an die Programmkonfiguration. In diesem
Ich werde versuchen, seine Hauptideen in dem Artikel zu enthüllen, sowie einen kleinen zu teilen
eine Bibliothek, die diese Ideen umsetzt.


Offensichtlich ist der Artikel sehr subjektiv und gibt nicht vor, objektiv zu sein
wahrheiten. Ich hoffe jedoch, dass es für die Community nützlich sein und zur Reduzierung beitragen kann
Zeit, die für eine so einfache Aufgabe aufgewendet wurde.


Wovon redest du


Generell ist die Konfiguration meiner Meinung nach die Definition der Variablen unserer
Programme, deren Werte wir schon zur Laufzeit von außen bekommen können.
Dies können Argumente oder Befehlszeilenparameter, Umgebungsvariablen,
Konfigurationsdateien, die auf der Festplatte oder irgendwo im Netzwerk gespeichert sind, Datenbanktabellen
Daten und so weiter.


Da Go eine stark statisch getippte Sprache ist, möchten wir dies tun
Ermitteln und Abrufen von Werten für solche Variablen unter Berücksichtigung ihres Typs.


Es gibt eine große Anzahl von Open-Source-Bibliotheken oder sogar Frameworks,
Lösen solcher Probleme. Die meisten von ihnen repräsentieren ihre eigene Vision.
wie es geht.


Ich möchte über einen weniger verbreiteten Ansatz zur Programmkonfiguration sprechen.
Außerdem scheint mir dieser Ansatz der einfachste zu sein.


Paket kennzeichnen


Ja, das ist kein Scherz und ich möchte wirklich Ihre Aufmerksamkeit auf alles lenken
Das berühmte Go-Standardbibliothekspaket.


Auf den ersten Blick ist flag ein Werkzeug zum Arbeiten mit Befehlsparametern
Linien und nicht mehr. Dieses Paket kann aber auch als Schnittstelle verwendet werden.
Bestimmung der Parameter unseres Programms. Und im Kontext des diskutierten Ansatzes
flag hauptsächlich so verwendet.


Wie oben erwähnt, möchten wir typisierte Parameter haben.
Das flag Paket bietet die Möglichkeit, dies für die meisten Basistypen zu tun.
- flag.String() , flag.Int() und sogar flag.Duration() .


Für komplexere Typen wie []string oder time.Time gibt es eine Schnittstelle
flag.Value , mit dem Sie beschreiben können, flag.Value Sie den Wert eines Parameters daraus flag.Value
String-Darstellung .


Beispielsweise kann ein Parameter vom Typ time.Time folgendermaßen implementiert werden:


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

Eine wichtige Eigenschaft eines Pakets ist das Vorhandensein in der Standardbibliothek - flag is
Standard Weg, um Programme zu konfigurieren , was seine Wahrscheinlichkeit bedeutet
Die Nutzung zwischen verschiedenen Projekten und Bibliotheken ist höher als bei anderen
Bibliotheken in der Gemeinde.


Warum nicht flag benutzen?


Es scheint mir, dass andere Bibliotheken verwendet werden und aus zwei Gründen existieren:


  • Parameter werden nicht nur von der Kommandozeile gelesen
  • Ich möchte die Parameter strukturieren

Wenn Sie zum Beispiel Parameter aus Dateien lesen, ist alles mehr oder weniger klar
Dies später), dann über strukturelle Parameter lohnt es sich, ein paar Worte direkt zu sagen
jetzt.


Es gibt meiner Meinung nach nicht die beste Möglichkeit, die Konfiguration zu bestimmen
Programme als Strukturen, deren Felder andere Strukturen sein könnten und so
weiter:


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

Aus diesem Grund werden Bibliotheken genutzt und existieren
Frameworks, mit denen Sie auf diese Weise mit der Konfiguration arbeiten können.


Ich denke, flag sollte keine strukturellen Konfigurationsmöglichkeiten bieten.
Dies kann leicht mit ein paar Codezeilen (oder einer Bibliothek) erreicht werden
flagutil , das weiter unten besprochen wird).


Wenn Sie darüber nachdenken, führt die Existenz einer solchen Struktur zu einer starken
Konnektivität zwischen den verwendeten Komponenten.


Strukturelle Konfiguration


Die Idee ist, Parameter unabhängig von der Struktur zu definieren
Programme und so nah wie möglich an dem Ort, an dem sie verwendet werden - das heißt,
direkt auf Paketebene.


Angenommen, wir haben eine Client-Implementierung für einen Dienst (Datenbank,
API oder was auch immer) namens yoogle :


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

Um die yoogle.Config Struktur zu yoogle.Config , benötigen wir eine Funktion, die
registriert die Strukturfelder im empfangenen *flag.FlagSet .


Eine solche Funktion kann auf yoogle oder in einem Paket deklariert werden
yooglecfg (im Falle einer Drittanbieter-Bibliothek können wir eine solche Funktion schreiben
an anderer Stelle):


 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 } 

Um die Abhängigkeit vom flag Paket zu beseitigen, können Sie eine Schnittstelle mit definieren
erforderliche flag.FlagSet Methoden:


 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 } 

Und ob die Konfiguration von den Parameterwerten abhängt (zB unter den Parametern
Der Algorithmus von etwas wird angezeigt), die Funktion yooglecfg.Export() kann zurückgeben
Konstruktorfunktion, die nach dem Parsen aller Werte aufgerufen wird
Konfigurationen:


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

Mit Exportfunktionen können Sie Paketparameter definieren, ohne die Struktur zu kennen
Programmkonfiguration und wie man ihre Werte erhält.

github.com/gobwas/flagutil


Wir haben eine große Konfigurationsstruktur herausgefunden und unsere Parameter festgelegt
unabhängig, aber es ist noch nicht klar, wie man sie alle zusammenbringt und
werte.


flagutil dieses Problem zu lösen, wurde das Paket flagutil geschrieben.


Zusammenstellen der Parameter


Alle Parameter des Programms, seiner Pakete und Bibliotheken von Drittanbietern erhalten ihr Präfix
und werden auf der Paketebene main gesammelt:


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

Die Funktion flagutil.Subset() macht eine einfache Sache: Sie fügt ein Präfix hinzu
( "yoogle" ) zu allen Parametern, die in sub innerhalb des Rückrufs registriert sind.


Das Ausführen des Programms könnte nun so aussehen:


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

Parameterwerte abrufen


Alle Parameter in flag.FlagSet enthalten die Implementierung von flag.Value ,
Das hat eine Set(string) error - das heißt, es bietet eine Gelegenheit
Festlegen der Zeichenfolgendarstellung des Werts .


Es bleiben Werte aus jeder Quelle in Form von Schlüssel-Wert-Paaren und auszulesen
flag.Set(key, value) aufrufen.


Dies gibt uns die Möglichkeit, die Syntax der Befehlsparameter nicht einmal zu verwenden
Zeilen im flag Paket beschrieben. Sie können die Argumente in beliebiger Weise analysieren,
B. wie posix-Programmargumente .

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

Dementsprechend könnte die Datei config.json aussehen:


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

Fazit


Natürlich bin ich nicht der erste, der von einem solchen Ansatz spricht. Viele von
Die oben beschriebenen Ideen wurden bereits vor einigen Jahren auf die eine oder andere Weise verwendet
Ich habe bei MailRu gearbeitet.


Also, um die Konfiguration unserer Anwendung zu vereinfachen und keine Zeit zu verschwenden
Es wird vorgeschlagen, das nächste Konfigurationsframework zu studieren (oder sogar zu schreiben)
das Folgende:


  • Verwenden Sie flag als Schnittstelle für die Parameterdefinition
    das Programm
  • Exportieren Sie die Parameter jedes Pakets separat, ohne Kenntnis der Struktur und
    eine Möglichkeit, später Werte zu erhalten
  • Definieren Sie, wie Werte, Präfixe und Konfigurationsstrukturen in main gelesen werden sollen

Die flagutil Bibliothek wurde flagutil meiner Bekanntschaft mit der Bibliothek flagutil
Peterbourgon / ff - und ich würde kein flagutil schreiben, wenn nicht für einige
Unstimmigkeiten in Gebrauch.


Vielen Dank für Ihre Aufmerksamkeit!


Referenzen


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


All Articles