Pergi konfigurasi perangkat lunak

Gopher dengan bendera


Halo semuanya! Setelah lima tahun pemrograman di Go, saya menemukan diri saya cukup
pendukung kuat pendekatan khusus untuk konfigurasi program. Dalam hal ini
Saya akan mencoba mengungkap ide utamanya dalam artikel tersebut, sekaligus membagikan yang kecil
perpustakaan yang mengimplementasikan ide-ide ini.


Jelas, artikel itu sangat subyektif dan tidak berpura-pura objektif
kebenaran Namun, saya berharap ini dapat bermanfaat bagi masyarakat dan membantu mengurangi
waktu yang dihabiskan untuk tugas sepele seperti itu.


Apa yang kamu bicarakan


Secara umum, konfigurasi, menurut pendapat saya, adalah definisi dari variabel kita
program yang nilainya bisa kita dapatkan dari luar sudah saat runtime.
Ini bisa berupa argumen atau parameter baris perintah, variabel lingkungan,
file konfigurasi disimpan di disk atau di mana saja di jaringan, tabel database
data dan sebagainya.


Karena Go adalah bahasa yang diketik sangat statis, kami ingin
tentukan dan dapatkan nilai untuk variabel-variabel tersebut, dengan mempertimbangkan tipenya.


Ada sejumlah besar perpustakaan open-source atau bahkan kerangka kerja,
memecahkan masalah seperti itu. Sebagian besar dari mereka mewakili visi mereka sendiri.
bagaimana melakukannya.


Saya ingin berbicara tentang pendekatan yang kurang umum untuk konfigurasi program.
Terlebih lagi, pendekatan ini menurut saya paling sederhana.


Paket flag


Ya, ini bukan lelucon dan saya benar-benar ingin menarik perhatian Anda untuk semuanya
Paket perpustakaan standar Go yang terkenal.


Sekilas, flag adalah alat untuk bekerja dengan parameter perintah
garis dan tidak lebih. Namun paket ini juga bisa digunakan sebagai antarmuka.
menentukan parameter program kami. Dan dalam konteks pendekatan yang dibahas
flag terutama digunakan seperti itu.


Seperti disebutkan di atas, kami ingin mengetik parameter.
Paket flag menyediakan kemampuan untuk melakukan ini untuk sebagian besar tipe dasar.
- flag.String() , flag.Int() dan bahkan flag.Duration() .


Untuk tipe yang lebih kompleks, seperti []string atau time.Time ada antarmuka
flag.Value , yang memungkinkan Anda menjelaskan flag.Value mendapatkan nilai parameter dari parameter tersebut
representasi string .


Misalnya, parameter tipe time.Time dapat diterapkan seperti ini:


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

Properti penting dari sebuah paket adalah keberadaannya di pustaka standar - flag
cara standar untuk mengkonfigurasi program , yang berarti probabilitasnya
penggunaan antara proyek dan perpustakaan berbeda lebih tinggi dari yang lain
perpustakaan di komunitas.


Mengapa tidak menggunakan flag ?


Sepertinya saya bahwa perpustakaan lain digunakan dan ada karena dua alasan:


  • Parameter dibaca tidak hanya dari baris perintah
  • Saya ingin menyusun parameter

Jika membaca parameter, misalnya, dari file, semuanya lebih atau kurang jelas (tentang
ini nanti), lalu tentang parameter struktural, ada baiknya mengucapkan beberapa kata secara langsung
sekarang


Menurut saya, tidak ada cara terbaik untuk menentukan konfigurasi
program sebagai struktur yang bidangnya bisa struktur lain dan sebagainya
selanjutnya:


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

Dan bagi saya itu sebabnya perpustakaan digunakan dan ada
kerangka kerja yang memungkinkan Anda untuk bekerja dengan konfigurasi seperti itu.


Saya pikir flag seharusnya tidak menyediakan kapabilitas konfigurasi struktural.
Ini dapat dengan mudah dicapai dengan beberapa baris kode (atau perpustakaan
flagutil , yang dibahas di bawah).


