Ir configuração de software

Gopher com bandeira


Olá pessoal! Após cinco anos de programação no Go, me vi bastante
um fervoroso defensor de uma abordagem específica para a configuração do programa. Neste
Vou tentar revelar suas principais idéias no artigo, bem como compartilhar uma pequena
uma biblioteca que implementa essas idéias.


Obviamente, o artigo é muito subjetivo e não pretende ser objetivo
verdades. No entanto, espero que possa ser útil à comunidade e ajudar a reduzir
tempo gasto em uma tarefa tão trivial.


Do que você está falando?


Em geral, a configuração, na minha opinião, é a definição das variáveis ​​de nossa
programas cujos valores podemos obter de fora já em tempo de execução.
Pode ser argumentos ou parâmetros de linha de comando, variáveis ​​de ambiente,
arquivos de configuração armazenados em disco ou em qualquer lugar da rede, tabelas de banco de dados
dados e assim por diante.


Como o Go é uma linguagem tipicamente fortemente estática, gostaríamos de
determinar e obter valores para essas variáveis, levando em consideração seu tipo.


Há um grande número de bibliotecas de código aberto ou até estruturas,
resolver esses problemas. A maioria deles representa sua própria visão.
de como fazê-lo.


Eu gostaria de falar sobre uma abordagem menos comum para a configuração do programa.
Além disso, essa abordagem me parece a mais simples.


flag pacote


Sim, isso não é uma piada e eu realmente quero chamar sua atenção para tudo
O famoso pacote de biblioteca padrão Go.


À primeira vista, o flag é uma ferramenta para trabalhar com parâmetros de comando
linhas e nada mais. Mas este pacote também pode ser usado como uma interface.
determinar os parâmetros do nosso programa. E no contexto da abordagem discutida
flag usado principalmente dessa maneira.


Como mencionado acima, gostaríamos de digitar parâmetros.
O pacote flag fornece a capacidade de fazer isso para os tipos mais básicos.
- flag.String() , flag.Int() e até mesmo flag.Duration() .


Para tipos mais complexos, como []string ou time.Time existe uma interface
flag.Value , que permite descrever flag.Value obter o valor de um parâmetro a partir dele
representação de string .


Por exemplo, um parâmetro do tipo time.Time pode ser implementado assim:


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

Uma propriedade importante de um pacote é sua presença na biblioteca padrão - o flag é
maneira padrão de configurar programas , o que significa sua probabilidade
o uso entre diferentes projetos e bibliotecas é maior que outros
bibliotecas da comunidade.


Por que não usar flag ?


Parece-me que outras bibliotecas são usadas e existem por duas razões:


  • Os parâmetros são lidos não apenas na linha de comando
  • Eu quero estruturar os parâmetros

Se ler parâmetros, por exemplo, de arquivos, tudo fica mais ou menos claro (sobre
mais tarde), então sobre parâmetros estruturais, vale a pena dizer algumas palavras diretamente
agora


Na minha opinião, não existe a melhor maneira de determinar a configuração
programas como estruturas cujos campos poderiam ser outras estruturas e assim
além disso:


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

E parece-me que é por isso que as bibliotecas são usadas e existem
estruturas que permitem trabalhar com a configuração dessa maneira.


Acho que o flag não deve fornecer recursos de configuração estrutural.
Isso pode ser facilmente alcançado com algumas linhas de código (ou uma biblioteca
flagutil , discutido abaixo).


Além disso, se você pensar bem, a existência de tal estrutura leva a uma forte
conectividade entre os componentes utilizados.


Configuração estrutural


A idéia é definir parâmetros independentemente da estrutura
programas e o mais próximo possível do local onde são usados ​​- ou seja,
diretamente no nível do pacote.


Suponha que tenhamos uma implementação de cliente para algum serviço (banco de dados,
API ou o que for) chamado yoogle :


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

Para preencher a estrutura yoogle.Config , precisamos de uma função que
registra os campos de estrutura no *flag.FlagSet recebido.FlagSet.


Essa função pode ser declarada no yoogle pacote yoogle ou em um pacote
yooglecfg (no caso de uma biblioteca de terceiros, podemos escrever essa função
em outros lugares):


 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 a dependência do pacote flag , você pode definir uma interface com
métodos flag.FlagSet necessários:


 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 } 

E se a configuração depende dos valores dos parâmetros (por exemplo, entre os parâmetros
o algoritmo de algo está indicado), a função yooglecfg.Export() pode retornar
função construtora a ser chamada após analisar todos os valores
configurações:


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

As funções de exportação permitem definir parâmetros de pacote sem conhecer a estrutura
configuração do programa e como obter seus valores.

github.com/gobwas/flagutil


Nós descobrimos uma grande estrutura de configuração e fizemos nossos parâmetros
independente, mas ainda não está claro como reuni-los e reunir
valores.


Foi para resolver esse problema que o pacote flagutil foi gravado.


Juntando os parâmetros


Todos os parâmetros do programa, seus pacotes e bibliotecas de terceiros recebem seu prefixo
e são coletados no nível main do pacote:


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

A função flagutil.Subset() faz uma coisa simples: adiciona um prefixo
( "yoogle" ) para todos os parâmetros registrados no sub dentro do retorno de chamada.


A execução do programa agora pode ser assim:


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

Obter valores de parâmetro


Todos os parâmetros dentro de flag.FlagSet contêm a implementação de flag.Value ,
que possui um método de Set(string) error - ou seja, fornece uma oportunidade
definindo a representação de sequência do valor .


Resta ler valores de qualquer fonte na forma de pares de valores-chave e
faça uma flag.Set(key, value) .


Isso nos dá a oportunidade de nem usar a sintaxe dos parâmetros de comando
linhas descritas no pacote flag . Você pode analisar os argumentos, de qualquer forma,
por exemplo, como argumentos do 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{}, }), ) } 

Assim, o arquivo config.json pode ser assim:


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

Conclusão


É claro que não sou o primeiro a falar dessa abordagem. Muitos
as idéias descritas acima já foram usadas de uma maneira ou de outra há vários anos, quando
Eu trabalhei na MailRu.


Assim, para simplificar a configuração de nossa aplicação e não perder tempo com
estudando (ou mesmo escrevendo) o próximo quadro de configuração é proposto
o seguinte:


  • Use o flag como uma interface de definição de parâmetro
    o programa
  • Exporte os parâmetros de cada pacote separadamente, sem o conhecimento da estrutura e
    uma maneira de obter valores mais tarde
  • Defina como ler valores, prefixos e estrutura de configuração nos main

A biblioteca flagutil foi flagutil meu conhecimento da biblioteca
Peterbourgon / ff - e eu não escreveria flagutil , se não fosse por algum
discrepâncias em uso.


Obrigado pela atenção!


Referências


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


All Articles