Sempre gostei de meta programação, mas vejo que no Go isso é meio obscuro... Você encontra uns poucos textos sobre (e a maioria deles usa o mesmo exemplo).
Um tempo atrás eu fui procurar uma solução pra serializar uma struct pro formato properties
do Java e, como não encontrei, resolvi criar algo que utilizasse o mesmo esquema de tags que o JSON serializer (o projeto esta nesse repositório eldius/properties). Vou usar esse caso como exemplo de como criar e utilizar suas tags customizadas.
Tentei seguir o exemplo do decoder de JSON.
if err := json.NewEncoder(os.Stdout).Encode(&struct{}{}); err != nil {
panic(err)
}
if err := json.NewDecoder(os.Stdin).Decode(&struct{}{}); err != nil {
panic(err)
}
Por onde começamos?
Então vamos lá, como podemos acessar as tags dos atributos de uma struct?
Vamos usar essa struct de exemplo:
type MyProperties struct {
ServerPort int `prop:"server_port"`
Hostname string `prop:"hostname"`
}
Para pegarmos o valor da tag prop
de cada atributo fazemos o seguinte:
props := MyProperties{
ServerPort: 8080,
Hostname: "localhost",
}
valueOf := reflect.ValueOf(&props)
element := valueOf.Elem()
valueType := element.Type()
// Primeiro atributo
field0TagValue, ok := valueType.Field(0).Tag.Lookup("prop")
if !ok {
panic(errors.New("no prop tag"))
}
fmt.Println(field0TagValue)
// Segundo atributo
field1TagValue, ok := valueType.Field(1).Tag.Lookup("prop")
if !ok {
panic(errors.New("no prop tag"))
}
fmt.Println(field1TagValue)
O trecho de código acima retornaria algo como
Output:
server_port
hostname
Daí, decidi simplificar a minha vida, achei que só
serializaria/deserializar os atributos da "instância mãe"
(não quis entrar em atributos de atributos 😂). Então
meu encoder ficou mais ou menos assim:
func (d *Decoder) Decode(v any) error {
// Leitura do arquivo e transformação dos valores
// em um mapa/dicionário
values, err := readToMap(d.r)
if err != nil {
err = fmt.Errorf("reading input content: %w", err)
return err
}
valueSource := reflect.ValueOf(v)
if valueSource.Kind() != reflect.Ptr {
return ErrNotAPointer
}
valueSource = valueSource.Elem()
if valueSource.Kind() != reflect.Struct {
return ErrNotAStruct
}
valueType := valueSource.Type()
// Iterando pelos atributos da struct
for i := 0; i < valueType.NumField(); i++ {
// Pegar valor da tag
fieldTag, ok := valueType.Field(i).Tag.Lookup(propertiesTag)
if !ok {
continue
}
// Buscar nome do atributo
fieldName := valueType.Field(i).Name
fieldValue := valueSource.FieldByName(fieldName)
if !fieldValue.IsValid() {
continue
}
// Validando se podemos alterar seu valor
if !fieldValue.CanSet() {
continue
}
// Pegar valor do atributo dentro do map
v, ok := values[fieldTag]
if !ok {
continue
}
// Definindo o valor do atributo de acordo
// com o seu tipo
switch valueSource.Field(i).Kind() {
case reflect.String:
fieldValue.SetString(v)
case reflect.Int:
err := setIntValue(v, 64, fieldValue)
if err != nil {
err = fmt.Errorf("failed to parse int value for field '%s':%w", fieldTag, err)
return err
}
// DEMAIS TIPOS //
}
}
return nil
}
func readToMap(r io.Reader) (map[string]string, error) {
b, err := io.ReadAll(r)
if err != nil {
err = fmt.Errorf("reading content: %w", err)
return nil, err
}
values := make(map[string]string)
for _, l := range strings.Split(string(b), "\n") {
if strings.HasPrefix(l, "#") {
continue
}
if len(l) == 0 {
continue
}
tmp := strings.Split(l, "=")
values[tmp[0]] = tmp[1]
}
return values, nil
}
No meu caso eu precisava apenas pegar o nome do
atributo na tag, então isso já resolveria, mas
caso minha tag tivesse propriedades, como as tags
de validação, que possuem atributos (algo tipo
validation:"field_name,required=true,min=1,max=10"
), você
precisa tratar a string para extrair essas
informações na mão.
Segue um exemplo simplificado de como fazer isso.
func parseComplexTag(rawTagValue string) (map[string]interface{}, error) {
var result map[string]interface{}
tagSplittedValues := strings.Split(rawTagValue, ",")
for _, tagPart := range tagSplittedValues {
var tagPartSplitted = strings.Split(tagPart, "=")
if len(tagPartSplitted) == 1 {
result["name"] = tagPartSplitted[0]
} else if len(tagPartSplitted) == 2 {
var err error
switch tagPartSplitted[0] {
case "required":
result[tagPartSplitted[0]], err = strconv.ParseBool(tagPartSplitted[1])
if err != nil {
return nil, err
}
case "min":
result[tagPartSplitted[0]], err = strconv.ParseInt(tagPartSplitted[1], 10, 64)
if err != nil {
return nil, err
}
case "max":
result[tagPartSplitted[0]], err = strconv.ParseInt(tagPartSplitted[1], 10, 64)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown tag parameter: %s", tagPart)
}
} else {
return nil, fmt.Errorf("invalid tag parameter format: %s", tagPart)
}
return result, nil
}
}
Você pode ver o output da execução deste snippet no
Go Playground.
Top comments (0)