Viper is a complete configuration solution for Go applications. It is designed to work within an application, and can handle all types of configuration needs and formats. Also, viper has many features, such as support for different configuration formats (JSON, TOML, YAML, etc.), live watching and re-reading of config files, reading from environment variables, etc.
Here will demostract how to use viper with local config, management with struct, and envrionment variables.
Here is the local yaml config file for the application, the config are mysql database connection parameters:
config/config.yml
mysql:
host: 127.0.0.1
port: 3306
username: default
password: deafult
database: database_name
Reading Local Configuration Files
First, we use InitConfig() to set the initial values for Viper, specifying the location of the local config we want to read:
utils/config.go
package utils
import (
viper "github.com/spf13/viper"
)
func InitConfig() error {
//set config file as default
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("config/")
...
Next, we can use viper.GetString to directly specify the nested structure key to get the corresponding value:
utils/database.go
package utils
import (
"fmt"
"sync"
viper "github.com/spf13/viper"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
var o sync.Once
var db *gorm.DB
func InitDB() (*gorm.DB, error) {
host := viper.GetString("mysql.localhost")
port := viper.GetString("mysql.port")
username := viper.GetString("mysql.username")
password := viper.GetString("mysql.password")
database := viper.GetString("mysql.database")
var err error
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", username, password, host, port, database)
...
}
main.go
package main
"go_api_example/utils"
)
func init() {
err := utils.InitConfig()
if err != nil {
panic("init config error:" + err.Error())
}
}
func main() {
Db, err := utils.InitDB()
...
}
Managing Through Structures
Originally, we read directly from the local file. To make the subsequent architectural design more flexible, we adjusted to read our local value through the InfoDbHost struct. The original function was adjusted to the InfoDbHost Method function. We use viper.GetStringMap to get the MySQL values and convert them into a map structure. Then, we use mapstructure.Decode from mapstructure to convert the map into a struct structure. This allows us to get the values using the struct method.
package utils
import (
"fmt"
"sync"
"github.com/go-redis/redis"
"github.com/mitchellh/mapstructure"
viper "github.com/spf13/viper"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type InfoDbHost struct {
Host string
Port any
Username string
Password string
Database string
}
func (infoDb InfoDbHost) InitDB() (*gorm.DB, error) {
var err error
o.Do(func() {
err = mapstructure.Decode(viper.GetStringMap("mysql"), &infoDb)
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", infoDb.Username, infoDb.Password, infoDb.Host, infoDb.Port, infoDb.Database)
fmt.Println("Init DB once")
...
}
...
In the original place where the InitDB() function was directly used, it has been adjusted to specify var InfoDb utils.InfoDb and call the InfoDb.InitDB() method:
main.go
package main
"go_api_example/utils"
)
func init() {
err := utils.InitConfig()
if err != nil {
panic("init config error:" + err.Error())
}
}
var InfoDb utils.InfoDb
func main() {
Db, err := InfoDb.InitDB()
...
}
Using Local as Default, Automatically Apply Environment Variables
In microservice design, environment variables are often used as default values to make CI/CD design more flexible. There are many packages in Go for setting and managing environment variables, but here we’ll only introduce Viper.
In this example, you need to first set up some environment variables. You can use export or Go’s os package to do this, for example:
// Set environment variables
os.Setenv("MYSQL_USERNAME", "root")
os.Setenv("MYSQL_PASSWORD", "root")
os.Setenv("MYSQL_DATABASE", "database")
os.Setenv("MYSQL_PORT", "3306")
os.Setenv("MYSQL_HOST", "localhost")
First, we define a Config structure that includes Mysql. This will serve as our management of environment variables. We use viper.AutomaticEnv() to allow Viper to automatically read environment variables. To align our environment variables with the nested structure, we use _ as a separator in environment variable names. We then use viper.SetEnvKeyReplacer(strings.NewReplacer(".", “_")) to replace the separators (Viper automatically reads all environment variables and the keys will be automatically uppercased). Finally, viper.ReadInConfig() allows environment variables that match the naming format to replace the local config value.
For example: MYSQL_USERNAME will replace mysql.username.
utils/config.go
package utils
import (
"fmt"
"os"
"strings"
viper "github.com/spf13/viper"
)
type Mysql struct {
Host string
Port string
Username string
Password string
Database string
}
t
type Config struct {
Mysql Mysql
}
func InitConfig() error {
//set config file as default
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("config/")
// viper auto read all env variables, the key will auto uppercase
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
err := viper.ReadInConfig()
if err != nil {
panic(fmt.Errorf("fatal error config file: %w", err))
}
return err
}
Here, we adjust the InfoDb struct to use the Mysql struct, which makes it easier to access and use the values in Viper.
The viper.Unmarshal function in Go’s Viper library is used to unmarshal the configuration data into a struct. This means it takes the configuration data that Viper has loaded and decodes it into a struct
In this example, Viper.Unmarshal decodes the configuration data into the Config struct. The Port and Host fields in the struct map to the port and host keys in the configuration data.
utils/database.go
package utils
var DB *gorm.DB
var o sync.Once
var db *gorm.DB
type InfoDb struct {
Mysql Mysql
}
func (infoDb InfoDb) InitDB() (*gorm.DB, error) {
var err error
o.Do(func() {
if err := viper.Unmarshal(&infoDb); err != nil {
panic(fmt.Errorf("unable to decode into struct, %v", err))
}
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", infoDb.Mysql.Username, infoDb.Mysql.Password, infoDb.Mysql.Host, infoDb.Mysql.Port, infoDb.Mysql.Database)
...
})
return db, err
}
Treatment the environment variable with prefix
Sometimes we need to add a different prefix to environment variables for local development or multiple tenant usage.
For example, with a prefix MYP
for all variables like:
// Set environment variables
os.Setenv("MYSQL_USERNAME", "username")
os.Setenv("MYSQL_PASSWORD", "password")
os.Setenv("MYSQL_DATABASE", "database")
os.Setenv("MYSQL_PORT", "3306")
os.Setenv("MYSQL_HOST", "localhost")
Here we can use the viper SetEnvPrefix
to specify our environment variable prefix:
package utils
import (
viper "github.com/spf13/viper"
)
func InitConfig() error {
//set config file as default
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("config/")
// viper auto read all env variables, the key will auto uppercase
viper.AutomaticEnv()
//Set prefix of env variables
viper.SetEnvPrefix("MYP")
//Replace the environment variables _ to .
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
err := viper.ReadInConfig()
if err != nil {
panic(fmt.Errorf("fatal error config file: %w", err))
}
return err
Conculsion
In conclusion, here we provides a comprehensive guide on how to use the Viper library in Go for configuration management. It covers how to read local configuration files, manage configurations through structures, and use local configurations as defaults while automatically applying environment variables.
By following this guide, you can effectively manage configurations in their Go applications, making their code more maintainable and their applications more flexible and robust.