اذهب تكوين البرنامج

غوفر مع العلم


مرحبا بالجميع! بعد خمس سنوات من البرمجة على Go ، وجدت نفسي تمامًا
مؤيد متحمس لنهج معين لتكوين البرنامج. في هذا
سأحاول الكشف عن أفكاره الرئيسية في المقال ، وكذلك مشاركة صغيرة
مكتبة تنفذ هذه الأفكار.


من الواضح أن المقال موضوعي للغاية ولا يدعي أنه موضوعي
الحقيقة. ومع ذلك ، آمل أن تكون مفيدة للمجتمع وتساعد في الحد منها
الوقت الذي يقضيه في هذه المهمة التافهة.


عن ماذا تتحدث


بشكل عام ، التكوين ، في رأيي ، هو تعريف المتغيرات الخاصة بنا
البرامج التي يمكننا الحصول عليها من الخارج بالفعل في وقت التشغيل.
يمكن أن يكون وسيطات أو معلمات سطر الأوامر ، متغيرات البيئة ،
ملفات التكوين المخزنة على القرص أو في أي مكان على الشبكة ، وجداول قاعدة البيانات
البيانات وهلم جرا.


بما أن Go هي لغة مكتوبة بشكل ثابت ، نود أن
تحديد والحصول على قيم لهذه المتغيرات ، مع مراعاة نوعها.


هناك عدد كبير من المكتبات المفتوحة المصدر أو حتى الأُطر ،
حل مثل هذه المشاكل. معظمهم يمثلون رؤيتهم الخاصة.
كيف نفعل ذلك.


أود التحدث عن طريقة أقل شيوعًا لتهيئة البرنامج.
علاوة على ذلك ، يبدو لي هذا النهج أكثر بساطة.


حزمة flag


نعم ، هذه ليست مزحة وأريد حقًا أن أوجه انتباهكم إلى كل شيء
حزمة مكتبة Go القياسية الشهيرة.


للوهلة الأولى ، يمثل flag أداة للعمل مع معلمات الأوامر
خطوط وليس أكثر. ولكن يمكن أيضًا استخدام هذه الحزمة كواجهة.
تحديد معالم برنامجنا. وفي سياق النهج الذي تمت مناقشته
يستخدم flag المقام الأول بهذه الطريقة.


كما ذكر أعلاه ، نود أن يكون لديك المعلمات المكتوبة.
توفر حزمة flag القدرة على القيام بذلك لمعظم الأنواع الأساسية.
- flag.String() ، و flag.Int() وحتى flag.Duration() .


لأنواع أكثر تعقيدًا ، مثل []string أو time.Time هناك واجهة
flag.Value ، والذي يسمح لك بوصف flag.Value الحصول على قيمة المعلمة منه
تمثيل سلسلة .


على سبيل المثال ، معلمة type time.Time يمكن تنفيذها مثل هذا:


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

من الخصائص المهمة للحزمة وجودها في المكتبة القياسية - flag هو
الطريقة القياسية لتكوين البرامج ، مما يعني احتمالها
الاستخدام بين المشروعات المختلفة والمكتبات أعلى من غيرها
المكتبات في المجتمع.


لماذا لا تستخدم flag ؟


يبدو لي أن المكتبات الأخرى مستخدمة وموجودة لسببين:


  • تتم قراءة المعلمات ليس فقط من سطر الأوامر
  • أريد أن هيكل المعلمات

إذا كانت معلمات القراءة ، على سبيل المثال ، من الملفات ، فسيكون كل شيء أكثر أو أقل وضوحًا (حول
هذا لاحقًا) ، ثم حول المعلمات الهيكلية يجدر قول بضع كلمات مباشرةً
الآن.


في رأيي ، ليست هناك أفضل طريقة لتحديد التكوين
البرامج مثل الهياكل التي يمكن أن تكون مجالاتها هياكل أخرى وهكذا
أبعد من ذلك:


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

ويبدو لي أن هذا هو سبب استخدام المكتبات ووجودها
الأطر التي تسمح لك بالعمل مع التكوين بهذه الطريقة.


أعتقد أن flag يجب ألا يوفر قدرات التكوين الهيكلي.
يمكن تحقيق ذلك بسهولة من خلال بضعة أسطر من التعليمات البرمجية (أو مكتبة
flagutil ، والتي تناقش أدناه).


