1. مقدمة حول Viper

golang viper

Comprehending the need for a configuration solution in Go applications

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

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

تتناول حلول التكوين مثل Viper هذه المخاوف من خلال توفير نهج موحد يدعم احتياجات وتنسيقات التكوين المتنوعة.

نظرة عامة على Viper ودوره في إدارة التكوينات

يعتبر Viper مكتبة تكوين شاملة لتطبيقات Go، حيث تهدف إلى أن تكون الحل الأساسي لجميع احتياجات التكوين. تتماشى Viper مع الممارسات المحددة في منهجية Twelve-Factor التي تشجع على تخزين التكوين في البيئة لتحقيق قابلية التنقل بين بيئات التنفيذ.

يؤدي Viper دوراً حاسمًا في إدارة التكوينات من خلال:

  • قراءة وتمرير ملفات التكوين بتنسيقات مختلفة مثل JSON و TOML و YAML و HCL وغيرها.
  • تجاوز قيم التكوين بمتغيرات البيئة، مما يلتزم بمبدأ التكوين الخارجي.
  • ربط وقراءة العلمات في سطر الأوامر للسماح بضبط ديناميكي لخيارات التكوين أثناء التشغيل.
  • السماح بتعيين قيم افتراضية داخل التطبيق لخيارات التكوين غير المقدمة خارجيًا.
  • مراقبة التغييرات في ملفات التكوين وإعادة التحميل الحي، مما يوفر المرونة ويقلل من وقت التوقف لتغييرات التكوين.

2. تثبيت وإعداد

تثبيت Viper باستخدام Go modules

لإضافة Viper إلى مشروع Go الخاص بك، تأكد من أن مشروعك يستخدم بالفعل وحدات Go لإدارة الاعتمادات. إذا كان لديك مشروع Go بالفعل، فمن المحتمل أن يكون لديك ملف go.mod في جذر مشروعك. إذا لم يكن كذلك، يمكنك تهيئة وحدات Go عن طريق تشغيل الأمر التالي:

go mod init <module-name>

استبدل <module-name> باسم مشروعك أو مساره. بمجرد تهيئة وحدات Go في مشروعك، يمكنك إضافة Viper كاعتمادية:

go get github.com/spf13/viper

سيقوم هذا الأمر بجلب حزمة Viper وتسجيل إصدارها في ملف go.mod.

تهيئة Viper في مشروع Go

لبدء استخدام Viper داخل مشروع Go الخاص بك، تحتاج أولاً إلى استيراد الحزمة ومن ثم إنشاء مثيل جديد من Viper أو استخدام سينغلتون محدد مسبقًا. فيما يلي مثال على كيفية القيام بكليهما:

package main

import (
	"fmt"
	"github.com/spf13/viper"
)

func main() {
	// باستخدام سينغلتون Viper، الذي يكون مُكونًا مسبقًا وجاهزًا للاستخدام
	viper.SetDefault("serviceName", "خدمتي الرائعة")

// بديليًا، إنشاء مثيل Viper جديد
	myViper := viper.New()
	myViper.SetDefault("serviceName", "خدمتي الجديدة")

// الوصول إلى قيمة التكوين باستخدام السينغلتون
serviceNm = viper.GetString("serviceName")
	fmt.Println("اسم الخدمة هو:", serviceName)

// الوصول إلى قيمة التكوين باستخدام المثيل الجديد
newServiceName := myViper.GetString("serviceName")
	fmt.Println("اسم الخدمة الجديد هو:", newServiceName)
}

في الكود أعلاه، يُستخدم SetDefault لتحديد قيمة افتراضية لمفتاح التكوين. يستردّ الأسلوب GetString قيمة. عند تشغيل هذا الكود، يُطبع كل من أسماء الخدمة التي قمنا بتكوينها باستخدام كل من سينغلتون المثيل الجديد.

3. قراءة وكتابة ملفات التكوين

العمل مع ملفات التكوين هو ميزة أساسية في Viper. يسمح بتحويل تكوين التطبيق إلى الخارج بحيث يمكن تحديثه دون الحاجة إلى إعادة تجميع الكود. فيما يلي سنستكشف إعداد تنسيقات التكوين المختلفة ونظهر كيفية القراءة منها والكتابة إليها.

