Go: Goの文字列と日付・時刻の扱い:`strings`、`strconv`、`time` パッケージの完全ガイド

最終更新:2026-08-26

文字列と日付は、あらゆるプログラムにおける2つの基本的なデータ型です。Goの標準ライブラリは、サードパーティの依存関係を必要とせずに、これら両方に対して包括的なツールセットを提供しています。

strings パッケージには 40 以上の関数が含まれており、strconv パッケージはすべての型変換を処理し、time パッケージは時間に関連するすべての操作を網羅しています。このレッスンでは、Go における文字列と時間を扱うためのツール一式を習得します。

1. 学習内容



2. 運用エンジニアの実話

(1) 課題:タイムスタンプを手動で解析すると、ログのすべての行でエラーが発生してしまう

ボブは運用エンジニアです。彼はQPSを算出するために、サーバーのログからタイムスタンプを抽出する必要があります:

「1日あたり50万行のログがあり、タイムスタンプは2026-07-08T10:00:00Z07/08/2026 10:00:00 AMの2つの形式で記録されています。Pythonのdatetimeモジュールを使って20行のコードを書きましたが、同僚にはその内容が理解できませんでした。そこでGoに切り替えたのですが、結局、日付・時刻のフォーマット文字列を理解するだけでも、ドキュメントを10回も参照しなければなりませんでした。」

彼が最初に書いたバージョンは次のとおりです:

GO
// Bad code: hardcoded time format, inflexible
func parseTimestamp(raw string) (time.Time, error) {
    // Remember: Go's time format is 2006-01-02 15:04:05, not arbitrary
    return time.Parse("2006-01-02 15:04:05", raw)
}

同僚たちは、Goのタイムスタンプ形式(「2006-01-02 15:04:05」)に戸惑うことがよくあります。

(2) Goソリューション:標準ライブラリの完全網羅

GO
// log_parser.go
package main

import (
    "fmt"
    "strings"
    "time"
)