علاوة على ذلك ، إذا فكرت في الأمر ، فإن وجود مثل هذا الهيكل يؤدي إلى وجود قوة
الاتصال بين المكونات المستخدمة.


التكوين الهيكلي


والفكرة هي لتحديد المعلمات بغض النظر عن الهيكل
البرامج وأقرب ما يمكن إلى المكان الذي يتم استخدامه فيه - أي ،
مباشرة على مستوى الحزمة.


لنفترض أن لدينا تطبيق العميل لبعض الخدمات (قاعدة البيانات ،
واجهة برمجة التطبيقات (API) أو أي شيء آخر) تسمى yoogle :


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

yoogle.Config بنية yoogle.Config ، نحتاج إلى وظيفة
يسجل حقول البنية في علامة *flag.FlagSet .


يمكن الإعلان عن هذه yoogle حزمة yoogle أو في حزمة
yooglecfg (في حالة مكتبة الطرف الثالث ، يمكننا كتابة مثل هذه الوظيفة
في أي مكان آخر):


 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 } 

لإزالة التبعية على حزمة flag ، يمكنك تحديد واجهة بها
علم ضروري. طرق تعيين:


 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 } 

وإذا كان التكوين يعتمد على قيم المعلمات (على سبيل المثال ، بين المعلمات
يشار إلى خوارزمية شيء) ، يمكن أن تعود وظيفة yooglecfg.Export()
يتم استدعاء وظيفة المنشئ بعد تحليل جميع القيم
التكوين:


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

تسمح لك وظائف التصدير بتحديد معلمات الحزمة دون معرفة الهيكل
تكوين البرنامج وكيفية الحصول على قيمهم.

github.com/gobwas/flagutil


لقد توصلنا إلى بنية تكوين كبيرة وجعلنا معاييرنا
مستقلة ، ولكن ليس من الواضح بعد كيفية الجمع بينهما والحصول عليها
القيم.


كان لحل هذه المشكلة أن حزمة flagutil .


وضع المعلمات معا


جميع معلمات البرنامج وحزمه ومكتبات الطرف الثالث تتلقى بادئة
ويتم جمعها على مستوى الحزمة 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) }) } 

تقوم وظيفة flagutil.Subset() بعمل بسيط: تضيف بادئة
( "yoogle" ) لجميع المعلمات المسجلة في sub داخل رد الاتصال.


قد يبدو تشغيل البرنامج الآن كما يلي:


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

الحصول على قيم المعلمة


جميع المعلمات داخل flag.FlagSet تحتوي على تنفيذ flag.Value ،
التي لديها طريقة Set(string) error - وهذا هو ، فإنه يوفر فرصة
ضبط تمثيل السلسلة للقيمة .


يبقى لقراءة القيم من أي مصدر في شكل أزواج قيمة المفتاح و
قم بإجراء flag.Set(key, value) .


هذا يتيح لنا الفرصة حتى لا نستخدم بناء جملة معلمات الأمر
الخطوط الموضحة في حزمة flag . يمكنك تحليل الحجج ، بأي شكل من الأشكال ،
على سبيل المثال مثل وسائط برنامج 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{}, }), ) } 

وفقًا لذلك ، قد يبدو ملف config.json كما يلي:


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

استنتاج


بالطبع ، أنا لست أول من تحدث عن مثل هذا النهج. كثير من
كانت الأفكار الموضحة أعلاه تستخدم بالفعل بطريقة أو بأخرى منذ عدة سنوات ، متى
لقد عملت في MailRu.


لذلك ، لتبسيط تكوين تطبيقنا وليس إضاعة الوقت في
دراسة (أو حتى كتابة) إطار التكوين التالي المقترح
ما يلي:


  • استخدام flag كواجهة تعريف المعلمة
    البرنامج
  • تصدير المعلمات من كل حزمة على حدة ، دون معرفة الهيكل و
    وسيلة للحصول على القيم في وقت لاحق
  • حدد كيفية قراءة القيم والبادئات وهيكل التكوين بشكل main

flagutil مكتبة flagutil معرفتي بالمكتبة
Peterbourgon / ff - وأنا لن أكتب flagutil ، إن لم يكن للبعض
التناقضات في الاستخدام.


شكرا لاهتمامكم!


مراجع


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


All Articles