DEV Community

Cover image for Meta programação em Go: usando Struct Tags
Eldio Santos Junior
Eldio Santos Junior

Posted on

Meta programação em Go: usando Struct Tags

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)
}
Enter fullscreen mode Exit fullscreen mode

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"`
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

O trecho de código acima retornaria algo como

Output:

server_port
hostname
Enter fullscreen mode Exit fullscreen mode

Exemplo no Go Playground

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
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Você pode ver o output da execução deste snippet no
Go Playground.

Top comments (0)