1. مقدمه‌ای در مورد Viper

golang viper

درک نیاز به یک راه‌حل پیکربندی در برنامه‌های Go

برای ساخت نرم‌افزار قابل اعتماد و قابل نگهداری، توسعه‌دهندگان نیاز دارند تا پیکربندی را از منطق برنامه جدا کنند. این کار به شما اجازه می‌دهد تا رفتار برنامه خود را بدون تغییر در کد پایه تنظیم کنید. یک راه‌حل پیکربندی این جداسازی را با تسهیل بیرون‌ریزی داده‌های پیکربندی ممکن می‌سازد.

برنامه‌های Go می‌توانند از چنین سیستمی بهره‌مند شوند، به خصوص زمانی که پیچیدگی آن‌ها افزایش یافته و با محیط‌های مختلف اجرا مواجه می‌شوند، مثل توسعه، استیجینگ و تولید. هر یک از این محیط‌ها ممکن است تنظیمات مختلفی برای اتصال به پایگاه داده، کلیدهای API، شماره‌های درگاه و غیره نیاز داشته باشند. تعیین این مقادیر به صورت ثابت ممکن است مشکل‌ساز و خطاپذیر باشد، زیرا منجر به چندین مسیر کد برای نگهداری تنظیمات مختلف می‌شود و ریسک فاش شدن داده‌های حساس را افزایش می‌دهد.

یک راه‌حل پیکربندی مانند Viper این افزونه‌ها را با فراهم کردن یک رویکرد یکپارچه که نیازهای مختلف پیکربندی را پشتیبانی می‌کند، به حل می‌رساند.

بررسی Viper و نقش آن در مدیریت پیکربندی‌ها

Viper یک کتابخانه جامع پیکربندی برای برنامه‌های Go است که هدف آن تبدیل شدن به راه‌حلی اصلی برای همه نیازهای پیکربندی است. این کتابخانه با شیوه‌های تعیین شده در روش Twelve-Factor App هماهنگی دارد که تشویق به ذخیره پیکربندی در محیط برای دست‌رسی در بین محیط‌های اجرا را ترویج می‌کند.

Viper نقش اساسی در مدیریت پیکربندی‌ها با این ویژگی‌ها دارد:

  • خواندن و unmarshaling فایل‌های پیکربندی در فرمت‌های مختلف مانند JSON، TOML، YAML، HCL و غیره.
  • نقض مقادیر پیکربندی با متغیرهای محیطی، بنابراین حکم اصلی پیکربندی بیرونی را رعایت می‌کند.
  • بایند و خواندن از پرچم‌های خط فرمان برای امکان تنظیم دینامیک گزینه‌های پیکربندی در زمان اجرا.
  • امکان تعیین پیش‌فرض‌ها در برنامه برای گزینه‌های پیکربندی که به‌طور خارجی ارائه نشده‌اند.
  • نظارت بر تغییرات در فایل‌های پیکربندی و بازخوانی زنده، ارائه انعطاف‌پذیری و کاهش زمان‌های اوقات انتظار برای تغییرات پیکربندی.

2. نصب و راه‌اندازی

نصب Viper با استفاده از ماژول‌های Go

برای اضافه کردن 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", "My Awesome Service")

	// یا به‌طور جایگزین، ایجاد یک نمونه جدید از Viper
	myViper := viper.New()
	myViper.SetDefault("serviceName", "My New Service")

	// دسترسی به یک مقدار پیکربندی با استفاده از نمونه یکتای
	serviceName := viper.GetString("serviceName")
	fmt.Println("Service Name is:", serviceName)

	// دسترسی به یک مقدار پیکربندی با استفاده از نمونه جدید
	newServiceName := myViper.GetString("serviceName")
	fmt.Println("New Service Name is:", 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. پشتیبانی از ذخیره‌سازی از راه دور کلید/مقدار

Viper پشتیبانی قوی از مدیریت پیکربندی برنامه با استفاده از ذخیره‌سازهای از راه دور مانند etcd، Consul یا Firestore را فراهم می‌کند. این امکان را فراهم می‌کند که پیکربندی‌ها متمرکز و به صورت پویا در سیستم‌های توزیع‌شده بروزرسانی شوند. علاوه بر این، Viper امکان رسانه‌ای امن از پیکربندی‌های حساس را از طریق رمزنگاری فراهم می‌کند.

ادغام Viper با ذخیره‌سازهای از راه دور کلید/مقدار (etcd، Consul، Firestore و غیره)

برای شروع استفاده از Viper با ذخیره‌سازهای از راه دور کلید/مقدار، شما نیاز دارید تا وارد کردن خالی بسته‌ای از پکیج 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()
    // منطق برنامه شما در این قسمت
}

در این مثال، Viper به یک سرور etcd که در http://127...1:4001 اجرا می‌شود متصل می‌شود و پیکربندی موجود در /config/myapp.json را می‌خواند. هنگام کار با ذخیره‌سازهای دیگر مانند Consul، "etcd" را با "consul" جایگزین کرده و پارامتر‌های مشخص‌شده توسط ارائه‌دهندگان را به‌طور مطابق تنظیم نمایید.

مدیریت پیکربندی‌های رمزنگاری شده

پیکربندی‌های حساس مانند کلیدهای API یا اعتبارات پایگاه داده نباید به صورت متن ساده ذخیره شوند. Viper امکان ذخیره پیکربندی‌های رمزنگاری شده در ذخیره‌ساز کلید/مقدار و رمزگشایی آنها در برنامه را فراهم می‌کند.

برای استفاده از این ویژگی، اطمینان حاصل کنید که تنظیمات رمزنگاری شده در ذخیره‌ساز کلید/مقدار شما ذخیره شده است. سپس از AddSecureRemoteProvider Viper بهره ببرید. در ادامه مثال استفاده از این ویژگی با 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. نظارت و برخورد با تغییرات پیکربندی

یکی از ویژگی‌های قدرتمند Viper، توانایی نظارت و پاسخ به تغییرات پیکربندی به صورت زمان واقعی بدون راه‌اندازی مجدد برنامه است.

نظارت بر تغییرات پیکربندی و مجدد خواندن پیکربندی‌ها

Viper برای نظارت بر تغییرات در فایل پیکربندی‌تان از پکیج 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 پیکربندی فایل را دوباره بارگذاری می‌کند و سپس برنامه باید پیکربندی‌ها یا وضعیت خود را به‌روزکند.

همواره به یاد داشته باشید که این پیکربندی‌ها را تست کنید تا بتوانید اطمینان حاصل کنید که برنامه‌ی شما بتواند به‌روزرسانی‌های پویا را به خوبی اداره کند.