Комментарии 35
Гениально класс круто
Представляю zerocfg — конфигурацию без лишних усилий
Неудачное название потому, что конфликтует с ZeroConf, Zero-configuration networking
ZeroConf - это общепринятое название для этой технологии. Любой сетевик, увидев слово zerocfg, употреблённое совсем в другом смысле, будет несколько озадачен...
Выглядит классно
Но статья менее понятна, чем README на ГитХабе 😬
Если нам нужно подгружать опции из альтернативного источника конфигурации, специфичного для нас или просто не поддержанного в текущий момент, мы можем это сделать!
А что с динамическими источниками вроде etcd?
А можно добавить возможность динамического перечитывания файла конфигурации каждые ххх секунд? Например, поменяли путь к логам или уровень сбора логов error -> debug и чтобы не стартовать приложение? Вот это мне всегда требуется ((
Выглядит неплохо.
На практике не пробовал, задаю вопрос исходя из статьи и ридми )
Есть ли механизм деприкейта?
Можно ли задавать префикс для энвов?
Как обрабатывается передача несуществующих значений в конфиге или флагах?
деприкейта нет, не очень понимаю как и для чего его стоит реализовавыть?
\\ Depricated на опцией в коде ведь можно указать, или это про рендер конфигурации чтобы там был варн?
префикс для энвов сейчас нельзя, но это вопрос 3 строчек кода, просто руки еще не дошли, поскольку дополировываю код основного пакета
несуществующие знания триггерят ошибку UnknownFields.. (для энв парсера она не триггерится только)
в статье про это есть, можно найти по ключ слову IsUnknown
В кобре есть hide (скрытые флаги), таким образом можно деприкейтить старые флаги, ну или собственно делать скрытые.
тут с точки зрения использования
- кобра - для кли - юзер взаимодействует с --help
- zerocfg - конфигурация приложения, юзер взаимодействует с кодом
мне кажется что для депрекейта достаточно коммента в коде
Еще через пару лет разработчики го опять выйдут из анабиоза в своей башне из слоновой кости, оглядятся по сторонам, и поймут, что конфиг должен быть частью официального набора инструментов.
Как предполагается менять значения конфига в тест кейсах?
Я в своё время наговнокодил библиотеку конфигов для go, но которая работает по другому принципу:
type SomeStruct {
field1 uint64 `env:"UINT" arg:"uint"`
}
type Config struct {
field1 string `env:"FIELD1" arg:"field1"`
field2 *SomeStruct `env:"FIELD2" arg:"field2"`
}
cfg, err := getConfig[Config](FromJson("base.json"), FromArg, FromEnv)
И в итоге получаем конфиг, который загружает параметры из json, если параметр задан в переменных среды, то берёт оттуда, а если заданы через аргументы, то из них.
Параметры во вложенных структурах определяются через имя этой структуры, т.е. в моём примере будут аргументы "--field2_field1" и его эквивалент в виде переменной среды "FIELD2_FIELD1".
Массивы пока не поддерживаются.
Пользуясь случаем, покажу свою, в которой у меня мержатся конфиги из переменных окружения, файлов или строк в формате env, ini и json.
https://github.com/Mansiper/GoConfiguration
type Config struct {
Field_1 int `env:"f1,required" def:"10"`
Field_2 []string `env:"f2,append" def:"a|b" sep:"|"`
Field_3 [3]time.Time `env:"f3" def:"now,now"`
Field_4 map[string]float64 `env:"f4" def:"a?1.1,b?2.2" sep2:"?"`
Field_5 SubConfig `env:"f5"`
Fiend_6 string `env:"GOPATH"`
Fiend_7 *float64 `env:"-"`
Field_8 []*int8 `env:"f8" def:"*nil,1,2,3,4,5"`
FIell_9 [2]*float32 `env:"f9,required" def:"1.23"`
}
type SubConfig struct {
Field_1 *uint32 `env:"sf1" def:"*nil"`
}
func main() {
config := &Config{}
cr := goconf.NewConfigReader().
RewriteValues(true).
AddEnvironment().
AddFile(".env").
AddFile("config.ini").
AddString("f5.sf1 = 10", goconf.FtEnv, "config 1").
AddFile("config.json").
EnsureHasNoErrors()
err := cr.ReadConfig(config)
if err != nil {
panic(err)
}
...
}
На мой взгляд, получалось довольно удобно в использовании.
Есть ещё koanf, у которого можно из желаемых источников собирать параметры в структуру
он в апи похож на viper, мне такое апи кажется бойлерплейтным, однако вплане количества источников у него нет равных и либа koanf хороша в этом плане
Я как раз люблю собирать параметры в какие-то структуры и потом ее использовать, не дергая какой-то объект за методы. Вот пример моего config.go текущего пет-проекта. Т.е. весь боилерплэйт умещается в одном файлике, который кладется рядом с main.go в папку cmd. А в main уже параметры читаются из структуры.
package main
import (
"fmt"
"os"
"strings"
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/parsers/toml"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/confmap"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"github.com/spf13/pflag"
)
// Config is application config struct
type Config struct {
Logger struct {
Level string `koanf:"level"`
} `koanf:"logger"`
Sentry struct {
DSN string `koanf:"dsn"`
TracesSampleRate float64 `koanf:"tracesSampleRate"`
} `koanf:"sentry"`
DB struct {
URL string `koanf:"url"`
}
GRPC struct {
Port int `koanf:"port"`
}
Services []ServiceConfig `koanf:"services"`
}
// ServiceConfig is a config for a service
type ServiceConfig struct {
Type string `koanf:"type"`
Parameters map[string]string `koanf:"parameters"`
}
const (
ServiceConfigTypeAsset = "asset"
ServiceConfigTypeUser = "user"
ServiceConfigTypePrice = "price"
ServiceConfigTypePortfolio = "portfolio"
ServiceConfigTypeTrade = "trade"
)
func getConfig() (*Config, error) {
var err error
k := koanf.New(".")
// Default values
defaults := map[string]interface{}{
"sentry.tracesSampleRate": 1.0,
}
err = k.Load(confmap.Provider(defaults, "."), nil)
if err != nil {
return nil, fmt.Errorf("can't load default config parameters: %w", err)
}
// Load command line and configs
f := pflag.NewFlagSet("config", pflag.ContinueOnError)
f.Usage = func() {
fmt.Println(f.FlagUsages())
os.Exit(0)
}
f.String("c", "", "Path to config file")
err = f.Parse(os.Args[1:])
if err != nil {
return nil, fmt.Errorf("can't parse command line arguments: %w", err)
}
// Load the config files provided in the commandline.
cFile, _ := f.GetString("c")
switch {
case strings.HasSuffix(cFile, "toml"):
if err := k.Load(file.Provider(cFile), toml.Parser()); err != nil {
return nil, fmt.Errorf("error loading file: %w", err)
}
case strings.HasSuffix(cFile, "yaml"):
if err := k.Load(file.Provider(cFile), yaml.Parser()); err != nil {
return nil, fmt.Errorf("error loading file: %w", err)
}
case strings.HasSuffix(cFile, "json"):
if err := k.Load(file.Provider(cFile), json.Parser()); err != nil {
return nil, fmt.Errorf("error loading file: %w", err)
}
}
// Load ENV
err = k.Load(env.Provider(ServiceName+"_", ".", func(s string) string {
return strings.ReplaceAll(strings.ToLower(
strings.TrimPrefix(s, ServiceName+"_")), "_", ".")
}), nil)
if err != nil {
return nil, fmt.Errorf("can't load env variables: %w", err)
}
// Unmarshal configs to struct
var config Config
err = k.Unmarshal("", &config)
if err != nil {
return nil, fmt.Errorf("can't unmarshal config: %w", err)
}
return &config, nil
}
Тут еще от нехер делать засунуты томл, json. ОБычно все запускаю в ENV так как с компоуза удобнее. Так что можно еще сократить, но накладные расходы на поддержку парсинга конфигов вряд ли фатальны.
раньше использовал вайпер с коброй. Но там вот реально надо было вроде отдельный init файл делать, директории с подкомандами и прочим. Т.е. уже решения для прям сложных гуевых приложений, в которых есть команды приложения и их собственные параметры и т.д.
это стандартный подход достаточно, он удобен и я не пытаюсь с ним бороться, но все проблемы которые я перечислил - применимы к нему, я сам таким всегда пользовался
var (
loggerLevel = zfg.Str("logger.level", "info", "Logger level")
sentryDSN = zfg.Str("sentry.dsn", "", "Sentry DSN")
sentryTracesSampleRate = zfg.Float64("sentry.tracesSampleRate", 1.0, "Sentry traces sample rate")
dbURL = zfg.Str("db.url", "", "Database URL")
port = zfg.Int("grpc.port", 8080, "gRPC port")
services = zfg.Map("services", nil, "List of service configs (JSON strings)")
)
func main() {
err := zfg.Parse(env.New())
if err != nil {
panic(err)
}
fmt.Println(zfg.Show())
}
как то так будет выглядеть подобная логика на зероконф
только в зероконф нужно обявлять опции конфигурации в пакете где она применяться, а не в одном файле
import zfg ...
Придумал вам маркетинговый лозунг:
"ZFGачь переменную, чтобы завелось"
🫣😁
Слишком сложно всё тут.
Как должно быть:
1. Создаём структуру
2. Заполняем все поля в структуру автоматически из окружения/.env/.yaml
одной строчкой кода (обязательно одной строчкой)
без заполнения никаких настроек,
если совпадает имя поля структуры и имя переменной окружения - то заполняем.
Весь код будет состоять из 1 строчки кода,
будет универсально для любых типов и названий полей.
(я уже так сделал почти, ещё доделаю)
Че только из map[string]any не делают)
Я сделал самую удобную либу для Go-конфига