Маршалинг и анмаршалинг нестандартных форматов дат в Go

от автора

Думаю, многие согласятся, что работа с датами практически в любом языке программирования это невероятная головная боль. Дата и время не десятичны, часовые пояса — господи, почему они не могут быть просто целыми — ну и конечно бесчисленное множество форматов даты и времени. Можно, конечно, возразить, что мол есть ISO 8601 и чего тебе дураку ещё надо, но давайте, положа руку на сердце, скажем — как часто вам доводилось сталкиваться с соблюдением этого стандарта в сторонних API? Не знаю, как с этим обстоят дела за рубежом, надеюсь, в комментариях мне подскажут, но на постсоветских просторах ситуация — обнять и плакать. Каждый использует свой собственный, только ему удобный, формат времени и даты и разбирайся с этим как хочешь.

Я расскажу про собственный опыт и про найденное решение.

Проблема, с которой я столкнулся, не выглядела так уж и ужасно, формат даты в API, с которым мне предстояло взимодействовать, выглядел так: YYYY-MM-DD hh:mm:dd. Смотрится вроде нормально, не так ли?

Нет.

Абсолютно ненормально.

Те, кто знаком с форматом ISO 8601, уже поняли в чем подвох. Для наглядности:

package main  import (     "encoding/json"     "log"     "time" )  type Dated struct {     DateTime time.Time }  func main() {     input := []byte("{\"datetime\": \"1900-01-01 12:00:04\"}")     var d Dated     err := json.Unmarshal(input, &d)     if err != nil {         log.Fatal(err)     } }

В результате что? Правильно, ошибка:

parsing time ""1900-01-01 12:00:04"" as ""2006-01-02T15:04:05Z07:00"": cannot parse " 12:00:04"" as "T"

Что логично, ведь в Go из коробки есть ограниченное количество форматов времени и даты,
которые он может распарсить:

ANSIC       = "Mon Jan _2 15:04:05 2006" UnixDate    = "Mon Jan _2 15:04:05 MST 2006" RubyDate    = "Mon Jan 02 15:04:05 -0700 2006" RFC822      = "02 Jan 06 15:04 MST" RFC822Z     = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone RFC850      = "Monday, 02-Jan-06 15:04:05 MST" RFC1123     = "Mon, 02 Jan 2006 15:04:05 MST" RFC1123Z    = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone RFC3339     = "2006-01-02T15:04:05Z07:00" RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00" Kitchen     = "3:04PM" // Handy time stamps. Stamp      = "Jan _2 15:04:05" StampMilli = "Jan _2 15:04:05.000" StampMicro = "Jan _2 15:04:05.000000" StampNano  = "Jan _2 15:04:05.000000000"

Отсюда: https://golang.org/src/time/format.go

Как вы можете заметить, формата YYYY-MM-DD hh:mm:ss там нет. Окей, беда понятна, но что делать то?

На самом деле проблема решается достаточно несложно, достаточно вспомнить, как в Go работает json.Unmarshall. А работает он просто — у каждого поля структуры, в которую мы пытаемся смапить наш JSON, он ищет соответствие в нашем наборе байт и, если находит, смотрит на тип поля структуры и вызывает у этого типа функцию UnmasrhallJSON, в которую
передаёт найденное значение.

Соответственно, чтобы нам распарсить нестандартную дату, надо задать для поля кастомный
тип, который будет реализовывать интерфейс Unmarshaler, то бишь иметь собственную функцию UnmasrhallJSON:

type CustomDate struct {     time.Time }  func (c *CustomDate) UnmarshalJSON(b []byte) (err error) {     layout := "2006-01-02 15:04:05"      s := strings.Trim(string(b), "\"") // remove quotes     if s == "null" {         return     }     c.Time, err = time.Parse(layout, s)     return }  type Dated struct {     DateTime CustomDate }

Ну и чтоб два раза не вставать, давайте добавим кастомный маршалер, нам ведь рано или поздно придется грузить данные в ту API, откуда мы их взяли, а значит, надо их снова привести к их нестандартному формату. В итоге всё вместе будет выглядеть так:

package main  import (     "encoding/json"     "fmt"     "log"     "strings"     "time" )  type CustomDate struct {     time.Time }  const layout = "2006-01-02 15:04:05"  func (c *CustomDate) UnmarshalJSON(b []byte) (err error) {     s := strings.Trim(string(b), `"`) // remove quotes     if s == "null" {         return     }     c.Time, err = time.Parse(layout, s)     return }  func (c CustomDate) MarshalJSON() ([]byte, error) {     if c.Time.IsZero() {         return nil, nil     }     return []byte(fmt.Sprintf(`"%s"`, c.Time.Format(layout))), nil }  type Dated struct {     DateTime CustomDate }  func main() {     input := []byte("{\"datetime\": \"1900-01-01 12:00:04\"}")     var d Dated     err := json.Unmarshal(input, &d)     if err != nil {         log.Fatal(err)     }     log.Println("Unmarshal:")     log.Println(d.DateTime)      b, err := json.Marshal(d)     if err != nil {         log.Fatal(err)     }     log.Println("Marshal:")     log.Println(string(b)) }

Обратите внимание на сигнатуру функций Marshal/Unmarshal. Они разные — MarchalJSON принимает значение, а UnmarshalJSON — ссылку

Результат:

Unmarshal: 1900-01-01 12:00:04 +0000 UTC Marshal: {"DateTime":"1900-01-01T12:00:04Z"}

Вот, собственно, и всё.

Резюмируя

Парсинг нестарндартных дат в Go — дело нетривиальное, но несложное, достаточно написать собственный маршаллер/анмаршаллер, в зависимости от того, какую цель вы преследуете; также этот подход поможет справиться не только с датами — так можно успешно парсить любой нестандартный контент.

Если вы готовы предложить другие интересные способы решения этой проблемы — буду рад увидеть их в комментариях. Ну и в заключение хочу пожелать вам пореже сталкиваться с нестандартными форматами чего бы то ни было.

Всем спасибо.

ссылка на оригинал статьи https://habr.com/ru/post/492996/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *