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 …


This content originally appeared on DEV Community and was authored by Eldio Santos Junior

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

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
}

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.


This content originally appeared on DEV Community and was authored by Eldio Santos Junior


Print Share Comment Cite Upload Translate Updates
APA

Eldio Santos Junior | Sciencx (2024-11-01T18:18:56+00:00) Meta programação em Go: usando Struct Tags. Retrieved from https://www.scien.cx/2024/11/01/meta-programacao-em-go-usando-struct-tags/

MLA
" » Meta programação em Go: usando Struct Tags." Eldio Santos Junior | Sciencx - Friday November 1, 2024, https://www.scien.cx/2024/11/01/meta-programacao-em-go-usando-struct-tags/
HARVARD
Eldio Santos Junior | Sciencx Friday November 1, 2024 » Meta programação em Go: usando Struct Tags., viewed ,<https://www.scien.cx/2024/11/01/meta-programacao-em-go-usando-struct-tags/>
VANCOUVER
Eldio Santos Junior | Sciencx - » Meta programação em Go: usando Struct Tags. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/11/01/meta-programacao-em-go-usando-struct-tags/
CHICAGO
" » Meta programação em Go: usando Struct Tags." Eldio Santos Junior | Sciencx - Accessed . https://www.scien.cx/2024/11/01/meta-programacao-em-go-usando-struct-tags/
IEEE
" » Meta programação em Go: usando Struct Tags." Eldio Santos Junior | Sciencx [Online]. Available: https://www.scien.cx/2024/11/01/meta-programacao-em-go-usando-struct-tags/. [Accessed: ]
rf:citation
» Meta programação em Go: usando Struct Tags | Eldio Santos Junior | Sciencx | https://www.scien.cx/2024/11/01/meta-programacao-em-go-usando-struct-tags/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.