🗺️

[Gin] リクエストとレスポンスにlocationを指定する

2022/12/05に公開

Ginを使用したREST APIでDBではUTC時刻を扱っているけど、
リクエスト、レスポンスはlocationの値でやり取りしたい時にやったことをまとめました。

環境

  • golang: v1.19.3
  • gin-gonic/gin: v1.8.1

リクエスト

クエリパラメータ

クエリパラメータをバインドした際のtime.Time型のデフォルトlocationはLocalになっている。
特定のlocationを指定したい場合は、リクエスト構造体タグにtime_location:"{timezone}"を追加します。
クエリ文字列からtime.Timeに変換する際に指定のlocationに設定した値が生成されます。

下記は"Asia/Tokyo"の日時を受け取り、レスポンスはUTCの値と一緒に返却する例です。

package main

import (
	"fmt"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
)

type MappingTime struct {
	JSTTime time.Time `form:"jst_time" json:"jst_time" time_format:"2006-01-02 15:04" time_location:"Asia/Tokyo"`
}

func main() {
	route := gin.Default()
	route.GET("/mappingtimes", getMappingTime)
	route.Run(":8080")
}

func getMappingTime(c *gin.Context) {
	var m MappingTime

	if err := c.ShouldBindWith(&m, binding.Query); err == nil {
		// JST to UTC
		utcFromJstTime := m.JSTTime.UTC()
		c.JSON(http.StatusOK, gin.H{
			"jst_time": m.JSTTime.Format(time.RFC3339),
			"utc_time": utcFromJstTime.Format(time.RFC3339),
		})
	} else {
		c.JSON(http.StatusBadRequest, gin.H{"err": err.Error()})
	}
}

リクエスト結果を見てみると、JST(+09:00)の時刻で受け取れています。

$curl "localhost:8080/mappingtimes?jst_time=2022-01-01%2010%3A00"
{"jst_time":"2022-01-01T10:00:00+09:00","utc_from_jst_time":"2022-01-01T01:00:00Z"}

リクエストボディ

上記のstructタグ指定はフォームのバインドのみ処理されます。
JSONでPOSTするリクエストボディの場合、Unmarshal(JSONからstructに変換)する際にlocationを指定する必要があリます。

UnmarshalするときはUnmarshalJSON()を実行するので、UnmarshalJSON()でlocationを指定するカスタム関数を定義したtime.Time型を実装します。

下記はJSTで受け取り
Unmarshal時にJSTで受け取り、MarshalにもJSTで返却する場合の例です。

type JSTTime struct {
	*time.Time
}

const (
	layout = "2006-01-02 15:04"
)

var (
	tz *time.Location
)

func init() {
	tz = time.FixedZone("Asia/Tokyo", 9*60*60)
}

// Marshal
func (jt JSTTime) MarshalJSON() ([]byte, error) {
	// 出力時のLocationを指定
	t2 := jt.Time.In(tz)
	return json.Marshal(t2.Format(layout))
}

// Unmarshal
func (jt *JSTTime) UnmarshalJSON(d []byte) error {

	// JSTのlocation付きtime.Timeに変換
	t, err := time.ParseInLocation(fmt.Sprintf(`"%s"`, layout), string(d), tz)
	if err != nil {
		return err
	}
	*jt = JSTTime{&t}
	return err
}

レスポンスボディ

time.Timeのlocationを変更するにはtime.Time.Inに対象のlocationを指定します。

Marshal(structからJSON)に変換したレスポンスボディを使用する場合は、
前述のリクエストボディに記載していますがMarshalJSON()内でlocationを指定する必要があります。

内部で使用する時刻

DBのクエリなどに指定する場合など、入出力以外の時刻はtime.UTC()を使用して明示的にUTCの時刻を扱うようにしました。

参考

Discussion