Goの文字列と日付・時刻の扱い
文字列と日付は、あらゆるプログラムにおける2つの基本的なデータ型です。Goの標準ライブラリは、サードパーティの依存関係を必要とせずに、これら両方に対して包括的なツールセットを提供しています。
strings パッケージには 40 以上の関数が含まれており、strconv パッケージはすべての型変換を処理し、time パッケージは時間に関連するすべての操作を網羅しています。このレッスンでは、Go での文字列および時間の処理に必要な一連のツールをすべて習得します。
1. 学習内容
stringsパッケージの一般的な機能(切り取り、分割、置換、検索)- strconv による型変換(Atoi/Itoa/Parse)
- strings.Builder: 効率的な文字列連結
- time.Now / 解析 / フォーマット
- time.Duration による時間の計算
- time.Timer / time.Ticker タイマー
- 包括的な例:ログタイムスタンプパーサー
2. 運用エンジニアの実話
(1) 課題:タイムスタンプを手動で解析すると、ログのすべての行でエラーが発生してしまう
ボブは運用エンジニアです。彼はQPSを算出するために、サーバーのログからタイムスタンプを抽出する必要があります:
「1日あたり50万行のログがあり、タイムスタンプは
2026-07-08T10:00:00Zと07/08/2026 10:00:00 AMの2つの形式で記録されています。Pythonのdatetimeモジュールを使って20行のコードを書きましたが、同僚にはその内容が理解できませんでした。そこでGoに切り替えたのですが、結局、日付・時刻のフォーマット文字列を理解するだけでも、ドキュメントを10回も参照しなければなりませんでした。」
彼が最初に書いたバージョンは次のとおりです:
// 不良コード:時刻形式のハードコーディング,柔軟性に欠ける
func parseTimestamp(raw string) (time.Time, error) {
// 忘れる Go 時刻の形式は 2006-01-02 15:04:05,適当に書いたわけではない
return time.Parse("2006-01-02 15:04:05", raw)
}
同僚たちは、Goのタイムスタンプ形式(「2006-01-02 15:04:05」)にしばしば戸惑っています。
(2) Goソリューション:標準ライブラリの完全網羅
// log_parser.go
パッケージ main
import (
"fmt"
"strings"
"time"
)
// ログ行の削除 + タイムスタンプの抽出
func extractTimestamp(logLine 文字列) (time.Time, エラー) {
// 1. strings パッケージ:TrimSpace 空白を削除
line := strings.TrimSpace(logLine)
if line == "" {
return time.Time{}, fmt.Errorf("empty log line")
}
// 2. strings パッケージ:Split 最初のフィールドを取得する
parts := strings.Fields(line)
if len(parts) < 1 {
return time.Time{}, fmt.Errorf("no fields")
}
// 3. strconv / strings:時間フィールドの抽出
rawTime := strings.Trim(parts[0], "[]")
// 4. time パッケージ:解析時間
formats := []文字列{
time.RFC3339,
"01/02/2006 03:04:05 PM",
"2006-01-02 15:04:05",
}
for _, f := range formats {
if t, err := time.Parse(f, rawTime); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("unrecognized time format: %s", rawTime)
}
func main() {
lines := []文字列{
"2026-07-08T10:00:00Z [INFO] Server started",
"07/08/2026 10:05:00 AM [ERROR] Connection timeout",
}
for _, line := range lines {
t, err := extractTimestamp(line)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("Parsed: %s -> %s\n", line, t.Format(time.RFC822))
}
}
出力:
Parsed: 2026-07-08T10:00:00Z [INFO] Server started -> 08 Jul 26 10:00 UTC
Parsed: 07/08/2026 10:05:00 AM [ERROR] Connection timeout -> 08 Jul 26 10:05 UTC
(3) 利点:標準ライブラリの網羅性
| パッケージ | 主な機能 | サードパーティ製ライブラリの代替 |
|---|---|---|
| 文字列 | 40以上の文字列関数 | lodashやunderscoreは不要 |
| strconv | 型変換 | 手動での解析は不要 |
| strings.Builder | 効率的な連結 | bytes.Buffer |
| 時間 | 時間の解析・書式設定・タイマー | moment.js / date-fns |
Mon Jan 2 15:04:05 MST 2006(01/02/03/04/05/6はそれぞれ月/日/時/分/秒/年に相当)が使用されるため、2006-01-02 15:04:05 を覚えておけば大丈夫です。
3. strings パッケージの主要機能
▶ サンプル:12の一般的な文字列関数
package main
import (
"fmt"
"strings"
)
func main() {
s := " Hello, Go World! "
// トリミング
fmt.Printf("TrimSpace: [%s]\n", strings.TrimSpace(s))
fmt.Printf("Trim: [%s]\n", strings.Trim(s, " !"))
fmt.Printf("TrimPrefix: %s\n", strings.TrimPrefix(s, " Hello"))
// 分割と統合
parts := strings.Split("a,b,c", ",")
fmt.Printf("Split: %v\n", parts)
fmt.Printf("Join: %s\n", strings.Join(parts, "-"))
// 検索
fmt.Printf("Contains: %v\n", strings.Contains(s, "Go"))
fmt.Printf("Index: %d\n", strings.Index(s, "Go"))
fmt.Printf("Count: %d\n", strings.Count(s, "o"))
// 置換
fmt.Printf("Replace: %s\n", strings.Replace(s, "o", "0", 1))
fmt.Printf("ReplaceAll: %s\n", strings.ReplaceAll(s, "o", "0"))
// 大文字・小文字
fmt.Printf("ToUpper: %s\n", strings.ToUpper(s))
fmt.Printf("ToLower: %s\n", strings.ToLower(s))
// フィールドの分割(空白の自動処理)
fields := strings.Fields(" hello go world ")
fmt.Printf("Fields: %v\n", fields)
}
出力:
TrimSpace: [Hello, Go World!]
Trim: [Hello, Go World]
TrimPrefix: , Go World!
Split: [a b c]
Join: a-b-c
Contains: true
Index: 8
Count: 3
Replace: Hell0, Go World!
ReplaceAll: Hell0, G0 W0rld!
ToUpper: HELLO, GO WORLD!
ToLower: hello, go world!
Fields: [hello go world]
(1) strings 関数のクイックリファレンス
| カテゴリ | 機能 | 目的 |
|---|---|---|
| トリミング | TrimSpace / Trim / TrimPrefix / TrimSuffix |
空白文字または指定した文字を削除 |
| 分割 | Split / SplitN / Fields |
スライスに分割 |
| マージ | Join |
スライスを文字列に結合 |
| 検索 | Contains / Index / LastIndex / Count |
部分文字列チェック |
| 置換 | Replace / ReplaceAll |
部分文字列の置換 |
| ケース | ToUpper / ToLower / Title |
大文字・小文字の変換 |
| 構築 | Builder / Repeat |
効率的な文字列連結 / 文字列の繰り返し |
4. strconv による型変換
(1) 文字列 ↔ 数値
パッケージ main
import (
"fmt"
"strconv"
)
func main() {
// 文字列 → int
n, err := strconv.Atoi("42")
fmt.Printf("Atoi: %d, err=%v\n", n, err)
// int → 文字列
s := strconv.Itoa(42)
fmt.Printf("Itoa: %s\n", s)
// ParseInt(進法と桁数)
v, _ := strconv.ParseInt("FF", 16, 64)
fmt.Printf("ParseInt(hex): %d\n", v)
// ParseFloat
f, _ := strconv.ParseFloat("3.14", 64)
fmt.Printf("ParseFloat: %f\n", f)
// FormatInt / FormatFloat
fmt.Printf("FormatInt(hex): %s\n", strconv.FormatInt(255, 16))
}
▶ サンプル:strconv と型変換の違い
package main
import (
"fmt"
"strconv"
)
func main() {
// int → float64(型変換を使用する)
var age int = 28
fAge := float64(age)
fmt.Printf("型変換: %f\n", fAge)
// string → int(で strconv)
numStr := "42"
if num, err := strconv.Atoi(numStr); err == nil {
fmt.Printf("strconv: %d\n", num)
}
// int → string(で strconv)
s := strconv.Itoa(42)
fmt.Printf("Itoa: %s\n", s)
// 型変換を使って string 転 int(コンパイルエラー)
// n := int(numStr) ❌ コンパイルエラー
}
| シナリオ | 型変換の使用 | strconv の使用 |
|---|---|---|
| int ↔ float64 | ✅ float64(n) |
❌ |
| int ↔ 文字列 | ❌ | ✅ strconv.Itoa / Atoi |
| 文字列 ↔ float64 | ❌ | ✅ ParseFloat / FormatFloat |
| 基数解析 | ❌ | ✅ ParseInt("FF", 16, 64) |
| 整数 ↔ uint/int32 | ✅ | ❌ |
5. strings.Builder を使った効率的な文字列連結
(1) なぜビルダーを使うのか?
package main
import (
"fmt"
"strings"
)
func main() {
// 非効率的な方法:毎回 + すべて新しい文字列を作成する
s1 := ""
for i := 0; i < 1000; i++ {
s1 += "a" // O(n²) のパフォーマンス
}
// 効率的な方法:Builder 内部バッファ
var sb strings.Builder
for i := 0; i < 1000; i++ {
sb.WriteString("a") // O(n) のパフォーマンス
}
s2 := sb.String()
fmt.Printf("len=%d, equal=%v\n", len(s2), s1 == s2)
}
▶ サンプル:Builderの完全な使い方
パッケージ main
import (
"fmt"
"strings"
)
func buildCSV(data [][]文字列) 文字列 {
var sb strings.Builder
sb.Grow(1024) // メモリの事前割り当て
for i, row := range data {
rowStr := strings.Join(row, ",")
sb.WriteString(rowStr)
if i < len(data)-1 {
sb.WriteByte('\n') // 1バイトを書き込む
}
}
return sb.String()
}
func main() {
data := [][]文字列{
{"Name", "Age", "City"},
{"Alice", "28", "Shanghai"},
{"Bob", "32", "Beijing"},
}
csv := buildCSV(data)
fmt.Println(csv)
fmt.Printf("Length: %d\n", len(csv))
}
出力:
Name,Age,City
Alice,28,Shanghai
Bob,32,Beijing
Length: 52
+ を使用して文字列を連結すると、その都度新しい文字列が生成されるため、パフォーマンスは O(n²) になります。一方、strings.Builder は内部で可変バッファを保持しているため、パフォーマンスは O(n) になります。連結を多数行う場合、パフォーマンスの差は最大で 1,000 倍にも達することがあります。
6. time パッケージ:時刻の処理
(1) 時間。「今」と時間の表記形式
package main
import (
"fmt"
"time"
)
func main() {
// 現在の時刻
now := time.Now()
fmt.Printf("Now: %v\n", now)
// よく使われる書式設定
fmt.Printf("RFC3339: %s\n", now.Format(time.RFC3339))
fmt.Printf("RFC822: %s\n", now.Format(time.RFC822))
fmt.Printf("カスタマイズ: %s\n", now.Format("2006-01-02 15:04:05"))
fmt.Printf("日付: %s\n", now.Format("2006-01-02"))
fmt.Printf("時間: %s\n", now.Format("15:04:05"))
}
▶ サンプル:時間分析
パッケージ main
import (
"fmt"
"time"
)
func main() {
// 標準フォーマットの解析
t1, _ := time.Parse(time.RFC3339, "2026-07-08T10:00:00Z")
fmt.Printf("RFC3339: %v\n", t1)
// カスタム形式の解析(2006-01-02 15:04:05 = 参考時間)
t2, _ := time.Parse("2006-01-02 15:04:05", "2026-07-08 10:30:00")
fmt.Printf("Custom: %v\n", t2)
// タイムゾーンを含む解析
t3, _ := time.Parse("2006-01-02T15:04:05-07:00", "2026-07-08T10:00:00+08:00")
fmt.Printf("With TZ: %v\n", t3)
// 月と日付の英語表記の解析
t4, _ := time.Parse("January 2, 2006", "July 8, 2026")
fmt.Printf("English: %v\n", t4)
}
出力:
RFC3339: 2026-07-08 10:00:00 +0000 UTC
Custom: 2026-07-08 10:30:00 +0000 UTC
With TZ: 2026-07-08 10:00:00 +0800 CST
English: 2026-07-08 00:00:00 +0000 UTC
(2) time.Duration による時間の計算
package main
import (
"fmt"
"time"
)
func main() {
// Duration を作成
d1 := 5 * time.Second
d2 := 100 * time.Millisecond
d3 := 2*time.Hour + 30*time.Minute
fmt.Printf("5s = %d ns\n", d1.Nanoseconds())
fmt.Printf("100ms = %v\n", d2)
fmt.Printf("2h30m = %v\n", d3)
// 時間演算
now := time.Now()
later := now.Add(2 * time.Hour)
duration := later.Sub(now)
fmt.Printf("差額: %v\n", duration)
// 比較
fmt.Printf("5s > 100ms? %v\n", d1 > d2)
fmt.Printf("d1.String(): %s\n", d1)
}
(3) 期間単位の一覧表
| 定数 | 意味 |
|---|---|
time.Nanosecond |
1 ns |
time.Microsecond |
1 µs = 1000 ns |
time.Millisecond |
1 ms = 1000 µs |
time.Second |
1 s = 1000 ms |
time.Minute |
60秒 |
time.Hour |
60分 |
7. タイマー/ティッカー
(1) time.Timer: 1回限りのタイマー
パッケージ main
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(2 * time.Second)
fmt.Println("待つ 2 秒...")
<-timer.C // タイムアウトになるまでブロックする
fmt.Println("時間切れ!")
// 以下のように使うこともできます time.After(より簡潔に,ただし、対応していません Stop)
fmt.Println("もう少し待つ 1 秒...")
<-time.After(1 * time.Second)
fmt.Println("完了!")
}
▶ サンプル:time.Ticker:周期的なタイマー
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(1 * time.Second)
done := make(chan bool)
go func() {
time.Sleep(5 * time.Second)
done <- true
}()
for count := 1; ; count++ {
select {
case t := <-ticker.C:
fmt.Printf("Tick %d at %s\n", count, t.Format("15:04:05"))
case <-done:
ticker.Stop() // ticker を停止
fmt.Println("Done!")
return
}
}
}
出力:
Tick 1 at 10:00:01
Tick 2 at 10:00:02
Tick 3 at 10:00:03
Tick 4 at 10:00:04
Tick 5 at 10:00:05
Done!
(2) タイマーとティッカー
| 特集 | タイマー | ティッカー |
|---|---|---|
| トリガー型 | 1回限り | 定期 |
| 停止 | timer.Stop() |
ticker.Stop() |
| 信号チャンネル | .C |
.C |
| リセット | timer.Reset(d) |
❌ |
| 一般的なシナリオ | タイムアウト制御/実行遅延 | ハートビート/スケジュールされたタスク |
8. タイムゾーンの取り扱い
package main
import (
"fmt"
"time"
)
func main() {
// タイムゾーンを読み込み中
loc, _ := time.LoadLocation("America/New_York")
// 指定されたタイムゾーンでの解析
t := time.Date(2026, 7, 8, 10, 0, 0, 0, loc)
fmt.Printf("ニューヨーク時間: %s\n", t.Format(time.RFC3339))
// 他のタイムゾーンに変換する
shanghai := t.In(time.FixedZone("CST", 8*3600))
fmt.Printf("上海時間: %s\n", shanghai.Format(time.RFC3339))
// UTC
utc := t.UTC()
fmt.Printf("UTC時間: %s\n", utc.Format(time.RFC3339))
}
| 方法 | 説明 |
|---|---|
time.LoadLocation("Asia/Shanghai") |
IANA タイムゾーン名 |
time.FixedZone("CST", 8*3600) |
オフセットを修正 |
t.In(loc) |
対象のタイムゾーンに変換 |
t.UTC() |
UTCに変換 |
t.Local() |
現地タイムゾーンに変換 |
9. 完全な例:ログタイムスタンプパーサー
// log_analyzer.go
package main
import (
"fmt"
"sort"
"strings"
"time"
)
// LogEntry ログエントリ
type LogEntry struct {
Timestamp time.Time
Level string // INFO / ERROR / WARN
Message string
}
// LogParser ログ解析ツール
type LogParser struct {
timeFormats []string
}
func NewLogParser() *LogParser {
return &LogParser{
timeFormats: []string{
time.RFC3339,
"2006-01-02 15:04:05",
"01/02/2006 03:04:05 PM",
"2006/01/02 15:04:05",
"Jan 2 15:04:05",
},
}
}
// Parse 1行のログの解析
func (p *LogParser) Parse(line string) (LogEntry, error) {
line = strings.TrimSpace(line)
if line == "" {
return LogEntry{}, fmt.Errorf("empty line")
}
parts := strings.Fields(line)
if len(parts) < 3 {
return LogEntry{}, fmt.Errorf("too few fields")
}
// タイムスタンプの解析を試みる(複数の形式に対応)
var ts time.Time
var tsLen int
for i := 0; i < len(parts); i++ {
candidate := strings.Trim(parts[i], "[]")
for _, fmt := range p.timeFormats {
if t, err := time.Parse(fmt, candidate); err == nil {
ts = t
tsLen = i + 1
break
}
}
if !ts.IsZero() {
break
}
}
if ts.IsZero() {
return LogEntry{}, fmt.Errorf("no timestamp found in: %s", line)
}
remaining := parts[tsLen:]
if len(remaining) < 2 {
return LogEntry{}, fmt.Errorf("no level/message after timestamp")
}
level := strings.Trim(remaining[0], "[]")
message := strings.Join(remaining[1:], " ")
return LogEntry{
Timestamp: ts,
Level: level,
Message: message,
}, nil
}
// Analyze 分析ログの集計
func (p *LogParser) Analyze(lines []string) {
var entries []LogEntry
var errors int
// で strings.Builder エラー報告の作成
var errBuf strings.Builder
errBuf.Grow(1024)
for i, line := range lines {
entry, err := p.Parse(line)
if err != nil {
errors++
errBuf.WriteString(fmt.Sprintf(" 了解 %d: %v\n", i+1, err))
continue
}
entries = append(entries, entry)
}
// 時間順に並べ替える
sort.Slice(entries, func(i, j int) bool {
return entries[i].Timestamp.Before(entries[j].Timestamp)
})
// 統計
var infoCount, errorCount, warnCount int
for _, e := range entries {
switch e.Level {
case "INFO":
infoCount++
case "ERROR":
errorCount++
case "WARN":
warnCount++
}
}
// レポートを出力する
var report strings.Builder
report.Grow(2048)
report.WriteString("=== ログ分析レポート ===\n")
report.WriteString(fmt.Sprintf("総行数: %d\n", len(lines)))
report.WriteString(fmt.Sprintf("解析に成功しました: %d\n", len(entries)))
report.WriteString(fmt.Sprintf("解析に失敗しました: %d\n", errors))
report.WriteString(fmt.Sprintf("INFO: %d, ERROR: %d, WARN: %d\n",
infoCount, errorCount, warnCount))
report.WriteString(fmt.Sprintf("期間: %s ~ %s\n",
entries[0].Timestamp.Format(time.RFC3339),
entries[len(entries)-1].Timestamp.Format(time.RFC3339)))
if errors > 0 {
report.WriteString("\n=== エラーの詳細 ===\n")
report.WriteString(errBuf.String())
}
fmt.Print(report.String())
}
func main() {
logLines := []string{
"2026-07-08T10:00:00Z [INFO] Server started",
"2026-07-08T10:01:15Z [ERROR] Connection timeout to db",
"2026-07-08T10:02:30Z [WARN] Memory usage 85%",
"2026-07-08T10:03:45Z [INFO] Request processed in 120ms",
"invalid line without timestamp",
"2026-07-08T10:05:00Z [ERROR] Disk space low",
}
parser := NewLogParser()
parser.Analyze(logLines)
}
期待される出力:
=== ログ分析レポート ===
総行数: 6
解析に成功しました: 5
解析に失敗しました: 1
INFO: 2, ERROR: 2, WARN: 1
期間: 2026-07-08T10:00:00Z ~ 2026-07-08T10:05:00Z
=== エラーの詳細 ===
了解 5: no timestamp found in: invalid line without timestamp
graph TB
now[time.Now] --> format[Format]
now --> sub[Sub/Duration]
now --> add[Add]
parse[time.Parse] --> t[time.Time]
t --> format
t --> sub
t --> add
sub --> duration[time.Duration]
duration --> hours[Hours/Minutes/Seconds]
timer[time.Timer] --> C[.C channel]
ticker[time.Ticker] --> C
style now fill:#e1f5fe
style t fill:#e1f5fe
style duration fill:#f3e5f5
Mon Jan 2 15:04:05 MST 2006 の数値パターンに基づいています。2006 = 年、01 = 月、02 = 日、15 = 時間(24時間制)、03 = 時間(12時間制)、04 = 分、05 = 秒。この設計は独特ですが、慣れれば自然に感じられるようになります。
❓ よくある質問
strings パッケージでよく使われる関数にはどのようなものがありますか?strconvと型変換の違いは何ですか?intからfloat64)に使用されますが、strconvは文字列と数値間の変換に使用されます。文字列とint、あるいはfloatの間では型キャストを使用できず、strconvを使用する必要があります。time.Parse のフォーマット文字列はどのように記述すればよいですか?2006-01-02 15:04:05 (月/日/時/分/秒/年の順)を覚えておいてください。2006 = 年、01 = 月、02 = 日、15 = 24時間制、03 = 12時間制、04 = 分、05 = 秒。Duration はどのように使用しますか?time.Duration は、ナノ秒単位の数値を表す int64 値です。作成: 5 * time.Second; 演算: t.Add(d), t.Sub(t2); 取得: d.Hours(), d.Minutes(), d.Seconds()..Cチャネルを介してシグナルを受信し、実行を停止するための.Stop()メソッドをサポートしています。datetime.timedeltaに似たものはありますか?time.Durationがまさにそれです。作成:d := 2*time.Hour + 30*time.Minute;加算と減算:t.Add(d)、t.Add(-d);2つの時刻の差:t2.Sub(t1)。time.LoadLocation("Asia/Shanghai") を使用してタイムゾーンを読み込み、t.In(loc) で変換してください。時刻は内部で UTC として保存されており、出力時に目的のタイムゾーンに変換されます。strings.Builderは+に比べてどれくらい高速ですか?+は毎回新しい文字列を作成するため => O(n²)、一方、Builderの内部バッファを使用するため => O(n)となります。10,000回の連結を行う場合、Builderの方が1,000倍以上高速です。📖 まとめ
stringsパッケージには、あらゆる文字列操作を網羅する 40 以上の関数が含まれています- strconv は、文字列 ↔ 数値/ブール値間の変換を処理します(型キャストでは不可能な変換です)。
strings.Builderは効率的な文字列連結を実現し、+演算子の O(n²) という計算量を回避しますtime.Nowは現在の時刻を取得し、Format/Parseはそれをフォーマットまたは解析します- Goの日付形式は、基準時刻
2006-01-02 15:04:05に基づいています。 time.Durationはナノ秒単位の数値であり、算術演算や比較演算に対応しています。- 単発のタイミングには
Timer、周期的なタイミングにはTicker - タイムゾーンの処理:
LoadLocationおよびIn()メソッド
📝 練習問題
-
基本問題(難易度 ⭐):
stringsパッケージを使用して、文字列内の各単語の出現回数を数えるwordCount(s 文字列) map[文字列]int関数を実装してください。Fields、ループ、およびマップを使用する必要があります。 -
上級問題(難易度 ⭐⭐):人間が読みやすい説明(「3分前」/「2時間前」/「昨日」/「3日前」)を返す
timeAgo(t time.Time) 文字列関数を実装してください。time.Since()とDurationの条件分岐を組み合わせて使用する必要があります。 -
課題(難易度 ⭐⭐⭐):ログ集計ツールを実装してください。文字列入力(1行につきタイムスタンプ1つ+レベル+メッセージ)が与えられた場合、
strings.Split、time.Parse、strings.Builder、およびsort.Sliceを使用して、以下の処理を行ってください: (1) ERRORレベルのログを解析して除外する;(2) それらを時間順に並べ替える;(3)Builderを使用して、集計統計情報を含むレポートを生成する。このツールは、少なくとも3つの時刻形式に対応していなければならない。