// Clean log line + extract timestamp
func extractTimestamp(logLine string) (time.Time, error) {
    // 1. strings package: TrimSpace removes whitespace
    line := strings.TrimSpace(logLine)
    if line == "" {
        return time.Time{}, fmt.Errorf("empty log line")
    }

    // 2. strings package: Split to get the first field
    parts := strings.Fields(line)
    if len(parts) < 1 {
        return time.Time{}, fmt.Errorf("no fields")
    }

    // 3. strconv / strings: extract the time field
    rawTime := strings.Trim(parts[0], "[]")

    // 4. time package: parse time
    formats := []string{
        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 := []string{
        "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))
    }
}

出力:

TEXT 📖 参照専用
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
💡 ヒント: Go では「参照時間」の形式 Mon Jan 2 15:04:05 MST 2006(01/02/03/04/05/6 はそれぞれ月/日/時/分/秒/年に相当)が使用されるため、2006-01-02 15:04:05 を覚えておけば大丈夫です。



3. strings パッケージの主要機能

▶ サンプル:12の一般的な文字列関数

GO
package main

import (
    "fmt"
    "strings"
)

func main() {
    s := "  Hello, Go World!  "

    // Trimming
    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"))

    // Split and Join
    parts := strings.Split("a,b,c", ",")
    fmt.Printf("Split: %v\n", parts)
    fmt.Printf("Join: %s\n", strings.Join(parts, "-"))

    // Search
    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"))

    // Replace
    fmt.Printf("Replace: %s\n", strings.Replace(s, "o", "0", 1))
    fmt.Printf("ReplaceAll: %s\n", strings.ReplaceAll(s, "o", "0"))

    // Case conversion
    fmt.Printf("ToUpper: %s\n", strings.ToUpper(s))
    fmt.Printf("ToLower: %s\n", strings.ToLower(s))

    // Field splitting (automatically handles whitespace)
    fields := strings.Fields("  hello   go  world  ")
    fmt.Printf("Fields: %v\n", fields)
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
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]

(2) strings 関数のクイックリファレンス

カテゴリ 機能 目的
トリミング TrimSpace / Trim / TrimPrefix / TrimSuffix 空白文字または指定した文字を削除
分割 Split / SplitN / Fields スライスに分割
参加 Join スライスを1つの文字列に結合
検索 Contains / Index / LastIndex / Count 部分文字列チェック
置換 Replace / ReplaceAll 部分文字列の置換
ケース ToUpper / ToLower / Title 大文字・小文字の変換
構築 Builder / Repeat 効率的な文字列連結 / 文字列の繰り返し


4. strconv による型変換

(1) 文字列 ↔ 数値

GO
package main

import (
    "fmt"
    "strconv"
)

func main() {
    // string → int
    n, err := strconv.Atoi("42")
    fmt.Printf("Atoi: %d, err=%v\n", n, err)

    // int → string
    s := strconv.Itoa(42)
    fmt.Printf("Itoa: %s\n", s)

    // ParseInt (with base and bit size)
    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 と型変換の違い

GO
package main

import (
    "fmt"
    "strconv"
)

func main() {
    // int → float64 (using type conversion)
    var age int = 28
    fAge := float64(age)
    fmt.Printf("Type conversion: %f\n", fAge)

    // string → int (using strconv)
    numStr := "42"
    if num, err := strconv.Atoi(numStr); err == nil {
        fmt.Printf("strconv: %d\n", num)
    }

    // int → string (using strconv)
    s := strconv.Itoa(42)
    fmt.Printf("Itoa: %s\n", s)

    // Cannot use type conversion to convert string to int (compilation error)
    // n := int(numStr)  ❌ compilation error
}
▶ 試してみよう
シナリオ 型変換の使用 strconv の使用
int ↔ float64 float64(n)
int ↔ string strconv.Itoa / Atoi
文字列 ↔ float64 ParseFloat / FormatFloat
基数解析 ParseInt("FF", 16, 64)
整数 ↔ uint/int32


5. strings.Builder を使った効率的な文字列連結

(1) なぜビルダーを使うのか?

GO
package main

import (
    "fmt"
    "strings"
)

func main() {
    // Inefficient: each + creates a new string
    s1 := ""
    for i := 0; i < 1000; i++ {
        s1 += "a"  // O(n²) performance
    }

    // Efficient: Builder's internal buffer
    var sb strings.Builder
    for i := 0; i < 1000; i++ {
        sb.WriteString("a")  // O(n) performance
    }
    s2 := sb.String()

    fmt.Printf("len=%d, equal=%v\n", len(s2), s1 == s2)
}

▶ サンプル:ビルダーの完全な使い方

GO
package main

import (
    "fmt"
    "strings"
)

func buildCSV(data [][]string) string {
    var sb strings.Builder
    sb.Grow(1024)  // pre-allocate memory

    for i, row := range data {
        rowStr := strings.Join(row, ",")
        sb.WriteString(rowStr)
        if i < len(data)-1 {
            sb.WriteByte('\n')  // write a single byte
        }
    }

    return sb.String()
}

func main() {
    data := [][]string{
        {"Name", "Age", "City"},
        {"Alice", "28", "Shanghai"},
        {"Bob", "32", "Beijing"},
    }

    csv := buildCSV(data)
    fmt.Println(csv)
    fmt.Printf("Length: %d\n", len(csv))
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
Name,Age,City
Alice,28,Shanghai
Bob,32,Beijing

Length: 52
🔥 よくある間違い: + を使用して文字列を連結すると、その都度新しい文字列が生成されるため、パフォーマンスは O(n²) になります。一方、strings.Builder は内部で変更可能なバッファを保持しているため、パフォーマンスは O(n) になります。多数の連結処理を行う場合、パフォーマンスの差は最大で 1,000 倍にも達することがあります。



6. time パッケージ:時刻の処理

(1) 時間。「今」と時間の表記形式

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    // Current time
    now := time.Now()
    fmt.Printf("Now: %v\n", now)

    // Common formatting
    fmt.Printf("RFC3339: %s\n", now.Format(time.RFC3339))
    fmt.Printf("RFC822: %s\n", now.Format(time.RFC822))
    fmt.Printf("Custom: %s\n", now.Format("2006-01-02 15:04:05"))
    fmt.Printf("Date: %s\n", now.Format("2006-01-02"))
    fmt.Printf("Time: %s\n", now.Format("15:04:05"))
}

▶ サンプル:時刻の解析

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    // Parse standard format
    t1, _ := time.Parse(time.RFC3339, "2026-07-08T10:00:00Z")
    fmt.Printf("RFC3339: %v\n", t1)

    // Parse custom format (2006-01-02 15:04:05 = reference time)
    t2, _ := time.Parse("2006-01-02 15:04:05", "2026-07-08 10:30:00")
    fmt.Printf("Custom: %v\n", t2)

    // Parse with time zone
    t3, _ := time.Parse("2006-01-02T15:04:05-07:00", "2026-07-08T10:00:00+08:00")
    fmt.Printf("With TZ: %v\n", t3)

    // Parse English month and day format
    t4, _ := time.Parse("January 2, 2006", "July 8, 2026")
    fmt.Printf("English: %v\n", t4)
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
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

(3) time.Duration による時間の計算

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    // Create 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)

    // Time arithmetic
    now := time.Now()
    later := now.Add(2 * time.Hour)
    duration := later.Sub(now)
    fmt.Printf("Difference: %v\n", duration)

    // Comparison
    fmt.Printf("5s > 100ms? %v\n", d1 > d2)
    fmt.Printf("d1.String(): %s\n", d1)
}

(4) 期間単位の一覧表

定数 意味
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回限りのタイマー

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    timer := time.NewTimer(2 * time.Second)

    fmt.Println("Waiting 2 seconds...")
    <-timer.C  // blocks until timeout
    fmt.Println("Time's up!")

    // Alternatively, use time.After (more concise, but does not support Stop)
    fmt.Println("Waiting another 1 second...")
    <-time.After(1 * time.Second)
    fmt.Println("Done!")
}

