Como usar tags struct em Go

Introdução

As estruturas, ou structs, são usadas para coletar vários fragmentos de informações em uma unidade. Essas coletas de informações são usadas para descrever conceitos de nível superior, como um Address composto de uma Street, City, State e PostalCode. Ao ler essas informações a partir de sistemas como bancos de dados, ou APIs, você pode usar tags struct para controlar como essa informação é atribuída aos campos de uma struct. As tags struct são pequenos fragmentos de metadados, anexados aos campos de uma struct – que fornecem instruções para outro código em Go – que funciona com a struct.

Qual é a aparência de uma tag struct?

As tags struct do Go são anotações que aparecem após o tipo, em uma declaração struct em Go. Cada tag é composta por strings curtas, associadas a um valor correspondente.

Uma tag struct se parece com isso, com a tag deslocada com caracteres de crase (backtick) “”`:

type User struct {     Name string `example:"name"` } 

Assim, outro código em Go consegue examinar essas structs e extrair os valores atribuídos a chaves específicas que ele solicita. As tags struct não têm efeito sobre a operação do seu código sem algum outro código que as examine.

Teste este exemplo, para ver como as tags de struct se parecem e para ver que, sem o código de outro pacote, elas não têm efeito.

package main  import "fmt"  type User struct {     Name string `example:"name"` }  func (u *User) String() string {     return fmt.Sprintf("Hi! My name is %s", u.Name) }  func main() {     u := &User{         Name: "Sammy",     }      fmt.Println(u) } 

Isso resultará em:

OutputHi! My name is Sammy 

Esse exemplo define um tipo User com um campo Name. O campo Name recebeu uma tag struct do example:"name". Teríamos que nos referir a essa tag específica na conversa como “exemplo de tag de struct”, pois ela usa a palavra “exemplo” como sua chave. A tag de struct example tem o valor "name" no campo Name. No tipo User, também definimos o método String() necessário através da interface fmt.Stringer. Isso será chamado automaticamente quando enviarmos o tipo para o fmt.Println e nos dará a chance de produzir uma versão bem formatada da nossa struct.

Dentro do corpo de main, criamos uma nova instância do nosso tipo User e a enviamos para o fmt.Println. Embora a struct tenha um identificador de struct presente, vemos que ela não tem efeito na operação deste código do Go. Ela se comportará exatamente da mesma forma se o identificador da struct não estiver presente.

Para usar os identificadores de struct para conseguir algo, será necessário que outro código do Go seja escrito para examinar as structs no tempo de execução. A biblioteca padrão tem pacotes que usam identificadores de struct como parte da sua operação. O mais popular deles é o pacote encoding/json.

Codificando o JSON

JavaScript Object Notation (JSON) é um formato textual para a codificação de coletas de dados organizados em diferentes chaves de string. É comumente usado para comunicar dados entre programas diferentes, uma vez que o formato é bastante simples, a ponto de haver bibliotecas para decodificá-lo em muitas linguagens diferentes. A seguir, temos um exemplo do JSON:

{   "language": "Go",   "mascot": "Gopher" } 

Esse objeto JSON contém duas chaves, language e mascot. Depois dessas chaves estão os valores associados. Aqui, a chave language tem um valor de Go e o valor Gopher foi atribuído à chave mascot.

O codificador JSON na biblioteca padrão usa os identificadores de struct como anotações que indicam ao codificador como você gostaria de nomear seus campos na saída em JSON. Esses mecanismos de codificação e decodificação do JSON podem ser encontrados no pacote encoding/json.

Experimente este exemplo para ver como JSON está codificado sem os identificadores de struct:

package main  import (     "encoding/json"     "fmt"     "log"     "os"     "time" )  type User struct {     Name          string     Password      string     PreferredFish []string     CreatedAt     time.Time }  func main() {     u := &User{         Name:      "Sammy the Shark",         Password:  "fisharegreat",         CreatedAt: time.Now(),     }      out, err := json.MarshalIndent(u, "", "  ")     if err != nil {         log.Println(err)         os.Exit(1)     }      fmt.Println(string(out)) } 

Isso imprimirá o seguinte resultado:

Output{   "Name": "Sammy the Shark",   "Password": "fisharegreat",   "CreatedAt": "2019-09-23T15:50:01.203059-04:00" } 

Definimos uma struct que descreve um usuário com campos, incluindo seu nome, senha e o momento em que o usuário foi criado. Dentro da função main, criamos uma instância desse usuário, fornecendo valores para todos os campos, exceto PreferredFish (Sammy gosta de todos os peixes). Então, enviamos a instância do User para a função json.MarshalIndent. Isso é usado para que possamos ver mais facilmente o resultado do JSON sem usar uma ferramenta de formatação externa. Essa chamada poderia ser substituída por json.Marshal(u) para receber o JSON sem qualquer espaço em branco adicional. Os dois argumentos adicionais para o json.MarshalIndent controlam o prefixo para a saída (que omitimos com a string vazia) e os caracteres que são usados para o recuo, que aqui são dois caracteres de espaço. Quaisquer erros produzido a partir do json.MarshalIndent ficam registrados e o programa se encerra usando o os.Exit(1). Por fim, lançamos o []byte retornado do json.MarshalIndent para uma string e entregamos a string resultante para o fmt.Println, para impressão no terminal.

Os campos da struct aparecem exatamente como as nomeamos. Esse não é o estilo típico que talvez você espere do JSON, o qual usa o camel casing para os nomes dos campos. [Nota: na convenção Camel-Case, a primeira letra da primeira palavra fica em minúscula e a primeira letra de todas as palavras subsequentes fica em maiúscula]. Neste exemplo, você alterará os nomes do campo para seguir o estilo camel case. Ao executar esse exemplo, você verá que isso não vai funcionar porque os nomes desejados para os campos entram em conflito com as regras do Go sobre nomes de campo exportados.

package main  import (     "encoding/json"     "fmt"     "log"     "os"     "time" )  type User struct {     name          string     password      string     preferredFish []string     createdAt     time.Time }  func main() {     u := &User{         name:      "Sammy the Shark",         password:  "fisharegreat",         createdAt: time.Now(),     }      out, err := json.MarshalIndent(u, "", "  ")     if err != nil {         log.Println(err)         os.Exit(1)     }      fmt.Println(string(out)) } 

Isso apresentará a seguinte saída:

Output{} 

Nesta versão, modificamos os nomes dos campos para serem em estilo camel case. Agora, Name passa a ser name, Password a password e, finalmente, CreatedAt passa a ser createdAt. Dentro do corpo de main, alteramos a instanciação de nossa struct para usar esses novos nomes. Então, enviamos a struct para a função json.MarshalIndent como antes. A saída, dessa vez é um objeto JSON vazio, {}.

Os campos em camel case requerem que o primeiro caractere seja minúsculo. Embora JSON não se importe como você nomeia seus campo, o Go se importa, uma vez que indica a visibilidade do campo fora do pacote. Como o pacote encoding/jason é um pacote separado do pacote main que estamos usando, devemos usar caixa alta para o primeiro caractere, a fim de torná-lo visível para o encoding/json. Neste caso, aparentemente, estaríamos com um impasse. Assim, precisamo encontrar uma maneira de transmitir ao codificador JSON como gostaríamos de nomear esse campo.

Usando identificadores de struct para controlar a codificação.

Você pode modificar o exemplo anterior para os campos sejam exportados com a codificação correta, ou seja, com nomes de campo em camel case, anotando cada campo com um identificador de struct. O identificador de struct que o encoding/json reconhece tem uma chave do json e um valor que controla a saída. Colocar a versão dos nomes dos campos em camel case como o valor para a chave json, fará o codificador usar aquele nome. Este exemplo conserta as duas tentativas anteriores:

package main  import (     "encoding/json"     "fmt"     "log"     "os"     "time" )  type User struct {     Name          string    `json:"name"`     Password      string    `json:"password"`     PreferredFish []string  `json:"preferredFish"`     CreatedAt     time.Time `json:"createdAt"` }  func main() {     u := &User{         Name:      "Sammy the Shark",         Password:  "fisharegreat",         CreatedAt: time.Now(),     }      out, err := json.MarshalIndent(u, "", "  ")     if err != nil {         log.Println(err)         os.Exit(1)     }      fmt.Println(string(out)) } 

Isso resultará em:

Output{   "name": "Sammy the Shark",   "password": "fisharegreat",   "preferredFish": null,   "createdAt": "2019-09-23T18:16:17.57739-04:00" } 

Revertemos os nomes de campo para ficarem visíveis para outros pacotes, usando caixa alta nas primeiras letras de seus respectivos nomes. No entanto, adicionamos desta vez os identificadores de struct na forma do json:"name", onde "name" era o nome que queríamos que o json.MarshalIndent usasse ao imprimir nossa struct como JSON.

Agora, formatamos nosso JSON corretamente. No entanto, note que os campos para alguns valores foram impressos, embora não tivéssemos definido tais valores. O codificador JSON também pode eliminar esses campos, se você quiser.

Removendo os campos vazios do JSON

Mais comumente, queremos suprimir os campos de saída que não estão definidos no JSON. Como todos os tipos em Go tem um “valor zero”, um valor padrão para o qual eles foram definidos, o pacote encoding/json precisa de informações adicionais para conseguir dizer que um dado campo deverá ser considerado como não definido ao assumir esse valor zero. Dentro de parte do valor de qualquer identificador de struct do json, você pode acrescentar um sufixo ao nome que deseja para o seu campo, usando o omitempty. Isso dirá ao codificador JSON que ele deve suprimir a saída desse campo quando ele estiver definido para o valor zero. O exemplo a seguir corrige os exemplos anteriores para não mostrar mais os campos vazios:

package main  import (     "encoding/json"     "fmt"     "log"     "os"     "time" )  type User struct {     Name          string    `json:"name"`     Password      string    `json:"password"`     PreferredFish []string  `json:"preferredFish,omitempty"`     CreatedAt     time.Time `json:"createdAt"` }  func main() {     u := &User{         Name:      "Sammy the Shark",         Password:  "fisharegreat",         CreatedAt: time.Now(),     }      out, err := json.MarshalIndent(u, "", "  ")     if err != nil {         log.Println(err)         os.Exit(1)     }      fmt.Println(string(out)) } 

Este exemplo irá mostrar o resultado:

Output{   "name": "Sammy the Shark",   "password": "fisharegreat",   "createdAt": "2019-09-23T18:21:53.863846-04:00" } 

Nós modificamos os exemplos anteriores para que o campo PreferredFish tenha agora o identificador de struct json:"preferredFish,omitempty". A presença do acréscimo ,omitempty faz com que o codificador JSON ignore aquele campo, uma vez que decidimos deixá-lo como não definido. Nos resultados de nossos exemplos anteriores, isso aparecia com valor null.

Esse resultado ficou muito melhor, mas ainda estamos imprimindo a senha do usuário. O pacote encoding/json proporciona outra maneira de ignorarmos totalmente os campos privados.

Ignorando os campos privados

Alguns campos devem ser exportados das structs para que outros pacotes possam interagir corretamente com o tipo. No entanto, esses campos podem ser de natureza confidencial. Assim, em circunstâncias como essas,vamos querer que o codificador JSON ignore totalmente esses campos – mesmo quando ele estiver definido. Para tanto, usamos o valor especial - como o argumento de valor para um identificador de struct do json:.

Este exemplo corrige o problema da exposição da senha do usuário.

package main  import (     "encoding/json"     "fmt"     "log"     "os"     "time" )  type User struct {     Name      string    `json:"name"`     Password  string    `json:"-"`     CreatedAt time.Time `json:"createdAt"` }  func main() {     u := &User{         Name:      "Sammy the Shark",         Password:  "fisharegreat",         CreatedAt: time.Now(),     }      out, err := json.MarshalIndent(u, "", "  ")     if err != nil {         log.Println(err)         os.Exit(1)     }      fmt.Println(string(out)) } 

Quando executar o exemplo, verá este resultado:

Output{   "name": "Sammy the Shark",   "createdAt": "2019-09-23T16:08:21.124481-04:00" } 

A única coisa que mudamos nesse exemplo – em relação aos exemplos anteriores, foi o campo da senha que agora usa o valor especial "-" em seu identificador de struct json:. Constatamos isso no resultado desse exemplo em que o campo password não está mais presente.

Os recursos ,omitempty e "-" do pacote encoding/json não são padrões. O que um pacote decide fazer com os valores de um identificador de struct depende de sua implementação. Como o pacote encoding/json faz parte da biblioteca padrão, outros pacotes também implementaram esses recursos da mesma forma somente a título de convenção. No entanto, é importante ler a documentação dos pacotes de terceiros que utilizem identificadores de struct, a fim de aprender quais são compatíveis e quais não são.

Conclusão

Os identificadores de struct proporcionam um meio poderoso para aumentar a funcionalidade do código que funciona com suas structs. Muitos pacotes da biblioteca padrão e de terceiros oferecem maneiras de personalizar sua operação, usando identificadores de struct. Usá-los de maneira eficaz no seu código propicia esse comportamento de personalização e documenta de maneira sucinta como esses campos são usados para conhecimento dos futuros desenvolvedores.