エラーの内容
time.Parse() を呼び出したところ、次のようなエラーが発生しました:
parsing time "2023/10/27" as "2006-01-02": cannot parse "/10/27" as "-"
あるいは、こんなケースも:
parsing time "27-10-2023 14:30:00" as "2006-01-02 15:04:05": cannot parse "27-10-2023" as "2006"
タイムスタンプ自体は正しく、コードも一見問題なさそうに見えます。それでも Go はパースを拒否します。
根本原因
Go の時刻フォーマットは他の言語とは仕組みが異なります。YYYY-MM-DD でも %d/%m/%Y でもありません。Go は特定のリファレンスタイムスタンプをフォーマットテンプレートとして使用します:
Mon Jan 2 15:04:05 MST 2006
各コンポーネントは固定のリファレンス値に対応しています:
- 年:
2006 - 月:
01(またはJan) - 日:
02 - 時:
15(24時間)または03(12時間) - 分:
04 - 秒:
05 - タイムゾーン:
MSTまたは-0700
つまり、入力が "2023/10/27" なのにレイアウトが "2006-01-02" だと、Go はスラッシュとダッシュを照合しようとして即座に失敗します。
// これは失敗する — 区切り文字が一致していない
t, err := time.Parse("2006-01-02", "2023/10/27")
// parsing time "2023/10/27" as "2006-01-02": cannot parse "/10/27" as "-"
修正方法:レイアウトを入力に合わせる
レイアウト内のすべての区切り文字は、入力文字列のものと完全に一致させる必要があります。
入力がスラッシュ区切りの場合:YYYY/MM/DD
t, err := time.Parse("2006/01/02", "2023/10/27")
if err != nil {
log.Fatalf("parse error: %v", err)
}
fmt.Println(t) // 2023-10-27 00:00:00 +0000 UTC
入力がドット区切りの場合:DD.MM.YYYY
t, err := time.Parse("02.01.2006", "27.10.2023")
// 注意:日・月・年の順に並んでいる — 実際のデータの順序に合わせること
入力がヨーロッパ形式の場合:DD-MM-YYYY
t, err := time.Parse("02-01-2006", "27-10-2023")
入力に時刻が含まれる場合:YYYY/MM/DD HH:MM:SS
t, err := time.Parse("2006/01/02 15:04:05", "2023/10/27 14:30:00")
よく使うレイアウトのクイックリファレンス
// 標準 ISO 8601
time.Parse("2006-01-02", "2023-10-27")
// スラッシュ区切り
time.Parse("2006/01/02", "2023/10/27")
// US形式 MM/DD/YYYY
time.Parse("01/02/2006", "10/27/2023")
// 時刻付き
time.Parse("2006-01-02 15:04:05", "2023-10-27 14:30:00")
// RFC3339(タイムゾーン付き ISO 8601)— 定数を使う
time.Parse(time.RFC3339, "2023-10-27T14:30:00Z")
// RFC1123 — HTTP日付
time.Parse(time.RFC1123, "Fri, 27 Oct 2023 14:30:00 UTC")
// Unixタイムスタンプ — Parseは使わない
t := time.Unix(1698416200, 0)
不明な入力フォーマットのデバッグ
外部 API からデータを受け取っていて、どんな形式で送られてくるかわからない場合は、処理前に生の文字列を出力して確認しましょう:
rawDate := response["created_at"].(string)
fmt.Printf("raw date: %q\n", rawDate)
// raw date: "2023/10/27"
// これで正確にどんなレイアウトを書けばよいかがわかる
次に、文字を1つずつ Go のリファレンス値にマッピングします:
Input: 2023/10/27
Layout: 2006/01/02
^^^^ ^^ ^^
年 月 日
複数の入力フォーマットへの対応
複数のソースからのデータが同じフォーマットを使うとは限りません。最もシンプルな方法は、成功するまで各レイアウトを順番に試すことです。
func parseDate(s string) (time.Time, error) {
layouts := []string{
"2006-01-02",
"2006/01/02",
"01/02/2006",
"02-01-2006",
"2006-01-02T15:04:05Z07:00",
time.RFC3339,
}
for _, layout := range layouts {
if t, err := time.Parse(layout, s); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("unrecognized date format: %q", s)
}
タイムゾーンの落とし穴
入力にタイムゾーンオフセットが含まれているのにレイアウトがそれを考慮していない場合、別のエラーが発生します。レイアウトにタイムゾーントークンを含めるか、time.ParseInLocation を使用してください:
// 入力にオフセットがある場合:"2023/10/27 14:30:00 +0900"
t, err := time.Parse("2006/01/02 15:04:05 -0700", "2023/10/27 14:30:00 +0900")
// または、タイムゾーンがわかっていて入力に含まれていない場合:
loc, _ := time.LoadLocation("Asia/Tokyo")
t, err := time.ParseInLocation("2006/01/02", "2023/10/27", loc)
動作確認
レイアウトを修正したら、簡単なサニティチェックを行いましょう。最も手軽な方法はラウンドトリップです。文字列をパースし、再びフォーマットして比較します:
t, err := time.Parse("2006/01/02", "2023/10/27")
if err != nil {
log.Fatalf("still failing: %v", err)
}
// フィールドを確認
fmt.Println(t.Year()) // 2023
fmt.Println(t.Month()) // October
fmt.Println(t.Day()) // 27
// ラウンドトリップ:元の文字列に再フォーマット
fmt.Println(t.Format("2006/01/02")) // 2023/10/27
出力が元の入力と完全に一致すれば、レイアウトは正しいです。
予防策
- 日付レイアウトはパッケージレベルの名前付き定数として宣言しましょう。インラインに散在した文字列はすぐにメンテナンスの問題になります。
- 新しいレイアウトを作ったら、まずラウンドトリップテストを行いましょう。既知の日付をパースして再フォーマットし、一致することを確認します。
- 自分で管理する API では、RFC3339(
time.RFC3339)に統一し、JSON マーシャリングでtime.Timeを使いましょう。Go がシリアライズを自動的に処理してくれます。 - 新しい
time.Parse呼び出しにはユニットテストを書きましょう。「エラーなし」チェックだけでなく、実際の入力・出力のペアを使ってテストしてください。
// グッドプラクティス:名前付き定数
const (
DateLayout = "2006/01/02"
DateTimeLayout = "2006/01/02 15:04:05"
)
// ユニットテスト
func TestParseDate(t *testing.T) {
got, err := time.Parse(DateLayout, "2023/10/27")
if err != nil {
t.Fatal(err)
}
if got.Day() != 27 || got.Month() != time.October || got.Year() != 2023 {
t.Errorf("unexpected date: %v", got)
}
}