▶ サンプル:time.Ticker:周期的なタイマー

GO
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()  // stop the ticker
            fmt.Println("Done!")
            return
        }
    }
}
▶ 試してみよう

出力:

TEXT 📖 参照専用
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!

(3) タイマーとティッカーの比較

特集 タイマー ティッカー
トリガー型 1回限り 定期
停止 timer.Stop() ticker.Stop()
信号チャンネル .C .C
リセット timer.Reset(d)
一般的なシナリオ タイムアウト制御/実行遅延 ハートビート/スケジュールされたタスク


8. タイムゾーンの取り扱い

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    // Load time zone
    loc, _ := time.LoadLocation("America/New_York")

    // Parse in specified time zone
    t := time.Date(2026, 7, 8, 10, 0, 0, 0, loc)
    fmt.Printf("New York time: %s\n", t.Format(time.RFC3339))

    // Convert to other time zone
    shanghai := t.In(time.FixedZone("CST", 8*3600))
    fmt.Printf("Shanghai time: %s\n", shanghai.Format(time.RFC3339))

    // UTC
    utc := t.UTC()
    fmt.Printf("UTC time: %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. 完全な例:ログタイムスタンプパーサー

GO
// log_analyzer.go
package main

import (
    "fmt"
    "sort"
    "strings"
    "time"
)

// LogEntry represents a log entry
type LogEntry struct {
    Timestamp time.Time
    Level     string  // INFO / ERROR / WARN
    Message   string
}

// LogParser parses log lines
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 parses a single log line
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")
    }

    // Try to parse timestamp (supports multiple formats)
    var ts time.Time
    var tsLen int

    for i := 0; i < len(parts); i++ {
        candidate := strings.Trim(parts[i], "[]")
        for _, format := range p.timeFormats {
            if t, err := time.Parse(format, 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 analyzes a collection of log lines
func (p *LogParser) Analyze(lines []string) {
    var entries []LogEntry
    var errors int

    // Use strings.Builder to construct the error report
    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(" Line %d: %v\n", i+1, err))
            continue
        }
        entries = append(entries, entry)
    }

    // Sort by time
    sort.Slice(entries, func(i, j int) bool {
        return entries[i].Timestamp.Before(entries[j].Timestamp)
    })

    // Count
    var infoCount, errorCount, warnCount int
    for _, e := range entries {
        switch e.Level {
        case "INFO":
            infoCount++
        case "ERROR":
            errorCount++
        case "WARN":
            warnCount++
        }
    }

    // Output report
    var report strings.Builder
    report.Grow(2048)

    report.WriteString("=== Log Analysis Report ===\n")
    report.WriteString(fmt.Sprintf("Total lines: %d\n", len(lines)))
    report.WriteString(fmt.Sprintf("Parsed successfully: %d\n", len(entries)))
    report.WriteString(fmt.Sprintf("Parse failures: %d\n", errors))
    report.WriteString(fmt.Sprintf("INFO: %d, ERROR: %d, WARN: %d\n",
        infoCount, errorCount, warnCount))
    report.WriteString(fmt.Sprintf("Time range: %s ~ %s\n",
        entries[0].Timestamp.Format(time.RFC3339),
        entries[len(entries)-1].Timestamp.Format(time.RFC3339)))

    if errors > 0 {
        report.WriteString("\n=== Error Details ===\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)
}

期待される出力:

TEXT 📖 参照専用
=== Log Analysis Report ===
Total lines: 6
Parsed successfully: 5
Parse failures: 1
INFO: 2, ERROR: 2, WARN: 1
Time range: 2026-07-08T10:00:00Z ~ 2026-07-08T10:05:00Z

=== Error Details ===
 Line 5: no timestamp found in: invalid line without timestamp
100%
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
🔥 よくある間違い: Goの日付と時刻の形式は、参照時刻 Mon Jan 2 15:04:05 MST 2006 の数値パターンに基づいています。2006 = 年、01 = 月、02 = 日、15 = 時間(24時間制)、03 = 時間(12時間制)、04 = 分、05 = 秒。この設計は独特ですが、慣れれば自然に感じられるようになります。


❓ よくある質問

Q strings パッケージでよく使われる関数にはどのようなものがありますか?
A トリミング(Trim/TrimSpace)、分割(Split/Fields)、結合(Join)、検索(Contains/Index/Count)、置換(Replace/ReplaceAll)、大文字小文字の変換(ToUpper/ToLower)、および生成(Builder/Repeat)――これら15の関数で、一般的なニーズの90%をカバーできます。
Q strconvと型変換の違いは何ですか?
A 型変換は同じデータ型内での変換(例:intからfloat64)に使用されますが、strconvは文字列と数値間の変換に使用されます。stringint、あるいはfloatの間では型キャストを使用できず、strconvを使用する必要があります。
Q time.Parse のフォーマット文字列はどのように記述すればよいですか?
A 基準時刻 2006-01-02 15:04:05 (月/日/時/分/秒/年の順)を覚えておいてください。2006 = 年、01 = 月、02 = 日、15 = 24時間制、03 = 12時間制、04 = 分、05 = 秒。
Q Duration はどのように使用しますか?
A time.Duration は、ナノ秒数を表す int64 の値です。作成:5 * time.Second;操作:t.Add(d)t.Sub(t2);取得:d.Hours()d.Minutes()d.Seconds()
Q タイマーとティッカーの違いは何ですか?
A タイマーは1回だけ実行される(遅延実行/タイムアウト制御)のに対し、ティッカーは繰り返し実行されます(スケジュールされたタスク/ハートビート)。どちらも.Cチャネルを介してシグナルを受信し、実行を停止するための.Stop()メソッドをサポートしています。
Q Goには、Pythonのdatetime.timedeltaに似たものはありますか?
A はい、time.Durationがまさにそれです。作成:d := 2*time.Hour + 30*time.Minute;加算と減算:t.Add(d)t.Add(-d);2つの時刻の差:t2.Sub(t1)
Q 異なるタイムゾーンの時刻をどのように処理すればよいですか?
A time.LoadLocation("Asia/Shanghai") を使用してタイムゾーンを読み込み、t.In(loc) で変換してください。時刻は内部で UTC として保存されており、出力時に目的のタイムゾーンに変換されます。
Q strings.Builder+に比べてどれくらい高速ですか?
A +は毎回新しい文字列を作成するため => O(n²)、一方、Builderの内部バッファを使用するため => O(n)となります。10,000回の連結を行う場合、Builderの方が1,000倍以上高速です。

📖 まとめ


📝 練習問題

  1. 基本問題(難易度 ⭐)wordCount(s string) map[string]int パッケージを使用して、文字列内の各単語の出現回数を数える strings 関数を実装してください。Fields、ループ、および map を使用する必要があります。

  2. 上級問題(難易度 ⭐⭐):人間が読みやすい説明(「3分前」/「2時間前」/「昨日」/「3日前」)を返す timeAgo(t time.Time) string 関数を実装してください。timeAgo(t time.Time) stringDuration の条件分岐を組み合わせて使用する必要があります。

  3. チャレンジ問題(難易度 ⭐⭐⭐)ログ集計ツールを実装してください。文字列入力(1行につき1つのタイムスタンプ+レベル+メッセージ)が与えられた場合、strings.Splittime.Parsestrings.Builder、およびsort.Sliceを使用して、以下の処理を行ってください: (1) ERRORレベルのログを解析して除外する;(2) それらを時間順に並べ替える;(3) Builder を使用して、集計統計情報を含むレポートを生成する。このツールは、少なくとも3つの時刻形式に対応していなければならない。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%