Apalagi kalau dipikir-pikir, keberadaan struktur seperti itu mengarah ke yang kuat
konektivitas antar komponen yang digunakan.


Konfigurasi struktural


Idenya adalah untuk mendefinisikan parameter terlepas dari struktur
program dan sedekat mungkin ke tempat mereka digunakan - yaitu,
langsung di tingkat paket.


Misalkan kita memiliki implementasi klien untuk beberapa layanan (database,
API atau apa pun) yang disebut yoogle :


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

Untuk mengisi struktur yoogle.Config , kita memerlukan fungsi itu
mendaftar bidang struktur di *flag.FlagSet diterima.


Fungsi semacam itu dapat dideklarasikan pada yoogle paket yoogle atau dalam suatu paket
yooglecfg (dalam hal perpustakaan pihak ketiga, kita bisa menulis fungsi seperti itu
di tempat lain):


 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 } 

Untuk menghilangkan ketergantungan pada paket flag , Anda dapat mendefinisikan antarmuka dengan
flag.FlagSet diperlukan.FlagMengatur metode:


 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 } 

Dan jika konfigurasi tergantung pada nilai parameter (misalnya, di antara parameter
algoritma sesuatu ditunjukkan), fungsi yooglecfg.Export() dapat kembali
fungsi konstruktor dipanggil setelah parsing semua nilai
konfigurasi:


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

Fungsi ekspor memungkinkan Anda untuk menentukan parameter paket tanpa mengetahui strukturnya
konfigurasi program dan cara mendapatkan nilainya.

github.com/gobwas/flagutil


Kami menemukan struktur konfigurasi besar dan membuat parameter kami
mandiri, tetapi belum jelas bagaimana menyatukan mereka semua
nilai-nilai.


Untuk mengatasi masalah ini, paket flagutil ditulis.


Menyatukan parameter


Semua parameter program, paketnya, dan pustaka pihak ketiga menerima awalannya
dan dikumpulkan di tingkat paket 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) }) } 

Fungsi flagutil.Subset() melakukan hal sederhana: ia menambahkan awalan
( "yoogle" ) ke semua parameter yang terdaftar di sub di dalam panggilan balik.


Menjalankan program sekarang mungkin terlihat seperti ini:


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

Dapatkan nilai parameter


Semua parameter di dalam flag.FlagSet berisi implementasi flag.Value ,
yang memiliki metode Set(string) error - yaitu, ia memberikan peluang
mengatur representasi string dari nilai .


Masih membaca nilai dari sumber mana pun dalam bentuk pasangan nilai kunci dan
buat flag.Set(key, value) .


Ini memberi kita kesempatan untuk tidak menggunakan sintaks dari parameter perintah
garis yang dijelaskan dalam paket flag . Anda dapat menguraikan argumen, dengan cara apa pun,
mis. menyukai argumen program 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{}, }), ) } 

Dengan demikian, file config.json mungkin terlihat seperti ini:


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

Kesimpulan


Tentu saja, saya bukan orang pertama yang berbicara tentang pendekatan seperti itu. Banyak
ide-ide yang dijelaskan di atas sudah digunakan satu atau lain cara beberapa tahun yang lalu, ketika
Saya bekerja di MailRu.


Jadi, untuk mempermudah konfigurasi aplikasi kita dan tidak membuang waktu
mempelajari (atau bahkan menulis) kerangka konfigurasi selanjutnya diusulkan
berikut ini:


  • Gunakan flag sebagai antarmuka definisi parameter
    programnya
  • Ekspor parameter dari setiap paket secara terpisah, tanpa sepengetahuan struktur dan
    cara untuk mendapatkan nilai nanti
  • Tetapkan cara membaca nilai, awalan, dan struktur konfigurasi di main

Perpustakaan flagutil kenalan saya dengan perpustakaan
Peterbourgon / ff - dan saya tidak akan menulis flagutil , jika tidak untuk beberapa
perbedaan yang digunakan.


Terima kasih atas perhatian anda!


Referensi


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


All Articles