用Golang创建自定义结构标签的详细指南

273 阅读2分钟

多年来,我们一直投入大量的个人时间和精力,与大家分享我们的知识。然而,我们现在需要你的帮助来维持这个博客的运行。你所要做的只是点击网站上的一个广告,否则它将由于托管等费用而不幸被关闭。谢谢你。

在这里,我们将创建一个名为config 的自定义结构标签,它将有两个字段,即kind|path ,只是为了玩玩。

我们的示例方案是关于处理应用程序的秘密和普通配置值。我们将覆盖 "秘密 "字段的值,并保留 "普通 "字段。这只是为了演示的目的。在现实生活中,你通常会使用 "路径 "值来定位内部/外部配置/秘密存储服务中的实际值,如AWS Secrets Manager/Parameter Store、HashiCorp Vault等的每个字段。这完全取决于你如何设计这个系统。这只是一个想法,所以请随意重构或采用它。虽然这是一个不同的主题,但你可以按照environment.service.***,environment/service/***,service/environment/*** 语法来表示 "路径 "或其他东西。语法也可以由你将使用的服务来强制执行。

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)
}

测试

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