課題13: 並行ダウンローダーの実装
課題概要
この課題では、複数のURLから並行してファイルをダウンロードする実用的なツールを実装します。goroutine、sync.WaitGroup、Mutexを使った並行処理の基礎を習得し、レースコンディションの回避方法を学びます。
マンダトリー要件
要件1: ダウンロード統計の実装(20点)
並行安全な統計情報を管理する構造体を実装してください。
ファイル: stats.go
package downloader
import (
"fmt"
"sync"
"time"
)
// DownloadStats はダウンロード統計を管理
type DownloadStats struct {
mu sync.Mutex
TotalFiles int
SuccessCount int
FailureCount int
TotalBytes int64
StartTime time.Time
CompletedURLs []string
FailedURLs []string
}
// NewDownloadStats は新しい統計を作成
func NewDownloadStats() *DownloadStats {
return &DownloadStats{
StartTime: time.Now(),
CompletedURLs: make([]string, 0),
FailedURLs: make([]string, 0),
}
}
// IncrementTotal は総ファイル数を増加
func (s *DownloadStats) IncrementTotal() {
s.mu.Lock()
defer s.mu.Unlock()
s.TotalFiles++
}
// RecordSuccess は成功を記録
func (s *DownloadStats) RecordSuccess(url string, bytes int64) {
s.mu.Lock()
defer s.mu.Unlock()
s.SuccessCount++
s.TotalBytes += bytes
s.CompletedURLs = append(s.CompletedURLs, url)
}
// RecordFailure は失敗を記録
func (s *DownloadStats) RecordFailure(url string) {
s.mu.Lock()
defer s.mu.Unlock()
s.FailureCount++
s.FailedURLs = append(s.FailedURLs, url)
}
// GetStats は統計のスナップショットを取得
func (s *DownloadStats) GetStats() (int, int, int, int64, time.Duration) {
s.mu.Lock()
defer s.mu.Unlock()
elapsed := time.Since(s.StartTime)
return s.TotalFiles, s.SuccessCount, s.FailureCount, s.TotalBytes, elapsed
}
// Report はレポートを生成
func (s *DownloadStats) Report() string {
s.mu.Lock()
defer s.mu.Unlock()
elapsed := time.Since(s.StartTime)
report := "\n=== ダウンロードレポート ===\n"
report += fmt.Sprintf("総ファイル数: %d\n", s.TotalFiles)
report += fmt.Sprintf("成功: %d\n", s.SuccessCount)
report += fmt.Sprintf("失敗: %d\n", s.FailureCount)
report += fmt.Sprintf("総ダウンロードサイズ: %d bytes (%.2f MB)\n",
s.TotalBytes, float64(s.TotalBytes)/(1024*1024))
report += fmt.Sprintf("所要時間: %v\n", elapsed)
if s.SuccessCount > 0 {
avgSpeed := float64(s.TotalBytes) / elapsed.Seconds()
report += fmt.Sprintf("平均速度: %.2f KB/s\n", avgSpeed/1024)
}
if len(s.FailedURLs) > 0 {
report += "\n失敗したURL:\n"
for _, url := range s.FailedURLs {
report += fmt.Sprintf(" - %s\n", url)
}
}
return report
}
実装すべき内容:
IncrementTotal: 総ファイル数を増加RecordSuccess: 成功を記録(URL、バイト数)RecordFailure: 失敗を記録GetStats: 統計情報を取得Report: レポートを生成- すべてのメソッドで
sync.Mutexを使った同期
要件2: ダウンローダーの実装(30点)
並行ダウンロードを実行するワーカーを実装してください。
ファイル: downloader.go
package downloader
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sync"
"time"
)
// DownloadTask はダウンロードタスク
type DownloadTask struct {
URL string
FileName string
}
// DownloadResult はダウンロード結果
type DownloadResult struct {
Task DownloadTask
Success bool
ByteSize int64
Error error
Duration time.Duration
}
// Downloader は並行ダウンローダー
type Downloader struct {
OutputDir string
MaxWorkers int
Timeout time.Duration
Stats *DownloadStats
}
// NewDownloader は新しいダウンローダーを作成
func NewDownloader(outputDir string, maxWorkers int, timeout time.Duration) *Downloader {
return &Downloader{
OutputDir: outputDir,
MaxWorkers: maxWorkers,
Timeout: timeout,
Stats: NewDownloadStats(),
}
}
// DownloadFile は単一ファイルをダウンロード
func (d *Downloader) DownloadFile(task DownloadTask) DownloadResult {
start := time.Now()
result := DownloadResult{
Task: task,
Success: false,
}
// HTTPクライアントの作成
client := &http.Client{
Timeout: d.Timeout,
}
// HTTPリクエスト
resp, err := client.Get(task.URL)
if err != nil {
result.Error = fmt.Errorf("HTTPリクエスト失敗: %w", err)
result.Duration = time.Since(start)
return result
}
defer resp.Body.Close()
// ステータスコードチェック
if resp.StatusCode != http.StatusOK {
result.Error = fmt.Errorf("HTTPステータスエラー: %d", resp.StatusCode)
result.Duration = time.Since(start)
return result
}
// 出力ファイルの作成
outputPath := filepath.Join(d.OutputDir, task.FileName)
outFile, err := os.Create(outputPath)
if err != nil {
result.Error = fmt.Errorf("ファイル作成失敗: %w", err)
result.Duration = time.Since(start)
return result
}
defer outFile.Close()
// ダウンロード
bytesWritten, err := io.Copy(outFile, resp.Body)
if err != nil {
result.Error = fmt.Errorf("ファイル書き込み失敗: %w", err)
result.Duration = time.Since(start)
return result
}
result.Success = true
result.ByteSize = bytesWritten
result.Duration = time.Since(start)
return result
}
// worker はダウンロードワーカー
func (d *Downloader) worker(id int, tasks <-chan DownloadTask, results chan<- DownloadResult, wg *sync.WaitGroup) {
defer wg.Done()
for task := range tasks {
fmt.Printf("Worker %d: %s をダウンロード中...\n", id, task.FileName)
result := d.DownloadFile(task)
if result.Success {
fmt.Printf("Worker %d: %s 完了 (%d bytes, %v)\n",
id, task.FileName, result.ByteSize, result.Duration)
d.Stats.RecordSuccess(task.URL, result.ByteSize)
} else {
fmt.Printf("Worker %d: %s 失敗 - %v\n",
id, task.FileName, result.Error)
d.Stats.RecordFailure(task.URL)
}
results <- result
}
}
// DownloadAll はすべてのタスクを並行ダウンロード
func (d *Downloader) DownloadAll(tasks []DownloadTask) []DownloadResult {
// 出力ディレクトリの作成
if err := os.MkdirAll(d.OutputDir, 0755); err != nil {
fmt.Printf("出力ディレクトリ作成失敗: %v\n", err)
return nil
}
// チャネルの作成
taskChannel := make(chan DownloadTask, len(tasks))
resultChannel := make(chan DownloadResult, len(tasks))
var wg sync.WaitGroup
// ワーカーの起動
for i := 1; i <= d.MaxWorkers; i++ {
wg.Add(1)
go d.worker(i, taskChannel, resultChannel, &wg)
}
// タスクの送信
for _, task := range tasks {
d.Stats.IncrementTotal()
taskChannel <- task
}
close(taskChannel)
// 結果収集用goroutine
go func() {
wg.Wait()
close(resultChannel)
}()
// 結果の収集
results := make([]DownloadResult, 0, len(tasks))
for result := range resultChannel {
results = append(results, result)
}
return results
}
実装すべき内容:
DownloadFile: 単一ファイルのダウンロードworker: ワーカーgoroutineDownloadAll: 並行ダウンロードの調整- エラー処理
- 統計情報の更新
要件3: プログレス表示(20点)
ダウンロードの進行状況を表示する機能を実装してください。
ファイル: progress.go
package downloader
import (
"fmt"
"sync"
"time"
)
// ProgressMonitor は進行状況を監視
type ProgressMonitor struct {
stats *DownloadStats
interval time.Duration
stop chan struct{}
wg sync.WaitGroup
}
// NewProgressMonitor は新しいモニターを作成
func NewProgressMonitor(stats *DownloadStats, interval time.Duration) *ProgressMonitor {
return &ProgressMonitor{
stats: stats,
interval: interval,
stop: make(chan struct{}),
}
}
// Start は監視を開始
func (pm *ProgressMonitor) Start() {
pm.wg.Add(1)
go pm.monitor()
}
// Stop は監視を停止
func (pm *ProgressMonitor) Stop() {
close(pm.stop)
pm.wg.Wait()
}
// monitor は定期的に進行状況を表示
func (pm *ProgressMonitor) monitor() {
defer pm.wg.Done()
ticker := time.NewTicker(pm.interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
pm.displayProgress()
case <-pm.stop:
return
}
}
}
// displayProgress は進行状況を表示
func (pm *ProgressMonitor) displayProgress() {
total, success, failure, bytes, elapsed := pm.stats.GetStats()
if total == 0 {
return
}
completed := success + failure
percentage := float64(completed) / float64(total) * 100
fmt.Printf("\r進行状況: %d/%d (%.1f%%) | 成功: %d | 失敗: %d | %.2f MB | %v",
completed, total, percentage, success, failure,
float64(bytes)/(1024*1024), elapsed)
}
実装すべき内容:
Start: 監視開始Stop: 監視停止monitor: 定期的な進行状況表示displayProgress: 進行状況のフォーマット
要件4: メインプログラム(10点)
実装した機能を統合するメインプログラムを作成してください。
ファイル: main.go
package main
import (
"fmt"
"time"
"yourmodule/downloader"
)
func main() {
// ダウンロードタスクのリスト
tasks := []downloader.DownloadTask{
{URL: "https://golang.org/doc/gopher/frontpage.png", FileName: "gopher1.png"},
{URL: "https://golang.org/doc/gopher/doc.png", FileName: "gopher2.png"},
{URL: "https://golang.org/doc/gopher/pkg.png", FileName: "gopher3.png"},
{URL: "https://golang.org/doc/gopher/project.png", FileName: "gopher4.png"},
{URL: "https://golang.org/doc/gopher/ref.png", FileName: "gopher5.png"},
}
// ダウンローダーの作成
dl := downloader.NewDownloader(
"./downloads", // 出力ディレクトリ
3, // 並行ワーカー数
30*time.Second, // タイムアウト
)
// プログレスモニターの起動
monitor := downloader.NewProgressMonitor(dl.Stats, 500*time.Millisecond)
monitor.Start()
// ダウンロード実行
fmt.Println("=== ダウンロード開始 ===")
results := dl.DownloadAll(tasks)
// モニター停止
monitor.Stop()
// 結果の表示
fmt.Println("\n\n=== 結果 ===")
for _, result := range results {
if result.Success {
fmt.Printf("✓ %s - %d bytes (%v)\n",
result.Task.FileName, result.ByteSize, result.Duration)
} else {
fmt.Printf("✗ %s - %v\n",
result.Task.FileName, result.Error)
}
}
// 統計レポート
fmt.Println(dl.Stats.Report())
}
実装すべき内容:
- ダウンロードタスクの定義
- ダウンローダーの作成と設定
- プログレスモニターの起動/停止
- 結果の表示
- 統計レポートの出力
期待される出力
=== ダウンロード開始 ===
Worker 1: gopher1.png をダウンロード中...
Worker 2: gopher2.png をダウンロード中...
Worker 3: gopher3.png をダウンロード中...
進行状況: 3/5 (60.0%) | 成功: 3 | 失敗: 0 | 0.25 MB | 1.2s
Worker 1: gopher1.png 完了 (85234 bytes, 850ms)
Worker 1: gopher4.png をダウンロード中...
Worker 2: gopher2.png 完了 (72156 bytes, 920ms)
Worker 2: gopher5.png をダウンロード中...
進行状況: 5/5 (100.0%) | 成功: 5 | 失敗: 0 | 0.45 MB | 2.1s
=== 結果 ===
✓ gopher1.png - 85234 bytes (850ms)
✓ gopher2.png - 72156 bytes (920ms)
✓ gopher3.png - 91024 bytes (1.1s)
✓ gopher4.png - 88765 bytes (780ms)
✓ gopher5.png - 95432 bytes (890ms)
=== ダウンロードレポート ===
総ファイル数: 5
成功: 5
失敗: 0
総ダウンロードサイズ: 432611 bytes (0.41 MB)
所要時間: 2.134s
平均速度: 197.83 KB/s
ボーナス課題
> ボーナス: これらはオプションです。マンダトリー部分が完了してから取り組んでください。
ボーナス1: リトライ機能(10点)
失敗したダウンロードを自動的にリトライする機能を追加してください。
type RetryConfig struct {
MaxRetries int
Delay time.Duration
}
func (d *Downloader) DownloadWithRetry(task DownloadTask, config RetryConfig) DownloadResult {
var result DownloadResult
for attempt := 1; attempt <= config.MaxRetries; attempt++ {
result = d.DownloadFile(task)
if result.Success {
return result
}
fmt.Printf("リトライ %d/%d: %s\n", attempt, config.MaxRetries, task.FileName)
time.Sleep(config.Delay)
}
return result
}
ボーナス2: レート制限(5点)
ダウンロード速度を制限する機能を実装してください。
type RateLimiter struct {
rate int64 // bytes per second
interval time.Duration
tokens int64
mu sync.Mutex
}
func NewRateLimiter(bytesPerSecond int64) *RateLimiter
func (rl *RateLimiter) Wait(bytes int64)
ボーナス3: レジューム機能(5点)
中断されたダウンロードを再開する機能を追加してください。
func (d *Downloader) ResumeDownload(task DownloadTask) DownloadResult {
// 既存ファイルのサイズを取得
outputPath := filepath.Join(d.OutputDir, task.FileName)
fileInfo, err := os.Stat(outputPath)
var offset int64 = 0
if err == nil {
offset = fileInfo.Size()
fmt.Printf("%s: %d bytesから再開\n", task.FileName, offset)
}
// Range ヘッダーを使ってリクエスト
req, _ := http.NewRequest("GET", task.URL, nil)
if offset > 0 {
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset))
}
// ダウンロード続行...
}
評価基準
| 項目 | 配点 | 詳細 |
|---|---|---|
| 統計管理 | 20点 | 並行安全な統計情報の管理ができている |
| ダウンローダー | 30点 | 並行ダウンロードが正しく動作する |
| プログレス表示 | 20点 | リアルタイムで進行状況を表示できている |
| メインプログラム | 10点 | 全機能を統合して動作している |
| コード品質 | 20点 | エラー処理、同期、リソース管理が適切 |
| **ボーナス1** | 10点 | リトライ機能が正しく動作する |
| **ボーナス2** | 5点 | レート制限が実装されている |
| **ボーナス3** | 5点 | レジューム機能が動作する |
提出方法
以下のファイルを提出してください:
submission/
├── go.mod
├── stats.go # 統計管理
├── downloader.go # ダウンローダー
├── progress.go # プログレス表示
├── main.go # メインプログラム
└── bonus/ # ボーナス課題(オプション)
├── retry.go # リトライ機能
├── ratelimit.go # レート制限
└── resume.go # レジューム機能
ヒント
mu.Unlock()、wg.Done()は必ずdefergo run -raceでレースコンディションを検出