Reading Configuration Files and Environment Variables in GO — GoLang
One of the best practices in the software industry when it comes to development is to maintain few variables (sensitive data, and also some static data) in configuration files and/or as environment variables. And hence, Configuration Files and Environment Variables are a norm in the present development world, especially to secure credentials. And it is very important to maintain these variables separate from the code base.
In this article, we will look at (why) the theory behind using variables in configuration files and/or as environment variables and (how) the implementation of reading them in GO.
Why do we need them??
The main purpose of maintaining variables (Sensitive data) in configuration files and/or environment variables is,
- To enhance security and not let anyone identify the values stored in those variables. Since database credentials, credentials to access 3rd party applications needed, secret keys and many more sensitive information are maintained there.
- Why maintain static data in configuration files and/or environment variables??
→ As an example consider, a database connection string, it may be changed if the environment is changed, and having it directly in the code may be risky as well, it will lead to rebuild the code. So to avoid that we maintain them separately in configuration files and/or environment variables.
→ Also consider static data like, static messages, colors codes, directory paths, string formats/constants, date formats, etc. They are maintained in configuration files, but not be maintained as environment variables since they do not need to be secure. These data are maintained in a centralized location, in a configuration file because, if any changes are needed in them, the changes can be done without rebuilding the code and also to avoid mistakes that may arise when modifying them, if they are not maintained in a central location, but all over in the code.
Why are they maintained separately?? (General and preferred way)
It is generally advised to maintain them separate from the code base because, if we have it within the code base, we may not be able to change values after running the build process, whenever we change the values in configuration files, we would need to rebuild the code. So to avoid such rebuilds, they are maintained separately.
Enough of the theory now, let's see how we can implements the above in GO.
I am going to use 2 packages and each has its own approach.
- Using viper → here we use viper to read from configuration file
config.ymland also from environment variables.
- Using godotenv and os package from GO together → here we use godotenv to read from
.envfile and os to read from environment variables.
** I prefer the first approach of using viper, since it is more professional way of handling configuration files and environment variables together than having 2 separate packages for either of the tasks as in the 2nd approach.
Let's Dive in to Coding..
Approach 1: Using VIPER
Here, we use viper to read both from configuration file and environment variables and this is my preferred approach. Viper package can be found at: https://github.com/spf13/viper
Create a directory for the project, give it a suitable name. Then create a
src folder and create
main.go file inside it. Then, in the root folder create
config.yml file. And to maintain a model of the config file, let's create a
config.go file inside
Let's add the following lines in the
Now let's create the model for the configurations, add the following code in the
The configuration file contains, several variables, namely the server port, database name, credentials to access the database and also "EXAMPLE_PATH" and "EXAMPLE_VAR" variables, where "EXAMPLE_VAR" is also available in the environment variables.
Now let's see how to set up viper to read the above mentioned variables. In the
main.go file create the main function and add the following code.
Let's walk through the code,
viper.SetConfigName("config")→ Provides the configuration file name.
viper.SetConfigPath(".")→ Provides the path in which viper needs to search for the configuration file.
viper.AutomaticEnv()→ Tells viper to look at the Environment Variables.
viper.ReadInConfig()→ Reads all the configuration variables.
viper.Get("variable_name")→ Returns the value of the "variable name" variable from environment configurations first and if it is not available, it reads from the configuration file.
viper.SetDefault("database.dbname", "test_db")→ Sets a default value to the given variable "database.dbname", if it is not set in the configuration file or environment variables.
The output of the code is as follows,
If we observe the output carefully, in the configurations file "EXAMPLE_VAR" is assigned the value "variable from config.yml", but the actual output is, "This is an example environment variable". So the precedence here is clear, that a higher priority is given for the environment variables that the ones in configuration file. Phew, we have completed the implementation of this approach.
Well, it went longer than I expected. So, I decided to write the second approach as a separate article and it will be published in the near future. Anyway, I hope I did a good job and this was helpful and you like what you just read. Please let me know your comments and suggestions.
** The source code can be found at: https://github.com/BNPrashanth/go-poc-bp/tree/env-var-approach1
** When you are cloning the source code make sure you checkout to the
env-var-approach1 branch, all the above mentioned code is available in that branch.