Here we are going to create a custom struct tag called config and it will have two fields to go with which are kind|path just to play with.


Our example scenario is about working with the application secrets and plain configuration values. We will be overriding the value of "secret" fields and leaving the "plain" fields intact. This is just for the sake of demonstration purposes. In a real life example, you would normally use the "path" values to locate the actual values in an internal/external config/secret storage service such as AWS Secrets Manager/Parameter Store, HashiCorp Vault etc. for each fields. It is all up to you how you design the system. This is just an idea so feel free to refactor or adopt it. Although it is the subject to a different topic you could follow environment.service.***, environment/service/***, service/environment/*** syntax for the "path" or something else. The syntax might also be enforced by the service you will be using.


config.go


package config

import (
"fmt"
"reflect"
)

// Bind iterates through all the fields in the config and operates only on
// custom "config" tag as long as it matches certain criteria.
func Bind(cfg interface{}) error {
configSource := reflect.ValueOf(cfg)
if configSource.Kind() != reflect.Ptr {
return fmt.Errorf("config must be a pointer")
}
configSource = configSource.Elem()
if configSource.Kind() != reflect.Struct {
return fmt.Errorf("config must be a struct")
}

configType := configSource.Type()

for i := 0; i < configSource.NumField(); i++ {
fieldTag, ok := configType.Field(i).Tag.Lookup("config")
if !ok {
continue
}

fieldName := configType.Field(i).Name
fieldValue := configSource.FieldByName(fieldName)
if !fieldValue.IsValid() {
continue
}
if !fieldValue.CanSet() {
continue
}

tagKind, tagPath := tag(fieldTag)
if tagKind == "" && tagPath == "" {
continue
}

if tagKind == tagSecret {
// This is just a random act. You should handle it as per your setup.
// Also add other type cases as well
switch configSource.Field(i).Kind() {
case reflect.String:
fieldValue.SetString("***")
case reflect.Int:
fieldValue.SetInt(000)
}
}
}

return nil
}

tag.go


package config

import (
"strings"
)

const (
tagPlain = "plain"
tagSecret = "secret"
)

// tag extracts kind and path values from the incoming tag value. Both values are
// must be non-empty string otherwise an empty string is returned for both.
func tag(tag string) (string, string) {
tagParts := strings.Split(tag, ",")
if len(tagParts) == 0 || len(tagParts) != 2 {
return "", ""
}

kindParts := strings.Split(tagParts[0], "=")
if len(kindParts) == 0 || len(kindParts) != 2 {
return "", ""
}
if kindParts[0] != "kind" || (kindParts[1] != tagPlain && kindParts[1] != tagSecret) {
return "", ""
}

pathParts := strings.Split(tagParts[1], "=")
if len(pathParts) == 0 || len(pathParts) != 2 {
return "", ""
}
if pathParts[0] != "path" || pathParts[1] == "" {
return "", ""
}

return kindParts[1], pathParts[1]
}

main.go


package main

import (
"fmt"
"log"
"time"

"you/config"
)

type Config struct {
Shutdown time.Duration
PrivateKey string `config:"kind=secret,path=common.ssh.private_key"`
Password string `config:"kind=secret,path=team.login.password"`
YearFound int `config:"kind=plain,path=team.year_found"`
}

func main() {
cfg := Config{
Shutdown: time.Minute,
PrivateKey: "prv",
Password: "psw",
YearFound: 2021,
}

fmt.Printf("ORIGINAL: %+v\n", cfg)

if err := config.Bind(&cfg); err != nil {
log.Fatalln(err)
}

fmt.Printf("MODIFIED: %+v\n", cfg)
}

Test


$ go run main.go 
ORIGINAL: {Shutdown:1m0s PrivateKey:prv Password:psw YearFound:2021}
MODIFIED: {Shutdown:1m0s PrivateKey:*** Password:*** YearFound:2021}