Tags de struct são metadados anexados aos campos de uma struct em Go. Elas permitem que bibliotecas e ferramentas externas interpretem informações adicionais sobre cada campo, como nomes alternativos, regras de validação ou instruções de serialização. Embora sejam opcionais, são amplamente utilizadas em aplicações reais, especialmente com JSON, bancos de dados e validação.

Nesta aula, vamos explorar a sintaxe das tags, como lê-las usando o pacote reflect e como aplicá-las em cenários comuns, com exemplos práticos que você pode adaptar aos seus projetos.

Sintaxe

Uma tag de struct é uma string literal que aparece após o tipo de um campo, entre crases (backticks). A tag é composta por uma ou mais diretivas, cada uma com um nome e um valor opcional, separadas por espaços. A forma geral é:

type Exemplo struct {
    Campo Tipo `nome:"valor" outra:"valor2"`
}

O valor da tag é uma string que pode conter qualquer caractere, mas é comum usar o formato chave:"valor". As chaves são identificadores, e os valores são strings entre aspas duplas. Se houver mais de uma diretiva, elas são separadas por espaços. É importante que a tag seja escrita exatamente com crases, pois aspas duplas não funcionam para tags (a menos que sejam escapadas, o que é raro).

Exemplo prático:

type Pessoa struct {
    Nome  string `json:"nome"`
    Idade int    `json:"idade"`
    Email string `json:"email,omitempty"`
}

Neste exemplo, usamos a tag json para indicar como o campo deve ser serializado/desserializado pelo pacote encoding/json. A opção omitempty omite o campo se for zero.

Lendo tags via reflexão

Para acessar as tags de uma struct em tempo de execução, usamos o pacote reflect. A função reflect.TypeOf retorna o tipo da struct, e o método Field(i) retorna informações sobre o campo na posição i. A tag é obtida com o método Tag.Get(key) ou Tag.Lookup(key).

Exemplo básico:

package main

import (
    "fmt"
    "reflect"
)

type Pessoa struct {
    Nome  string `json:"nome" db:"nome"`
    Idade int    `json:"idade" db:"idade"`
}

func main() {
    t := reflect.TypeOf(Pessoa{})
    for i := 0; i < t.NumField(); i++ {
        campo := t.Field(i)
        fmt.Printf("Campo: %s\n", campo.Name)
        fmt.Printf("Tag json: %s\n", campo.Tag.Get("json"))
        fmt.Printf("Tag db: %s\n", campo.Tag.Get("db"))
        fmt.Println("---")
    }
}

Ao executar, veremos os nomes dos campos e as tags associadas. O método Get retorna uma string vazia se a chave não existir. Já Lookup retorna dois valores: a tag e um booleano indicando se a chave foi encontrada.

Exemplo com Lookup:

tag, ok := campo.Tag.Lookup("json")
if ok {
    fmt.Println("Tag json:", tag)
} else {
    fmt.Println("Tag json não encontrada")
}

Usos comuns (json, db)

O uso mais comum de tags é para serialização JSON, como vimos. O pacote encoding/json usa as tags json para controlar o nome do campo no JSON, opções como omitempty e string, e até mesmo ignorar o campo com json:"-".

Exemplo:

type Produto struct {
    ID        int     `json:"id"`
    Nome      string  `json:"nome"`
    Preco     float64 `json:"preco,omitempty"`
    Desconto  float64 `json:"-"`
    Categoria string  `json:"categoria,omitempty"`
}

Outro uso comum é em bancos de dados, especialmente com ORMs como GORM. As tags db ou gorm indicam o nome da coluna, restrições, índices etc. Por exemplo:

type Usuario struct {
    ID        uint   `gorm:"primaryKey"`
    Nome      string `gorm:"size:100;not null"`
    Email     string `gorm:"uniqueIndex;not null"`
    CreatedAt time.Time
}

Aqui, a tag gorm define a chave primária, o tamanho do campo, a restrição NOT NULL e um índice único. O GORM lê essas tags para criar a tabela e as operações de banco.

Exemplos

Vamos consolidar com exemplos completos. Primeiro, um exemplo com JSON:

package main

import (
    "encoding/json"
    "fmt"
)

type Pessoa struct {
    Nome  string `json:"nome"`
    Idade int    `json:"idade"`
    Email string `json:"email,omitempty"`
    Senha string `json:"-"`
}

