Read configuration files using Go

Keywords: Go github git

brief introduction

In the last practice, a basic restful api server was launched.

There were many hard-coded attributes in the code at that time, so this time we would try to read them from the configuration file.

Read the configuration using viper

Use here viper Read the configuration, first install it.

go get -u github.com/spf13/viper

Create a config directory, then add the config.go file, define a structure Config inside, and use Name to save the configuration path.

type Config struct {
    Name string
}

Then it defines two methods, one reading configuration and the other observing configuration changes.

// Read configuration
func (c *Config) InitConfig() error {
    if c.Name != "" {
        viper.SetConfigFile(c.Name)
    } else {
        viper.AddConfigPath("conf")
        viper.SetConfigName("config")
    }
    viper.SetConfigType("yaml")

    // Total read from environment variables
    viper.AutomaticEnv()
    viper.SetEnvPrefix("web")
    viper.SetEnvKeyReplacer(strings.NewReplacer("_", "."))

    return viper.ReadInConfig()
}

// Monitoring configuration changes
func (c *Config) WatchConfig(change chan int) {
    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        log.Printf("Configuration has been changed: %s", e.Name)
        change <- 1
    })
}

There are many ways to read the configuration. The first one is that Config.Name is not defined.
If c.Name is an empty string, the configuration file will be found from the default path.

Another way is to specify the path of the configuration file directly, which is to use the configuration file directly.

In addition, it activates reading configuration parameters from environment variables, and pays attention to setting prefixes for all environment variables.
The prefix is automatically converted to uppercase _format.

In addition, for multi-level configuration parameters, the in the environment variable is automatically converted to..

For example, the prefix currently set is web. Define an environment variable named WEB_LOG_PATH.
It automatically converts to log.path, and you can use viper.GetString("log.path")
Or the corresponding value of this environmental variable.

Creating command-line tools using Cobra

After using viper to read the configuration, CLI tools are necessary for more flexible use.
In order to specify parameters and so on at runtime.

Cobra is a library for creating modern CLI interfaces that provide capabilities similar to git and go tools.

Cobra The author is the author who created viper.
So these libraries are all named after viper, viper is viper, corba is cobra.

corba is good at aggregating multiple commands. It follows the concepts of commands, parameters and logos.

The model that follows this idea is APPNAME VERB NOUN --ADJECTIVE or APPNAME COMMAND ARG --FLAG.

For our web project, we only have to start this operation at present, so let's create an initiative first.

Create a cmd directory and create a file named root.go.

var rootCmd = &cobra.Command{
    Use:   "server",
    Short: "server is a simple restful api server",
    Long: `server is a simple restful api server
    use help get more ifo`,
    Run: func(cmd *cobra.Command, args []string) {
        runServer()
    },
}

Mainly use & cobra. Command to define a command.

The parameter Use s inside defines the name of the command, Short and Long are short and long descriptions, respectively.
Run defines the actual code to run.

After defining the main command, you may need to add some operations, which are defined in the init() function.
It also runs cobra.OnInitialize, which is run at the execution stage of each command.

// Initialize, set flag, etc.
func init() {
    cobra.OnInitialize(initConfig)
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default: ./conf/config.yaml)")
}

// Initialization configuration
func initConfig() {
    c := config.Config{
        Name: cfgFile,
    }

    if err := c.InitConfig(); err != nil {
        panic(err)
    }
    log.Printf("Load Configuration Successful")
    c.WatchConfig(configChange)
}

Here I set up a flag named config, which is the path corresponding to the configuration file.

Finally, you need to define a function to wrap the execution of the main command:

// Wrapped rootCmd.Execute()
func Execute() {
    if err := rootCmd.Execute(); err != nil {
        log.Println(err)
        os.Exit(1)
    }
}

In this way, the main file main.go is very simple, because we have already done the main execution.
Encapsulated as runServer(), and defined under the main command.

func main() {
    cmd.Execute()
}

Thermal overload

A function is defined to observe viper configuration changes, noting that it has a channel parameter.
I use channels as messaging mechanisms.

// Monitoring configuration changes
func (c *Config) WatchConfig(change chan int) {
    viper.WatchConfig()
    viper.OnConfigChange(func(e fsnotify.Event) {
        log.Printf("Configuration has been changed: %s", e.Name)
        change <- 1
    })
}

When the configuration file is changed, it actually passes a file called fsnotify.Event itself.
But instead of studying it carefully, I used channels to deliver messages.

// Define the execution of the rootCmd command
func runServer() {
    // Setting up Operation Mode
    gin.SetMode(viper.GetString("runmode"))

    // Initialize empty servers
    app := gin.New()
    // Save Middleware
    middlewares := []gin.HandlerFunc{}

    // Route
    router.Load(
        app,
        middlewares...,
    )

    go func() {
        if err := check.PingServer(); err != nil {
            log.Fatal("The server did not respond", err)
        }
        log.Printf("Server starts normally")
    }()

    // Server Yuxing's address and port
    addr := viper.GetString("addr")
    log.Printf("Start the server at http address: %s", addr)

    srv := &http.Server{
        Addr:    addr,
        Handler: app,
    }
    // Startup service
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("listen: %s\n", err)
        }
    }()

    // Wait for configuration changes, then restart
    <-configChange
    if err := srv.Shutdown(context.Background()); err != nil {
        log.Fatal("Server Shutdown:", err)
    }
    runServer()
}

The preceding are regular start-ups, including using a goroutine to check the health of the start-up.
Start the server with another goroutine.

Notice that in the last few lines, we are waiting for the channel notification that the configuration file has changed, and then we start to shut down the server.
Finally, run the startup function again.

Note: There may be a bug where OnConfigChange triggers twice after modifying the configuration file.
For the time being, there is no good solution. Or consider the one mentioned in github issues.
Current limiting mode.

summary

This process mainly studies how to read configuration files, and also uses command line related libraries.
It's easy to expand more commands later.

The current part of the code

As version 0.2.0

Posted by Sealr0x on Thu, 12 Sep 2019 04:06:25 -0700