Continuando a conversa sobre interfaces e sobre manipulação de JSON, tem um recurso muito útil que usamos para ajudar o nosso sistema a falar melhor com o PostgresQL e pREST.
O formato de data padrão do Postgres é incompatível com o formato padrão do Go e o nosso sistema usa muito JSON tanto para mandar como receber informações do banco de dados.
Ter que lembrar toda hora de fazer o parser da data para o formato correto simplesmente não é pratico; é muito melhor ensinar o Go a lidar com data e hora no bom e velho formato ISO 8601.
Aliás, recomendo muito sempre usar ISO 8601 UTC para tudo no back-end e apenas mudar para o formato e timezone local quando for exibir para o usuário, mas isso é uma história para outro dia.
O código fonte do package Go está disponível no GitHub.
MarshalJSON
Agora veremos como o código funciona. No exemplo abaixo criamos uma struct Time e implementamos uma função MarshalJSON para ela. Na função main instanciamos uma struct com alguns dados, em seguida usamos a função json.MarshalIndent que percorre a struct e quando ela encontrar o campo Time vai usar a função que definimos e não a default do sistema.
package main import ( "encoding/json" "fmt" "time" ) type Time struct { time.Time } const layout = "2006-01-02T15:04:05.999999" func (t Time) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf(`"%s"`, t.Time.Format(layout))), nil } func main() { data := struct { Name string Time Time }{ Name: "teste", Time: Time{time.Now().Add(time.Millisecond * time.Duration(54321))}, } json, err := json.MarshalIndent(data, "", "\t") if err != nil { fmt.Println(err) } fmt.Println(string(json)) }
Exemplo de retorno usando The Go Playground – veja que o formato da data obedece a nossa função.
{ "Name": "teste", "Time": "2009-11-10T23:00:54.321" }
UnmarshalJSON
No próximo exemplo faremos o contrário: agora definiremos uma função UnmarshalJSON para nossa struct. Veja o exemplo:
package main import ( "encoding/json" "fmt" "time" ) type Time struct { time.Time } const layout = "2006-01-02T15:04:05.999999" func (t *Time) UnmarshalJSON(b []byte) (err error) { if b[0] == '"' && b[len(b)-1] == '"' { b = b[1 : len(b)-1] } if string(b) == `null` { *t = Time{} return } t.Time, err = time.Parse(layout, string(b)) return } func main() { data := struct { Name string Time Time } b := []byte(`{"Name": "teste", "Time": "2009-11-10T23:00:54.321"}`) err := json.Unmarshal(b, &data) if err != nil { fmt.Println(err) } fmt.Println(data.Time.String()) }
Quando o sistema recebe o array de bytes para fazer parse e popular a struct, ele percorre os dados e ao encontrar o campo Time o pacote usa a nossa função UnmarshalJSON no lugar da default do sistema, assim conseguimos ler corretamente os dados mesmo não sendo o padrão do sistema.
Além de formatar os dados corretamente, também podemos usar esse recurso para outras tarefas. Por exemplo, é possível percorrer os campos verificando as informações de um determinado tipo e retornar um erro caso encontre algum dado inválido; é uma forma de validação de dados que trabalha internamente no parser do tipo e pode ser muito útil.
Só devemos tomar cuidado porque pode acabar ocultando de onde o erro está vindo, então escreva boas mensagens de erro. No exemplo do manual do próprio pacote json, o exemplo é um contador que conta animais em um zoológico.
Links úteis
Código fonte dos exemplos de hoje: