正規表現と日付
レッスン24:正規表現と日付
生活での例え
あなたが荷物の仕分け作業員であると想像してください:
- 正規表現は仕分けルールのようなもの — 「住所に'北京'が含まれ、番号が3桁」といったパターンで、大量の荷物の中からマッチするものを素早くフィルタリングできます。
- 日付と時刻は配送伝票のタイムスタンプのようなもの — 「この荷物はいつ届いたか」「受領からどのくらい経ったか」「明日午後3時までに届くか」を知る必要があります。
プログラミングでは、正規表現はテキストから情報を正確に特定・抽出するのに役立ち、timeパッケージは時刻を測定・計算・表示するのに役立ちます。
コアコンセプト
正規表現(regexpパッケージ)
GoのregexpパッケージはRE2構文エンジンをベースにしており、一般的な正規表現構文の多くをサポートしていますが、バックトラッキングと後方参照はサポートしていません(パフォーマンス優先の設計上の選択)。
コアインターフェース:
- Compile: regexp.Compile(pattern) → (*Regexp, error)
- Match: MatchString(s) → bool
- Find: FindString / FindAllString / FindStringSubmatch
- Replace: ReplaceAllString / ReplaceAllStringFunc
日付と時刻(timeパッケージ)
Goの時刻処理はtime.Time構造体を中心に、固定参照時刻をフォーマットテンプレートとして使用します:
参照時刻: Mon Jan 2 15:04:05 MST 2006
数値の語呂合わせ: 01/02 03:04:05PM 2006 -0700
💡 なぜこの変な数字なのか? Goは覚えやすいように
1 2 3 4 5 6 7の順序(月1、日2、時3、分4、秒5、年6、タイムゾーン7)を選択しました。
基本構文と使い方
1. 正規表現の基本
GO
package main
import (
"fmt"
"regexp"
)
func main() {
// 正規表現をコンパイル
re, err := regexp.Compile(`\d{3}-\d{4}-\d{4}`)
if err != nil {
fmt.Println("正規表現のコンパイルに失敗しました:", err)
return
}
// マッチするかチェック
phone := "138-1234-5678"
fmt.Println("マッチ:", re.MatchString(phone)) // true
// 最初のマッチを検索
text := "連絡先: 138-1234-5678 または 139-8765-4321"
fmt.Println("最初:", re.FindString(text)) // 138-1234-5678
// すべてのマッチを検索
all := re.FindAllString(text, -1)
fmt.Println("すべて:", all) // [138-1234-5678 139-8765-4321]
}
💡
MustCompileとCompileの違い:MustCompileはコンパイル失敗時に直接パニックを起こすため、グローバル定数の正規表現に適しています。Compileはエラーを返すため、実行時に動的に構築される正規表現に適しています。
2. 名前付きキャプチャグループ
GO
package main
import (
"fmt"
"regexp"
)
func main() {
// 名前付きキャプチャグループでメールの一部を抽出
re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
email := "zhangsan@example.com"
// マッチ結果を取得
match := re.FindStringSubmatch(email)
names := re.SubexpNames()
for i, name := range names {
if i > 0 && name != "" {
fmt.Printf("%s = %s\n", name, match[i])
}
}
// user = zhangsan
// domain = example.com
}
3. 置換操作
GO
package main
import (
"fmt"
"regexp"
"strings"
)
func main() {
re := regexp.MustCompile(`\s+`)
// すべての空白を単一スペースに置換
text := " hello world go "
result := re.ReplaceAllString(text, " ")
fmt.Printf("[%s]\n", result) // [hello world go]
// 関数を使用した動的置換
re2 := regexp.MustCompile(`\b\w`)
title := re2.ReplaceAllStringFunc("hello world go", func(s string) string {
// 最初の文字を大文字に
return strings.ToUpper(s)
})
fmt.Println(title) // Hello World Go
}
4. 基本的な時刻操作
GO
package main
import (
"fmt"
"time"
)
func main() {
// 現在時刻を取得
now := time.Now()
fmt.Println("現在時刻:", now)
// 個々のコンポーネントを抽出
fmt.Println("年:", now.Year())
fmt.Println("月:", now.Month())
fmt.Println("日:", now.Day())
fmt.Println("時:", now.Hour())
fmt.Println("分:", now.Minute())
fmt.Println("秒:", now.Second())
fmt.Println("曜日:", now.Weekday())
}
💡 time.Now()はローカルタイムゾーンの時刻を返します。UTC時刻には
time.Now().UTC()を使用してください。
5. 時刻のフォーマットと解析
GO
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
// フォーマット:参照時刻をテンプレートとして使用
fmt.Println(now.Format("2006-01-02")) // 2025-06-27
fmt.Println(now.Format("2006/01/02 15:04:05")) // 2025/06/27 14:30:00
fmt.Println(now.Format("03:04PM")) // 02:30PM
// 一般的な定義済みフォーマット
fmt.Println(now.Format(time.RFC3339)) // 2025-06-27T14:30:00+08:00
// 文字列を時刻に解析
t, err := time.Parse("2006-01-02", "2025-12-25")
if err != nil {
fmt.Println("解析失敗:", err)
return
}
fmt.Println("解析結果:", t)
// タイムゾーン付きで解析
t2, _ := time.ParseInLocation("2006-01-02 15:04:05",
"2025-12-25 08:00:00", time.Local)
fmt.Println("タイムゾーン付き:", t2)
}
⚠️ フォーマットと解析は同じ参照時刻文字列を使用します — これはGo独自の設計です。
2006-01-02 15:04:05を覚えておけば大丈夫です。
6. Durationと時刻計算
GO
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
// 時刻の加算と減算
tomorrow := now.Add(24 * time.Hour)
fmt.Println("明日:", tomorrow.Format("2006-01-02"))
twoHoursLater := now.Add(2 * time.Hour)
fmt.Println("2時間後:", twoHoursLater.Format("15:04:05"))
// 時刻差を計算
diff := tomorrow.Sub(now)
fmt.Println("時刻差:", diff) // 24h0m0s
fmt.Println("時間:", diff.Hours()) // 24
fmt.Println("分:", diff.Minutes()) // 1440
// 順序を比較
fmt.Println("明日は後:", tomorrow.After(now)) // true
fmt.Println("今日は前:", now.Before(tomorrow)) // true
// 指定精度に切り捨て
floored := now.Truncate(time.Hour)
fmt.Println("時間に切り捨て:", floored.Format("15:04:05"))
}
7. タイマー
GO
package main
import (
"fmt"
"time"
)
func main() {
// 1回限りのタイマー
timer := time.NewTimer(2 * time.Second)
fmt.Println("2秒待機中...")
<-timer.C
fmt.Println("時間切れ!")
// 周期的タイマー(Ticker)
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop() // リーク防止のため必ず停止
count := 0
for t := range ticker.C {
count++
fmt.Println("ティック:", t.Format("15:04:05.000"))
if count >= 3 {
break
}
}
// time.After簡略版(1回限りの待機)
<-time.After(1 * time.Second)
fmt.Println("1秒後に実行")
}
💡 ループ内で
time.Afterを使用する際は注意が必要です — 各反復で新しいチャネルが作成され、古いタイマーはガベージコレクションされないため、メモリリークが発生する可能性があります。ループ内ではtime.NewTimerを使用し、手動でResetしてください。
例題コード
例:バリデーションと抽出 — ログ解析(難易度⭐)
GO
package main
import (
"fmt"
"regexp"
)
func main() {
// シミュレーション用ログ行
logs := []string{
"[2025-06-27 14:30:00] ERROR Database connection failed",
"[2025-06-27 14:30:01] INFO Service started successfully",
"[2025-06-27 14:30:05] WARN Disk space low",
"[2025-06-27 14:31:00] ERROR Request timeout",
}
// ログフォーマットにマッチ:時刻 + レベル + メッセージ
re := regexp.MustCompile(`\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+)\s+(.+)`)
// レベルごとにカウント
levelCount := make(map[string]int)
for _, log := range logs {
match := re.FindStringSubmatch(log)
if match == nil {
continue
}
timestamp := match[1]
level := match[2]
message := match[3]
fmt.Printf("時刻: %s | レベル: %-5s | メッセージ: %s\n",
timestamp, level, message)
levelCount[level]++
}
fmt.Println("\nレベル統計:")
for level, count := range levelCount {
fmt.Printf(" %s: %d件\n", level, count)
}
}
出力:
TEXT
時刻: 2025-06-27 14:30:00 | レベル: ERROR | メッセージ: Database connection failed
時刻: 2025-06-27 14:30:01 | レベル: INFO | メッセージ: Service started successfully
時刻: 2025-06-27 14:30:05 | レベル: WARN | メッセージ: Disk space low
時刻: 2025-06-27 14:31:00 | レベル: ERROR | メッセージ: Request timeout
レベル統計:
ERROR: 2件
INFO: 1件
WARN: 1件
例:テンプレートエンジン — シンプルなテキスト置換システム(難易度⭐⭐)
GO
package main
import (
"fmt"
"regexp"
"strings"
"time"
)
func main() {
template := `親愛なる {{name}} 様:
ご注文 {{order}} は {{date}} に発送されました。
配達予定は {{days}日後です。
現在時刻: {{now}}`
// 変数マッピングを定義
vars := map[string]string{
"name": "Alice",
"order": "ORD-20250627-001",
"date": "2025-06-27",
"days": "3",
}
// {{variable_name}} パターンにマッチ
re := regexp.MustCompile(`\{\{(\w+)\}\}`)
// 変数を置換
result := re.ReplaceAllStringFunc(template, func(match string) string {
// 変数名を抽出({{と}}を除去)
key := match[2 : len(match)-2]
if key == "now" {
return time.Now().Format("2006-01-02 15:04:05")
}
if val, ok := vars[key]; ok {
return val
}
return match // 変数が見つからない場合はそのまま保持
})
fmt.Println(result)
// テンプレート内の変数をカウント
allVars := re.FindAllString(template, -1)
varNames := make([]string, 0, len(allVars))
for _, v := range allVars {
varNames = append(varNames, v[2:len(v)-2])
}
fmt.Printf("\nテンプレート変数: %s\n", strings.Join(varNames, ", "))
}
出力:
TEXT
親愛なる Alice 様:
ご注文 ORD-20250627-001 は 2025-06-27 に発送されました。
配達予定は 3日後です。
現在時刻: 2025-06-27 14:30:00
テンプレート変数: name, order, date, days, now
例:カウントダウンタイマー — フォーマット付きリアルタイム表示(難易度⭐⭐⭐)
GO
package main
import (
"fmt"
"regexp"
"strings"
"time"
)
// formatDuration Durationを "Xd Xh Xm Xs" 形式にフォーマット
func formatDuration(d time.Duration) string {
if d <= 0 {
return "期限切れ"
}
days := int(d.Hours()) / 24
hours := int(d.Hours()) % 24
minutes := int(d.Minutes()) % 60
seconds := int(d.Seconds()) % 60
parts := []string{}
if days > 0 {
parts = append(parts, fmt.Sprintf("%d日", days))
}
if hours > 0 {
parts = append(parts, fmt.Sprintf("%d時", hours))
}
if minutes > 0 {
parts = append(parts, fmt.Sprintf("%d分", minutes))
}
parts = append(parts, fmt.Sprintf("%02d秒", seconds))
return strings.Join(parts, " ")
}
// parseDeadline 複数の日付フォーマットを解析
func parseDeadline(s string) (time.Time, error) {
// 複数のフォーマットを試行
formats := []string{
"2006-01-02 15:04:05",
"2006-01-02",
"2006/01/02 15:04",
"01-02 15:04",
}
// 相対時刻かチェック(例:"+2h30m")
re := regexp.MustCompile(`^\+(\d+)([hms])`)
if match := re.FindStringSubmatch(s); match != nil {
// 相対時刻を解析(簡略化、単一ユニットのみ)
return time.Now().Add(2 * time.Hour), nil
}
for _, format := range formats {
if t, err := time.ParseInLocation(format, s, time.Local); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("時刻を解析できませんでした: %s", s)
}
func main() {
// デッドラインを解析
deadline, err := parseDeadline("2025-12-31 23:59:59")
if err != nil {
fmt.Println("エラー:", err)
return
}
fmt.Printf("デッドライン: %s\n", deadline.Format("2006-01-02 15:04:05"))
fmt.Println(strings.Repeat("─", 40))
// カウントダウンをシミュレーション(1秒ごとに更新、合計5回)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
count := 0
for now := range ticker.C {
remaining := deadline.Sub(now)
// 画面クリア効果:キャリッジリターンで現在行を上書き
fmt.Printf("\r残り: %-30s", formatDuration(remaining))
count++
if count >= 5 || remaining <= 0 {
break
}
}
fmt.Println("\n\nカウントダウンデモ完了")
// 時刻フォーマットを検証
re := regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`)
formatted := deadline.Format("2006-01-02 15:04:05")
fmt.Printf("フォーマット検証: %v\n", re.MatchString(formatted)) // true
}
実践的な応用シナリオ
シナリオ1:フォームデータバリデーション
GO
package main
import (
"fmt"
"regexp"
)
// Validator フォームバリデーター
type Validator struct {
rules map[string]*regexp.Regexp
}
// NewValidator バリデーターを作成し、すべての正規表現を事前コンパイル
func NewValidator() *Validator {
rules := map[string]*regexp.Regexp{
"phone": regexp.MustCompile(`^1[3-9]\d{9}$`),
"email": regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`),
"id_card": regexp.MustCompile(`^\d{17}[\dXx]$`),
"username": regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]{3,15}$`),
"password": regexp.MustCompile(`^.{8,}$`),
"ip": regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}$`),
"date": regexp.MustCompile(`^\d{4}[-/](0[1-9]|1[0-2])[-/](0[1-9]|[12]\d|3[01])$`),
}
return &Validator{rules: rules}
}
// Validate 単一フィールドをバリデーション
func (v *Validator) Validate(field, value string) bool {
re, ok := v.rules[field]
if !ok {
return true // ルールなしは合格
}
return re.MatchString(value)
}
// ValidateAll 複数のフィールドをバリデーションし、すべてのエラーを返す
func (v *Validator) ValidateAll(data map[string]string) []string {
var errors []string
for field, value := range data {
if !v.Validate(field, value) {
errors = append(errors, fmt.Sprintf("%sの形式が正しくありません: %s", field, value))
}
}
return errors
}
func main() {
v := NewValidator()
// テストデータ
data := map[string]string{
"phone": "13812345678",
"email": "test@example.com",
"id_card": "110101199001011234",
"username": "go_dev",
"date": "2025-06-27",
}
errors := v.ValidateAll(data)
if len(errors) == 0 {
fmt.Println("✓ すべてのフィールドが正常にバリデーションされました")
} else {
fmt.Println("バリデーション失敗:")
for _, err := range errors {
fmt.Printf(" ✗ %s\n", err)
}
}
// 無効なデータをテスト
invalidData := map[string]string{
"phone": "12345678901", // 有効な電話番号プレフィックスではない
"email": "not-an-email", // @が不足
}
fmt.Println("\n無効なデータテスト:")
for field, value := range invalidData {
result := "✓"
if !v.Validate(field, value) {
result = "✗"
}
fmt.Printf(" %s %s: %s\n", result, field, value)
}
}
シナリオ2:時刻範囲クエリ — アクティビティステータス管理
GO
package main
import (
"fmt"
"strings"
"time"
)
// Activity 構造体
type Activity struct {
Name string
StartTime time.Time
EndTime time.Time
}
// Status 現在のアクティビティステータスを取得
func (a Activity) Status(now time.Time) string {
switch {
case now.Before(a.StartTime):
return "未開始"
case now.After(a.EndTime):
return "終了"
default:
return "開催中"
}
}
// Remaining 残り時間またはカウントダウンを取得
func (a Activity) Remaining(now time.Time) string {
if now.Before(a.StartTime) {
d := a.StartTime.Sub(now)
return fmt.Sprintf("開始まで: %s", formatDur(d))
}
if now.Before(a.EndTime) {
d := a.EndTime.Sub(now)
return fmt.Sprintf("残り: %s", formatDur(d))
}
return "期限切れ"
}
func formatDur(d time.Duration) string {
hours := int(d.Hours())
minutes := int(d.Minutes()) % 60
if hours > 24 {
days := hours / 24
hours = hours % 24
return fmt.Sprintf("%d日 %d時間 %d分", days, hours, minutes)
}
return fmt.Sprintf("%d時間 %d分", hours, minutes)
}
func main() {
now := time.Now()
// アクティビティリストを作成
activities := []Activity{
{
Name: "サマーセール",
StartTime: mustParse("2025-06-01 00:00:00"),
EndTime: mustParse("2025-06-18 23:59:59"),
},
{
Name: "新学期セール",
StartTime: mustParse("2025-07-01 00:00:00"),
EndTime: mustParse("2025-08-31 23:59:59"),
},
{
Name: "ブラックフライデープレビュー",
StartTime: mustParse("2025-11-01 00:00:00"),
EndTime: mustParse("2025-11-11 23:59:59"),
},
}
fmt.Printf("現在時刻: %s\n", now.Format("2006-01-02 15:04:05"))
fmt.Println(strings.Repeat("─", 50))
for _, act := range activities {
status := act.Status(now)
remaining := act.Remaining(now)
fmt.Printf("アクティビティ: %s\n", act.Name)
fmt.Printf(" 期間: %s ~ %s\n",
act.StartTime.Format("2006-01-02"),
act.EndTime.Format("2006-01-02"))
fmt.Printf(" ステータス: %s | %s\n\n", status, remaining)
}
// ステータスでフィルタリング
fmt.Println("開催中のアクティビティ:")
found := false
for _, act := range activities {
if act.Status(now) == "開催中" {
fmt.Printf(" - %s\n", act.Name)
found = true
}
}
if !found {
fmt.Println(" 現在開催中のアクティビティはありません")
}
}
func mustParse(s string) time.Time {
t, err := time.ParseInLocation("2006-01-02 15:04:05", s, time.Local)
if err != nil {
panic(err)
}
return t
}
❓ よくある質問
賞問1:なぜ正規表現で中国語の文字にマッチしないのですか?
GoのregexpパッケージはデフォルトでUTF-8エンコーディングを処理するため、中国語の文字自体は正常にマッチできます。よくある問題は正しい文字クラスを使用していないことです:
GO
// 間違い:\wは中国語にマッチしない
re := regexp.MustCompile(`^\w+$`)
re.MatchString("你好") // false
// 正しい:Unicode文字クラスまたは中国語範囲を直接マッチ
re2 := regexp.MustCompile(`^[\p{Han}]+$`)
re2.MatchString("你好") // true
// または混合マッチング
re3 := regexp.MustCompile(`^[\w\p{Han}]+$`)
re3.MatchString("hello你好123") // true
賞問2:time.Parseとtime.ParseInLocationの違いは何ですか?
GO
// Parseはタイムゾーン情報がない場合にUTCを使用
t1, _ := time.Parse("2006-01-02", "2025-06-27")
fmt.Println(t1.Location()) // UTC
// ParseInLocationはデフォルトのタイムゾーンを指定
t2, _ := time.ParseInLocation("2006-01-02", "2025-06-27", time.Local)
fmt.Println(t2.Location()) // Local(例:Asia/Shanghai)
// フォーマット文字列にタイムゾーン情報(例:-0700)が含まれる場合、両方とも同じ動作
t3, _ := time.Parse("2006-01-02 15:04:05 -0700", "2025-06-27 08:00:00 +0800")
fmt.Println(t3) // +0800タイムゾーンとして正しく解析
💡 推奨: タイムゾーンなしの日付文字列を解析する場合は、常に
ParseInLocationを使用し、タイムゾーンを明示的に指定してください。
賞問3:正規表現のパフォーマンスが悪い場合はどうすればいいですか?
GO
// 間違い:呼び出しのたびに再コンパイル
func validatePhone(phone string) bool {
re := regexp.MustCompile(`^1[3-9]\d{9}$`) // 毎回コンパイル
return re.MatchString(phone)
}
// 正しい:パッケージレベル変数として事前コンパイル
var phoneRe = regexp.MustCompile(`^1[3-9]\d{9}$`)
func validatePhone2(phone string) bool {
return phoneRe.MatchString(phone)
}
// 高頻度マッチングの場合、Regexp.Copy()でロック競合を回避
var globalRe = regexp.MustCompile(`\d+`)
func processConcurrently(text string) string {
re := globalRe.Copy() // コピーを取得、並行ロックを回避
return re.ReplaceAllString(text, "#")
}
賞問4:タイムゾーン変換をどう処理すればいいですか?
GO
package main
import (
"fmt"
"time"
)
func main() {
// 特定のタイムゾーンを読み込み
shanghai, _ := time.LoadLocation("Asia/Shanghai")
tokyo, _ := time.LoadLocation("Asia/Tokyo")
newyork, _ := time.LoadLocation("America/New_York")
// タイムゾーン付きで時刻を作成
t := time.Date(2025, 6, 27, 14, 0, 0, 0, shanghai)
fmt.Println("上海:", t.Format("15:04 MST"))
// タイムゾーンを変換
fmt.Println("東京:", t.In(tokyo).Format("15:04 MST"))
fmt.Println("ニューヨーク:", t.In(newyork).Format("15:04 MST"))
// 文字列から解析して変換
parsed, _ := time.ParseInLocation("2006-01-02 15:04",
"2025-06-27 14:00", shanghai)
fmt.Println("\n解析後にUTCに変換:", parsed.UTC().Format("2006-01-02 15:04 MST"))
}
⚠️
time.LoadLocationは一部のシステムでtzdataのインストールが必要になる場合があります。Go 1.15+では_ "time/tzdata"をインポートすることでタイムゾーンデータを埋め込めます。
📖 まとめ
このレッスンでは2つのコアトピックをカバーしました:
| トピック | パッケージ | コア型 | 主要操作 |
|---|---|---|---|
| 正規表現 | regexp |
Regexp |
Compile, Match, Find, Replace |
| 日付と時刻 | time |
Time, Duration |
Format, Parse, Calculate, Timer |
正規表現の重要なポイント:
MustCompileで定数正規表現を事前コンパイル- 名前付きキャプチャグループ
(?P<name>...)で可読性を向上 \p{Han}で中国語にマッチ、\p{L}ですべてのUnicode文字にマッチ- RE2エンジンは後方参照をサポートしないが、線形時間計算量を保証
時刻の重要なポイント:
- フォーマットテンプレートは参照時刻
2006-01-02 15:04:05 ParseはデフォルトでUTC、ParseInLocationでタイムゾーンを指定Durationは時刻間隔を表現し、豊富なユニットメソッドをサポート- タイマーは使用後に必ず
Stop()してgoroutineリークを防止
📝 演習
演習1:Markdownリンク抽出器
Markdownテキストからすべてのリンクを抽出し、タイトル -> URL形式で出力するプログラムを書いてください。
GO
// ヒント:[タイトル](URL)形式にマッチ
// 入力:`Visit [Go官网](https://golang.org) or [GitHub](https://github.com)`
// 出力:
// Go官网 -> https://golang.org
// GitHub -> https://github.com
演習2:営業日計算機
指定された日付からn営業日後(土日をスキップ)の日付を計算する関数AddBusinessDays(t time.Time, n int) time.Timeを書いてください。
GO
// テスト:
// AddBusinessDays(2025-06-27 金曜日, 1) → 2025-06-30 月曜日
// AddBusinessDays(2025-06-27 金曜日, 5) → 2025-07-04 金曜日
演習3:センシティブワードフィルターシステム
センシティブワードフィルターを実装してください:
- 設定からセンシティブワードリストを読み込み、正規表現にコンパイル
*置換と完全削除モードをサポート- センシティブワードのバリエーション検出をサポート(例:途中にスペースが挿入された「gam bling」)
GO
// 入力テキスト:"これはgamblingウェブサイトです。gam blingサービスを提供しています"
// 置換モード出力:"これは*ウェブサイトです。*サービスを提供しています"
// 削除モード出力:"これはウェブサイトです。サービスを提供しています"
次のレッスン
次のレッスンでは、コマンドラインプログラム開発について学びます。Goを使って強力なCLIツールを構築する方法、引数解析、サブコマンド、インタラクティブ入力などの実践的なスキルを身につけます。