إعداد صيغ التكوين (JSON، TOML، YAML، HCL، إلخ)

يدعم Viper العديد من صيغ التكوين مثل JSON و TOML و YAML و HCL، وما إلى ذلك. للبدء، يجب عليك تحديد اسم ونوع ملف التكوين الذي يجب على Viper البحث عنه:

v := viper.New()

v.SetConfigName("app")  // اسم ملف التكوين بدون الامتداد
v.SetConfigType("yaml") // أو "json"، "toml"، "yml"، "hcl"، إلخ.

// مسارات البحث عن ملفات التكوين. يمكنك إضافة مسارات متعددة إذا كان موقع ملف التكوين يتغير.
v.AddConfigPath("$HOME/.appconfig") // موقع تكوين المستخدم UNIX النموذجي
v.AddConfigPath("/etc/appconfig/")  // مسار التكوين على نطاق نظام UNIX
v.AddConfigPath(".")                // المجلد الحالي

قراءة من وكتابة إلى ملفات التكوين

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

if err := v.ReadInConfig(); err != nil {
    if _, ok := err.(viper.ConfigFileNotFoundError); ok {
        // لم يتم العثور على ملف التكوين؛ تجاهل ذلك إذا كان مرغوبًا أو التعامل معه بطريقة أخرى
        log.Printf("لم يتم العثور على ملف تكوين. يتم استخدام القيم الافتراضية، و/أو متغيرات البيئة.")
    } else {
        // تم العثور على ملف التكوين ولكن حدث خطأ آخر
        log.Fatalf("خطأ في قراءة ملف التكوين، %s", err)
    }
}

لكتابة التعديلات إلى ملف التكوين ، أو إنشاء ملف جديد، يقدم Viper عدة طرق. هكذا تكتب التكوين الحالي إلى ملف:

err := v.WriteConfig() // يكتب التكوين الحالي إلى المسار المحدد مسبقًا بواسطة `v.SetConfigName` و `v.AddConfigPath`
if err != nil {
    log.Fatalf("خطأ في كتابة ملف التكوين، %s", err)
}

تأسيس قيم التكوين الافتراضية

تكون القيم الافتراضية كبديل في حالة عدم تعيين مفتاح في ملف التكوين أو بواسطة متغيرات البيئة:

v.SetDefault("ContentDir", "content")
v.SetDefault("LogLevel", "debug")
v.SetDefault("Database.Port", 5432)

// هيكل بيانات أكثر تعقيدًا للقيمة الافتراضية
viper.SetDefault("Taxonomies", map[string]string{
    "tag":       "tags",
    "category":  "categories",
})

4. إدارة متغيرات البيئة والأعلام

لا يقتصر دور Viper فقط على ملفات التكوين - بل يمكنه أيضًا إدارة متغيرات البيئة والأعلام في سطور الأوامر، وهو أمر مفيد بشكل خاص عند التعامل مع إعدادات تخص بيئة معينة.

ربط متغيرات البيئة والأعلام بـ Viper

ربط متغيرات البيئة:

v.AutomaticEnv() // البحث تلقائيًا عن مفاتيح متغيرات البيئة التي تتطابق مع مفاتيح Viper

v.SetEnvPrefix("APP") // بادئة لمتغيرات البيئة لتمييزها عن الأخرى
v.BindEnv("port")     // ربط متغير البيئة PORT (مثلاً، APP_PORT)

// يمكنك أيضًا توصيل متغيرات البيئة بأسماء مختلفة إلى مفاتيح في تطبيقك
v.BindEnv("database_url", "DB_URL") // يخبر Viper باستخدام قيمة متغير البيئة DB_URL لمفتاح التكوين "database_url"

ربط الأعلام باستخدام pflag، وهي حزمة Go لتحليل الأعلام:

var port int

// تعريف علم باستخدام pflag
pflag.IntVarP(&port, "port", "p", 808, "المنفذ للتطبيق")

