🐍
Viper

Viper logo
Viper, 12-Factor uygulamaları da dahil olmak üzere Go uygulamaları için eksiksiz bir yapılandırma çözümüdür. Bir uygulama içinde çalışmak üzere tasarlanmıştır. Her türlü yapılandırma gereksinimini ve biçimini karşılayabilir.
Desteklediği yöntemler şunlardır:
- varsayılanları ayarlama,
- JSON, TOML, YAML, HCL, envfile ve Java properties config dosyalarından okuma,
- yapılandırma dosyalarını canlı izleme ve yeniden okuma (opsiyonel),
- ortam değişkenlerinden okuma,
- uzak yapılandırma sistemlerinden (etcd ya da Consul) okuma ve canlı izleme,
- komut-satırı flag'lerinden okuma,
- buffer'dan okuma,
- açık değerler atama.
Viper, tüm uygulama yapılandırma ihtiyaçlarınız için bir kayıt defteri olarak düşünülebilir.
Bu kısımda,
json
tipinde bir yapılandırma dosyası üzerinden, yapılandırmamızı nasıl yönetebileceğimizi (okuma, yazma...)
göreceğiz.
Öncelikle projemizi oluşturalım.
go mod init <proje>
daha sonra ihtiyacımız olduğu için
main.go
ve config.json
isimli dosyalarımızı oluşturalım. Sonuç olarak projemizin içeriği aşağıdaki gibi olacak.$ tree .
.
├── config.json
├── go.mod
├── go.sum
└── main.go
go get github.com/spf13/viper
config.json
dosyamızda aşağıdaki gibi bir yapı oluşturalım.config.json
{
"host": "127.0.0.1",
"port": "80"
}
Artık
config.json
dosyamızı host
ve port
değerlerini okuyup yazmak için kullanabiliriz.main.go
dosyamızın içeriği de aşağıdaki gibi olsun.main.go
package main
import (
"github.com/spf13/viper"
"log"
)
func main() {
vp := viper.New() // yeni bir viper örneği oluşturalım
vp.AddConfigPath(".") // config dosyamızın bulunduğu dizin
vp.SetConfigName("config") // config dosyamızın ismi
vp.SetConfigType("json") // config dosyamızın uzantısı
if err := vp.ReadInConfig(); err != nil {
log.Fatalf("yapılandırma dosyası okunurken hata oluştu: %v", err)
}
}
Yukarıdaki yöntem ile oluşturduğumuz yapılandırma dosyasının içeriğini artık yönetebiliriz.
Hadi nasıl okuyacağımıza bakalım.
vp.Get("host") // 127.0.0.1
Get()
fonksiyonu ile belirli bir anahtarın değerini okuyabiliriz. Burada dikkat edilmesi gereken nokta Get()
fonksiyonu bize any
tipinde bir değer döndürür. Döndürülen bu değeri asıl tipine çevirmeden kullanamayacağımız durumlar olabilir. Bu durumlarda type assertion yaparak değeri çıkartabiliriz ;fakat daha kolay bir yöntem olarak GetString()
fonksiyonunu kullanabilirizvp.GetString("host") // 127.0.0.1
Tabi ki bu yöntem sadece
string
tipi için geçerli değil, hepsine tek tek değinmek uzun süreceği için direkt olarak dönüştürebileceğimiz tiplere kısa değinelim.bool
string, []string, map[string]interface{}, map[string]string{}, map[string][]string{}
int, int32, int64, []int
uint, uint16, uint32, uint64
float64
time.Time, time.Duration
Yukarıda verilen tipler için ilgili fonksiyonu
Get<tip-ismi>()
yazarak bulabilirsiniz. Bu şekilde kullanamadığınız tipleri de type assertion yaparak dönüştürebilirsiniz.
Şimdi de yapılandırma dosyamızdaki bir anahtara nasıl atama yapacağımıza bakalım.
vp.Set("host", "example.com") // atama işlemi
// değişiklikleri yapılandırma dosyasına yazdıralım
if err := vp.WriteConfig(); err != nil {
log.Fatalf("yapılandırma dosyası yazılırken hata oluştu: %v", err)
}
Yazdırma işleminde sonra
config.json
dosyamız aşağıdaki gibi değişecektir.config.json
{
"host": "example.com",
"port": "80"
}
Atama işlemini eğer var olmayan bir anahtar ile yapsaydık, yeni bir alan oluştururduk. Örnek:
vp.Set("user", "kaan")
Yapılandırma dosyamızdaki değişikliğe bakalım.
config.json
{
"host": "example.com",
"port": "80",
"user": "kaan"
}
Anahtar değerinin boş olarak atanması veya silinmesi gibi işlemler uygulamanın kararsız çalışmasına neden olabileceği için bu tür fonksiyonların kullanımını engellemişler. Zaten anahtarın silinmesi için bir fonksiyon bulunmamaktadır. Bu yüzden viper paketinin kullanımı sadece
Set
ve Get
işlemlerini destekler.WriteConfig()
fonksiyonu dosyanın yazılmasında karşılaşılan hataları döndürür.Yapılandırma dosyasının uygulamamız tarafından oluşturulmasını istediğimiz durumlar olabilir. Bu durumlarda, yukarıdaki örneklerimizde gördüğümüzün aksine yapılandırma dosyamızı okumadan önce oluşturmamız gerekir.
main.go
package main
import (
"github.com/spf13/viper"
"log"
)
func main() {
vp := viper.New() // yeni bir viper örneği oluşturalım
vp.AddConfigPath(".") // config dosyamızın bulunduğu dizin
vp.SetConfigName("config") // config dosyamızın ismi
vp.SetConfigType("json") // config dosyamızın uzantısı
vp.Set("host", "example.com")
vp.Set("port", "80")
// değişiklikleri oluşturmak istediğimiz yapılandırma dosyasına yazdıralım
if err := vp.WriteConfig(); err != nil {
log.Fatalf("yapılandırma dosyası yazılırken hata oluştu: %v", err)
}
}
WriteConfig()
fonksiyonunda dikkat edilmesi gereken ayrıntı, eğer oluşturulan yapılandırma dosyası hali hazırda bulunuyorsa bu dosyanın üzerine yazar, fakat üzerine yazma işleminde belirtilmemiş olan anahtarlar eski yapılandırma dosyasındaki hali ile gelir. Yani yapılandırma dosyası tamamen sıfırdan oluşturulmaz.Eğer yapılandırma dosyamızın oluşturulduktan sonra bir daha değiştirilmemesini ve sadece hali hazırda bulunmadığı durumlarda oluşturulmasını istersek
SafeWriteConfig()
fonksiyonunu kullanabiliriz. Böylelikle yapılandırma dosyamız zaten oluşturulmuşsa bize hata döndürecektir.Örnek olarak:
main.go
package main
import (
"github.com/spf13/viper"
"log"
)
func main() {
vp := viper.New() // yeni bir viper örneği oluşturalım
vp.AddConfigPath(".") // config dosyamızın bulunduğu dizin
vp.SetConfigName("config") // config dosyamızın ismi
vp.SetConfigType("json") // config dosyamızın uzantısı
vp.Set("host", "example.com")
vp.Set("port", "80")
// değişiklikleri oluşturmak istediğimiz yapılandırma dosyasına yazdıralım
if err := vp.SafeWriteConfig(); err != nil {
log.Fatalf("yapılandırma dosyası yazılırken hata oluştu: %v", err)
}
}
config.json
dosyamız bulunmuyorsa, atanılan değerler ile birlikte yeni bir config.json
dosyası oluşturulacaktır. Eğer config.json
dosyamız bulunuyorsa, yukarıdaki işlemler yapıldığında SafeWriteConfig()
fonksiyonu aşağıdaki gibi bir hata döndürecektir.$ go run .
2022/10/06 17:50:56 yapılandırma dosyası yazılırken hata oluştu: Config File "/Volumes/SAMSUNG/main/Dev/go/viper-example/config.json" Already Exists
Eğer yapılandırma dosyamız iç içe bir yapıdan oluşuyorsa, okuma-yazma işlemlerinde alt-alana ulaşmak için
.
(nokta) kullanabiliriz.Örnek config.json dosyamız;
config.json
{
"user": {
"full_name": "Kaan Kuscu",
"age": 25
}
}
Okuma işlemi için,
vp.Get("user.full_name") // Kaan Kuscu
Yazma işlemi de aynı şekilde,
vp.Set("user.full_name") // Kaan Kuscu
Uygulamayı çalıştırdığımız sistemimizdeki ortam değişkenlerine erişmek istediğimizde, başvurabileceğimiz iki farklı yöntem var.
Bu yöntemlerden ilki kullanmak istediğimiz ortam değişkenini viper'a bildirmek. Örneğin
TEST
isminde bir ortam değişkenimiz olsun ve değeri de abc
olsun. TEST
'in değerini okuyabilmek için aşağıdaki yöntemleri kullanabiliriz.if err := vp.BindEnv("TEST"); err != nil {
log.Fatalln(err)
}
fmt.Println(vp.Get("TEST"))
Sisteminizde
TEST
isimli bir ortam değişkeni bulunmuyorsa, aşağıdaki gibi deneyebilirsiniz.$ TEST="abc" go run .
abc
BindEnv()
fonksiyonunun hata döndürmeme şartı sadece en az bir parametre girmemizdir.Yapılandırma dosyaların ve ortam değişkenlerinden okuma yaptığımızda, bazı anahtarlar tanımlı olmadığı için, sıfır-değerli halde gelebilir. Eğer bu istemediğimiz bir durumsa bu anahtarların varsayılan değerlerini ayarlayabiliriz. Örnek;
config.json
{
"host": "localhost",
"port": "80",
"user": "root"
}
Gelelim kullanımına;
if err := vp.ReadInConfig(); err != nil {
log.Fatalln(err)
}
vp.SetDefault("password", "1234") // anahtar, değer
fmt.Println(vp.GetString("password")) // 1234
config.json
dosyamızda password
isimli bir alan olmadığı için varsayılan olarak ayarladığımız 1234
değeri yazdırılacaktır.Eğer varsayılan değerler çok sayıda olacaksa, şöyle bir yöntem kullanabiliriz.
defaults := map[string]any{
"host": "localhost",
"user": "root",
"password": "1234",
}
for k, v := range defaults {
vp.Set(k, v)
}
Buraya kadar olan bölümlerde Viper'ı basitçe nasıl kullanacağımızı gördük. Ölçeklenebilir bir projede sadece bu yöntemleri kullarak ilerlememiz zor olduğu için işimizi kolaylaştıracak başka çözümler gerekiyor. Proje yapımızı düzenli tutmak için, bu yöntemlerimizi daha pratik hale getirmemiz gerekiyor.
Bunun için gruplandırma yaparak, yapılandırma ayarlarımızı
struct
halinde kullanabiliriz.Örneğin aşağıdaki gibi bir yapılandırma dosyamız olsun.
config.json
{
"api": {
"host": "example.com",
"port": "80",
"ssl": true
},
"database": {
"host": "localhost",
"user": "root",
"password": "1234",
"name": "project"
}
}
Yukarıdaki
json
yapısı aktarabileceğimiz struct
'ları belirleyelim.type Config struct {
API APIConfig `mapstructure:"api"`
Database DataBaseConfig `mapstructure:"database"`
}
type APIConfig struct {
Host string `mapstructure:"host"`
Port string `mapstructure:"port"`
SSL bool `mapstructure:"ssl"`
}
type DataBaseConfig struct {
Host string `mapstructure:"host"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Name string `mapstructure:"name"`
}
Yukarıdaki
struct
'larda dikkat edilmesi gereken ayrıntı, struct tag
'lerdeki mapstructure
anahtarıdır.Yapılandırma dosyamızın tipi
json
olduğu için mapstructure
yerine json
'da kullanabilirdik. Fakat yapılandırma dosyamızın tipi, örnek olarak json
'dan yaml
'a geçseydi, struct tag
'lerde yaml
olarak düzenleme yapmamız gerekirdi. Bu yüzden tüm desteklenen yapılandırma dosyası tiplerinde unmarshal yapabilmesi için mapstructure
olarak uyguladık.Tabi ki bu yöntem her durumda işimizi görmeyebilir. Bazı durumlarda farklı dosya tiplerinde farklı anahtar isimlendirmeleri olabilir. Bu gibi durumlarda da dosya tipine göre
struct tag
'lerini düzenlemekte fayda vardır.Unmarshal
etmek için aşağıdaki yöntemi izleyebiliriz.main.go
func main() {
vp := viper.New()
vp.AddConfigPath(".")
vp.SetConfigName("config")
vp.SetConfigType("json")
// öncelikle yapılandırma dosyasını okumamız gerekiyor
if err := vp.ReadInConfig(); err != nil {
log.Fatalln(err)
}
var configs Config
if err := vp.Unmarshal(&configs); err != nil {
log.Fatalln(err)
}
fmt.Printf("%+v\n", configs)
}
Çıktımız aşağıdaki gibi olacaktır.
$ go run .
{API:{Host:example.com Port:80 SSL:true} Database:{Host:localhost User:root Password:1234 Name:project}}
Last modified 7mo ago