Validations, in general, are one of the most common problems across all programming languages.
Before I start explaining how to do it idiomatically in golang... Let me briefly explain why I dived into it in the first place.
While working on a project with tonnes of micro-services,
there was a need to maintain build time configurations for each of them.
This meant validating those configurations as well. Otherwise the micro-services would have been at the risk of runtime failures! 🤕
So let's say we have the below configuration in config.json
{
"server_url": "https://some-server-url",
"app_port": 8080
}
This can then be encapsulated in a golang struct:
type Config struct {
ServerUrl string `json:"server_url"`
AppPort int `json:"app_port"`
}
We would load the config.json to the struct:
import (
"encoding/json"
"errors"
"fmt"
"os"
)
func LoadConfig() (*Config, error) {
configFile, err := os.Open("configuration/config.json")
if err != nil {
log.Fatal("Could not open config file : ", err.Error())
}
decoder := json.NewDecoder(configFile)
config := Config{}
decodeErr := decoder.Decode(&config)
if decodeErr != nil {
log.Fatal("Could not decode config file : ", decodeErr.Error())
}
if !Validate(config) {
return nil, errors.New("Invalid config !")
}
return &config, nil
}
Now, let's say we want to validate:
- Server URL and port are non-empty
- The value of port is numeric and is between 8080 and 8085
Now the straight-forward way would be :
Write if-else
conditional code to validate each of them. Something like:
func Validate(config Config) bool {
if config.ServerUrl == "" {
return false
}
if config.AppPort == 0 {
return false
}
if config.AppPort >= 8080 || config.AppPort <= 8085 {
return false
}
return true
}
This can get pretty messy with more such fields and validations. Yikes! 😵💫
There's got to be a better way (Otherwise this blog wouldn't exist ! )
A cleaner, idiomatic way to do this is to use struct validations ❤
type Config struct {
ServerUrl string `json:"server_url" validate:"required"`
AppPort int `json:"app_port" validate:"required,numeric,gte=8080,lte=8085"`
}
Declarative and closer to the struct definition. Isn't that beautiful!
Now our validate function would look something like this:
func Validate(config Config) bool {
validate := validator.New()
err := validate.Struct(config)
if err != nil {
fmt.Println("Invalid config !", err.Error())
return false
}
return true
}
Furthermore, we can have more meaningful errors to point out failing validations:
import "github.com/go-playground/validator/v10"
func Validate(config Config) bool {
validate := validator.New()
err := validate.Struct(config)
if err != nil {
fmt.Println("Invalid config !")
for _, validationErr := range err.(validator.ValidationErrors) {
fmt.Println(validationErr.StructNamespace() + " violated " + validationErr.Tag() + " validation.")
}
return false
}
return true
}
The final gist would look something like this:
https://github.com/PankhudiB/go-config-validation/blob/main/main.go
Head over to the awesome validator library for more such tags...
"But Pankhudi, what if I need to add validations of my own?" 🤔 - you ask.
To this, I answer head over to the next blog in the series...
Top comments (3)
Great!
Very nicely explained.
I assume we can use similar methods for other kinds of validations like yaml?
Yes, definitely...the validator library is agnostic of how you store the data. It acts on top of a struct.