// ربط العلم بمفتاح Viper
pflag.Parse()
if err := v.BindPFlag("port", pflag.Lookup("port")); err != nil {
    log.Fatalf("خطأ في ربط العلم بالمفتاح، %s", err)
}

التعامل مع تكوينات تخص بيئة

غالبًا ما يحتاج التطبيق إلى العمل بطريقة مختلفة في بيئات مختلفة (تطوير، مسرحية، إنتاج، إلخ). يمكن لـ Viper أن يستهلك التكوين من متغيرات البيئة التي يمكن أن تتجاوز الإعدادات في ملف التكوين، مما يسمح بتكوينات تخص بيئة معينة:

v.SetConfigName("config") // اسم ملف التكوين الافتراضي

// يمكن تجاوز التكوين بواسطة متغيرات البيئة
// ببادئة APP وبقية المفتاح بالحالة الكبيرة
v.SetEnvPrefix("APP")
v.AutomaticEnv()

// في بيئة الإنتاج، قد تستخدم متغير البيئة APP_PORT لتجاوز المنفذ الافتراضي
fmt.Println(v.GetString("port")) // سيكون الإخراج قيمة متغير APP_PORT إذا تم تعيينه، وإلا القيمة من ملف التكوين أو القيمة الافتراضية

تذكر التعامل مع الاختلافات بين البيئات داخل كود التطبيق، إذا لزم الأمر، بناءً على التكوينات التي يتم تحميلها بواسطة Viper.

.5 دعم مخزن المفاتيح/القيم البعيد

فيبر يوفر دعمًا قويًا لإدارة تكوين التطبيق باستخدام مخازن المفاتيح/القيم البعيدة مثل etcd و Consul و Firestore. يتيح ذلك توحيد التكوينات وتحديثها بشكل ديناميكي عبر الأنظمة الموزعة. بالإضافة إلى ذلك، يتيح فيبر التعامل الآمن مع التكوينات الحساسة من خلال التشفير.

دمج فيبر مع مخازن المفاتيح/القيم البعيدة (etcd، Consul، Firestore، إلخ)

للبدء باستخدام فيبر مع مخازن المفاتيح/القيم البعيدة، يجب عليك القيام بأمر import فارغ لحزمة viper/remote في تطبيقك Go:

import _ "github.com/spf13/viper/remote"

دعونا ننظر في مثال يتماشى مع etcd:

import (
    "log"

    "github.com/spf13/viper"
    _ "github.com/spf13/viper/remote"
)

func initRemoteConfig() {
    viper.SetConfigType("json") // تعيين نوع ملف التكوين البعيد
    viper.AddRemoteProvider("etcd", "http://127...1:4001", "/config/myapp.json")
  
    err := viper.ReadRemoteConfig() // محاولة قراءة التكوين البعيد
    if err != nil {
        log.Fatalf("فشل في قراءة التكوين البعيد: %v", err)
    }
  
    log.Println("تم قراءة التكوين البعيد بنجاح")
}

func main() {
    initRemoteConfig()
    // منطق التطبيق الخاص بك هنا
}

في هذا المثال، يقوم فيبر بالاتصال بخادم etcd الذي يعمل على http://127...1:4001 ويقرأ التكوين الموجود على /config/myapp.json. عند العمل مع مخازن أخرى مثل Consul، قم بتعويض "etcd" بـ "consul" وضبط المعلمات الخاصة بمزوّدي الخدمة وفقًا لذلك.

إدارة التكوينات المُشفّرة

التكوينات الحساسة، مثل مفاتيح API أو بيانات اعتماد قاعدة البيانات، يجب ألا تُخزن على شكل نص عادي. يُسمح لـ فيبر بتخزين تكوينات مُشفّرة في مخزن مفاتيح/قيم وفك تشفيرها في تطبيقك.

للاستفادة من هذه الميزة، تأكد من أن الإعدادات المُشفّرة مُخزنة في مخزن مفاتيح/قيم. ثم استفد من AddSecureRemoteProvider في فيبر. فيما يلي مثال عن استخدام هذا مع etcd:

import (
    "log"

    "github.com/spf13/viper"
    _ "github.com/spf13/viper/remote"
)

func initSecureRemoteConfig() {
    const secretKeyring = "/path/to/secret/keyring.gpg" // مسار ملف حلقة مفاتيحك
  
    viper.SetConfigType("json")
    viper.AddSecureRemoteProvider("etcd", "http://127...1:4001", "/config/myapp.json", secretKeyring)
  
    err := viper.ReadRemoteConfig()
    if err != nil {
        log.Fatalf("تعذّر قراءة التكوين البعيد: %v", err)
    }

    log.Println("تم قراءة وفك تشفير التكوين البعيد بنجاح")
}

func main() {
    initSecureRemoteConfig()
    // منطق التطبيق الخاص بك هنا
}

في المثال أعلاه، تم استخدام AddSecureRemoteProvider محددًا مسار حلقة GPG التي تحتوي على المفاتيح الضرورية للفك تشفير.

6. مراقبة ومعالجة تغييرات التكوين

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

رصد تغييرات التكوين وإعادة قراءة التكوينات

يستخدم فيبر حزمة fsnotify لمراقبة تغييرات ملف التكوين الخاص بك. يمكنك إعداد مراقب لتنشيط الأحداث كلما حدث تغيير في ملف التكوين:

import (
    "log"

    "github.com/fsnotify/fsnotify"
    "github.com/spf13/viper"
)

func watchConfig() {
    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        log.Printf("تغيير في ملف التكوين: %s", e.Name)
        // هنا يمكنك قراءة التكوين المحدث إذا لزم الأمر
        // قم بإجراء أي عمل مثل إعادة تهيئة الخدمات أو تحديث المتغيرات
    })
}

func main() {
    viper.SetConfigName("myapp")
    viper.AddConfigPath(".")
    err := viper.ReadInConfig()
    if err != nil {
        log.Fatalf("خطأ في قراءة ملف التكوين، %s", err)
    }

    watchConfig()
    // منطق التطبيق الخاص بك هنا
}

الحوافز لتحديث التكوينات في تطبيق قيد التشغيل

في تطبيق قيد التشغيل، قد ترغب في تحديث التكوينات استجابة لمختلف الحوافز، مثل إشارة، وظيفة مُعتمدة على الوقت، أو طلب واجهة برمجة التطبيقات (API). يمكنك تنظيم تطبيقك لتحديث حالته الداخلية بناءً على إعادة قراءة التكوينات الخاصة بـ Viper:

import (
    "os"
    "os/signal"
    "syscall"
    "time"
    "log"

    "github.com/spf13/viper"
)

func setupSignalHandler() {
    signalChannel := make(chan os.Signal, 1)
    signal.Notify(signalChannel, syscall.SIGHUP) // الاستماع لإشارة SIGHUP

    go func() {
        for {
            sig := <-signalChannel
            if sig == syscall.SIGHUP {
                log.Println("تلقي إشارة SIGHUP. إعادة تحميل التكوين...")
                err := viper.ReadInConfig() // إعادة قراءة التكوين
                if err != nil {
                    log.Printf("خطأ في إعادة قراءة التكوين: %s", err)
                } else {
                    log.Println("تمت إعادة تحميل التكوين بنجاح.")
                    // إعادة تكوين تطبيقك بناءً على التكوين الجديد هنا
                }
            }
        }
    }()
}

func main() {
    viper.SetConfigName("myapp")
    viper.AddConfigPath(".")
    err := viper.ReadInConfig()
    if err != nil {
        log.Fatalf("خطأ في قراءة ملف التكوين، %s", err)
    }

    setupSignalHandler()
    for {
        // منطق التطبيق الرئيسي
        time.Sleep(10 * time.Second) // محاكاة بعض العمل
    }
}

في هذا المثال، نقوم بإعداد معالج للاستماع إلى إشارة SIGHUP. عند تلقي الإشارة، يقوم Viper بإعادة قراءة ملف التكوين ويجب على التطبيق بعد ذلك تحديث التكوينات أو الحالة حسب الحاجة.

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