func main() {
    p := Pessoa{
        Nome:  "Maria",
        Idade: 30,
        Email: "maria@example.com",
        Senha: "secreta",
    }
    dados, _ := json.Marshal(p)
    fmt.Println(string(dados))
}

Saída: {"nome":"Maria","idade":30,"email":"maria@example.com"} (a senha é ignorada).

Segundo, um exemplo de leitura de tags via reflexão para criar um mapeamento genérico:

package main

import (
    "fmt"
    "reflect"
)

type Config struct {
    Host string `env:"HOST" def:"localhost"`
    Port int    `env:"PORT" def:"8080"`
}

func main() {
    t := reflect.TypeOf(Config{})
    v := reflect.ValueOf(Config{})
    for i := 0; i < t.NumField(); i++ {
        campo := t.Field(i)
        valor := v.Field(i)
        env := campo.Tag.Get("env")
        def := campo.Tag.Get("def")
        fmt.Printf("%s: env=%s def=%s valor=%v\n", campo.Name, env, def, valor.Interface())
    }
}

Isso pode ser usado para preencher campos a partir de variáveis de ambiente, com valores padrão.

Boas práticas

Ao usar tags, siga estas recomendações:

  • Use tags consistentes e documentadas.
  • Prefira chaves padronizadas (ex.: json, db, validate) para evitar conflitos.
  • Evite tags desnecessárias; mantenha as structs limpas.
  • Para bibliotecas próprias, defina nomes de tags com prefixo (ex.: mylib:"...").
  • Teste a leitura de tags com reflexão quando criar ferramentas genéricas.

Referências

Exercícios

  1. Defina uma struct Livro com campos Titulo, Autor, Ano e ISBN. Use tags JSON para que os campos sejam serializados como titulo, autor, ano e isbn, e faça com que ISBN seja omitido se vazio.

    ✓ Resposta:
    type Livro struct {
        Titulo string `json:"titulo"`
        Autor  string `json:"autor"`
        Ano    int    `json:"ano"`
        ISBN   string `json:"isbn,omitempty"`
    }
  2. Escreva uma função que recebe uma struct qualquer e imprime todas as tags json de seus campos, no formato NomeCampo: tag.

    ✓ Resposta:
    func imprimirTagsJSON(s interface{}) {
        t := reflect.TypeOf(s)
        if t.Kind() != reflect.Struct {
            fmt.Println("Não é uma struct")
            return
        }
        for i := 0; i < t.NumField(); i++ {
            campo := t.Field(i)
            tag := campo.Tag.Get("json")
            fmt.Printf("%s: %s\n", campo.Name, tag)
        }
    }
  3. Usando a struct do exercício 1, serialize uma instância com JSON e mostre a saída. O que acontece se ISBN estiver vazio?

    ✓ Resposta:
    livro := Livro{Titulo: "O Senhor dos Anéis", Autor: "J.R.R. Tolkien", Ano: 1954}
    dados, _ := json.Marshal(livro)
    fmt.Println(string(dados))
    // Saída: {"titulo":"O Senhor dos Anéis","autor":"J.R.R. Tolkien","ano":1954}
    // O campo isbn é omitido porque está vazio.
  4. Crie uma struct Config com tags env e default. Escreva um código que lê as tags e imprime o nome da variável de ambiente e o valor padrão para cada campo.

    ✓ Resposta:
    type Config struct {
        Host string `env:"HOST" default:"localhost"`
        Port int    `env:"PORT" default:"8080"`
    }
    
    func main() {
        t := reflect.TypeOf(Config{})
        for i := 0; i < t.NumField(); i++ {
            campo := t.Field(i)
            env := campo.Tag.Get("env")
            def := campo.Tag.Get("default")
            fmt.Printf("Campo %s: env=%s default=%s\n", campo.Name, env, def)
        }
    }
  5. Explique a diferença entre os métodos Get e Lookup de reflect.StructTag e dê um exemplo de uso de Lookup.

    ✓ Resposta: Get retorna apenas a string (vazia se não existir). Lookup retorna a string e um booleano indicando se a chave existe. Exemplo:
    tag, ok := campo.Tag.Lookup("json")
    if ok {
        fmt.Println("Tag encontrada:", tag)
    } else {
        fmt.Println("Tag não encontrada")
    }