DEV Community

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

Posted on

2

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.

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay