Go: Goの文字列と日付・時刻の扱い:`strings`、`strconv`、`time` パッケージの完全ガイド
最終更新:2026-08-26
文字列と日付は、あらゆるプログラムにおける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回も参照しなければなりませんでした。」
彼が最初に書いたバージョンは次のとおりです:
// 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ソリューション:標準ライブラリの完全網羅
// 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))
}
}
出力:
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! "
// 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)
}
出力:
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) 文字列 ↔ 数値
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 と型変換の違い
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) なぜビルダーを使うのか?
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)
}
▶ サンプル:ビルダーの完全な使い方
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))
}
出力:
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() {
// 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"))
}
▶ サンプル:時刻の解析
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)
}
出力:
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 による時間の計算
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回限りのタイマー
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:周期的なタイマー
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
}
}
}
出力:
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. タイムゾーンの取り扱い
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. 完全な例:ログタイムスタンプパーサー
// 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)
}
期待される出力:
=== 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
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は文字列と数値間の変換に使用されます。stringと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()方式
📝 練習問題
-
基本問題(難易度 ⭐):
wordCount(s string) map[string]intパッケージを使用して、文字列内の各単語の出現回数を数えるstrings関数を実装してください。Fields、ループ、および map を使用する必要があります。 -
上級問題(難易度 ⭐⭐):人間が読みやすい説明(「3分前」/「2時間前」/「昨日」/「3日前」)を返す
timeAgo(t time.Time) string関数を実装してください。timeAgo(t time.Time) stringとDurationの条件分岐を組み合わせて使用する必要があります。 -
チャレンジ問題(難易度 ⭐⭐⭐):ログ集計ツールを実装してください。文字列入力(1行につき1つのタイムスタンプ+レベル+メッセージ)が与えられた場合、
strings.Split、time.Parse、strings.Builder、およびsort.Sliceを使用して、以下の処理を行ってください: (1) ERRORレベルのログを解析して除外する;(2) それらを時間順に並べ替える;(3)Builderを使用して、集計統計情報を含むレポートを生成する。このツールは、少なくとも3つの時刻形式に対応していなければならない。