Ir a la configuración del software

Gopher con bandera


Hola a todos! Después de cinco años de programación en Go, me encontré bastante
un ferviente defensor de un enfoque específico para la configuración del programa. En este
Trataré de revelar sus ideas principales en el artículo, así como compartir un pequeño
Una biblioteca que implementa estas ideas.


Obviamente, el artículo es muy subjetivo y no pretende ser objetivo.
verdades Sin embargo, espero que pueda ser útil para la comunidad y ayudar a reducir
tiempo dedicado a una tarea tan trivial.


De que estas hablando


En general, la configuración, en mi opinión, es la definición de las variables de nuestro
programas cuyos valores podemos obtener desde afuera ya en tiempo de ejecución.
Pueden ser argumentos o parámetros de línea de comando, variables de entorno,
archivos de configuración almacenados en el disco o en cualquier lugar de la red, tablas de bases de datos
datos y así sucesivamente.


Como Go es un lenguaje tipeado muy estático, nos gustaría
determinar y obtener valores para tales variables, teniendo en cuenta su tipo.


Hay una gran cantidad de bibliotecas de código abierto o incluso marcos,
resolviendo tales problemas. La mayoría de ellos representan su propia visión.
de cómo hacerlo.


Me gustaría hablar sobre un enfoque menos común para la configuración del programa.
Además, este enfoque me parece el más simple.


Paquete de la flag


Sí, esto no es una broma y realmente quiero llamar su atención sobre todo
El famoso paquete de biblioteca estándar Go.


A primera vista, flag es una herramienta para trabajar con parámetros de comando
líneas y no más. Pero este paquete también se puede usar como una interfaz.
determinando los parámetros de nuestro programa. Y en el contexto del enfoque discutido
flag usa principalmente de esa manera.


Como se mencionó anteriormente, nos gustaría tener parámetros escritos.
El paquete de flag proporciona la capacidad de hacer esto para la mayoría de los tipos básicos.
- flag.String() , flag.Int() e incluso flag.Duration() .


Para tipos más complejos, como []string o tiempo. time.Time hay una interfaz
flag.Value , que le permite describir flag.Value obtener el valor de un parámetro
Representación de cadena .


Por ejemplo, un parámetro de tipo time.Time puede implementarse así:


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

Una propiedad importante de un paquete es su presencia en la biblioteca estándar : el flag es
forma estándar de configurar programas , lo que significa su probabilidad
El uso entre diferentes proyectos y bibliotecas es mayor que otros
bibliotecas en la comunidad.


¿Por qué no usar la flag ?


Me parece que otras bibliotecas se usan y existen por dos razones:


  • Los parámetros se leen no solo desde la línea de comando
  • Quiero estructurar los parametros

Si lee parámetros, por ejemplo, de archivos, todo está más o menos claro (aproximadamente
esto más adelante), luego, sobre los parámetros estructurales, vale la pena decir algunas palabras directamente
ahora


En mi opinión, no hay la mejor manera de determinar la configuración
programas como estructuras cuyos campos podrían ser otras estructuras y así
además:


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

Y me parece que es por eso que las bibliotecas se usan y existen
marcos que le permiten trabajar con la configuración de esa manera.


Creo que flag no debería proporcionar capacidades de configuración estructural.
Esto se puede lograr fácilmente con unas pocas líneas de código (o una biblioteca
flagutil , que se analiza a continuación).


Además, si lo piensas, la existencia de tal estructura conduce a una fuerte
conectividad entre los componentes utilizados.


Configuración estructural


La idea es definir parámetros independientemente de la estructura
programas y lo más cerca posible del lugar donde se usan, es decir,
directamente a nivel de paquete.


Supongamos que tenemos una implementación de cliente para algún servicio (base de datos,
API o lo que sea) llamado yoogle :


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

Para llenar la estructura yoogle.Config , necesitamos una función que
registra los campos de estructura en el *flag.FlagSet recibido.


Dicha función se puede declarar a yoogle paquete de yoogle o en un paquete
yooglecfg (en el caso de una biblioteca de terceros, podemos escribir dicha función
en otro lugar):


 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 } 

Para eliminar la dependencia del paquete de flag , puede definir una interfaz con
flag.FlagSet necesaria Métodos de conjunto de flag.FlagSet :


 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 } 

Y si la configuración depende de los valores de los parámetros (por ejemplo, entre los parámetros
se indica el algoritmo de algo), la función yooglecfg.Export() puede devolver
función de constructor que se llamará después de analizar todos los valores
configuraciones:


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

Las funciones de exportación le permiten definir los parámetros del paquete sin conocer la estructura
configuración del programa y cómo obtener sus valores.

github.com/gobwas/flagutil


Descubrimos una gran estructura de configuración e hicimos nuestros parámetros
independiente, pero aún no está claro cómo reunirlos a todos y obtener
valores


Fue para resolver este problema que se flagutil paquete flagutil .


Poniendo los parámetros juntos


Todos los parámetros del programa, sus paquetes y bibliotecas de terceros reciben su prefijo
y se recopilan en el nivel de paquete 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 función flagutil.Subset() hace algo simple: agrega un prefijo
( "yoogle" ) a todos los parámetros registrados en sub dentro de la devolución de llamada.


Ejecutar el programa ahora puede verse así:


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

Obtener valores de parámetros


Todos los parámetros dentro de flag.FlagSet contienen la implementación de flag.Value ,
que tiene un método de Set(string) error , es decir, proporciona una oportunidad
establecer la representación de cadena del valor .


Queda por leer los valores de cualquier fuente en forma de pares clave-valor y
hacer una flag.Set(key, value) .


Esto nos da la oportunidad de ni siquiera usar la sintaxis de los parámetros del comando
líneas descritas en el paquete de flag . Puede analizar los argumentos, de cualquier manera,
por ejemplo, como argumentos del programa 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{}, }), ) } 

En consecuencia, el archivo config.json puede verse así:


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

Conclusión


Por supuesto, no soy el primero en hablar de tal enfoque. Muchos de
Las ideas descritas anteriormente ya se utilizaron de una forma u otra hace varios años, cuando
Trabajé en MailRu.


Entonces, para simplificar la configuración de nuestra aplicación y no perder tiempo en
estudiar (o incluso escribir) se propone el siguiente marco de configuración
lo siguiente:


  • Use el flag como una interfaz de definición de parámetros
    el programa
  • Exportar los parámetros de cada paquete por separado, sin conocimiento de la estructura y
    una forma de obtener valores más tarde
  • Definir cómo leer valores, prefijos y estructura de configuración en main

La biblioteca flagutil se flagutil mi conocimiento de la biblioteca.
Peterbourgon / ff - y no escribiría flagutil , si no fuera por algunos
discrepancias en el uso.


Gracias por su atencion!


Referencias


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


All Articles