Goでファイルをダウンロードしたいと考えたとき、まず押さえたい結論はシンプルです。
基本形は net/http でレスポンスを取得し、os.Create で保存先ファイルを作り、io.Copy でそのまま書き込む形です。
これだけで小さなサンプルは動きますが、実務では「HTTPステータスの確認」「レスポンスボディのクローズ」「タイムアウト設定」「一時ファイル経由での保存」「ファイル名の決め方」まで考えないと、途中失敗や壊れたファイル、処理のぶら下がりが起きやすくなります。
実際に調べると、Goでのファイルダウンロード解説は最小サンプル中心のものと、実運用を意識した安全設計まで踏み込むものに分かれます。
そのため本記事では、単に動くコードの説明で終わらせず、初心者が最初につまずきやすい点から、業務で使うときの設計判断まで整理して解説します。
「とりあえず1本ダウンロードしたい」「CLIツールで配布物を保存したい」「失敗時の扱いも含めて丁寧に実装したい」という方は、この記事を読めば判断基準まで見えてきます。
タップできる目次
Goでファイルをダウンロードする基本構成
Goでのファイルダウンロードは、標準ライブラリだけで十分に実装できます。
中心になるのは、HTTP通信の net/http、ファイル作成の os、ストリーム転送の io です。
流れとしては、URLへリクエストを送り、レスポンス本文を受け取り、その内容をファイルへコピーします。
最小構成の全体像
最もよく使われる考え方は次の流れです。
| 手順 | 使う主な機能 | 役割 |
|---|---|---|
| URLへアクセス | http.Get または http.Client |
ダウンロード元へ接続 |
| 保存先を用意 | os.Create |
出力ファイルを作成 |
| データを書き込む | io.Copy |
レスポンス本文をそのまま保存 |
| 後処理 | Close |
リソースを解放 |
この形はシンプルで覚えやすく、Goらしい実装でもあります。
特に io.Copy は、Reader から Writer へストリームを転送する標準的な方法として広く使われています。
まず覚えたい最小サンプル
まずは最小形を理解すると、全体像をつかみやすいです。
以下のイメージです。
http.Getでレスポンス取得defer resp.Body.Closeでボディを閉じるos.Createで保存ファイル作成defer file.Closeでファイルを閉じるio.Copy(file, resp.Body)で保存
この形だけなら数行で書けます。
ただし、実務ではこの最小形のまま使うのは危険な場面もあります。
理由は、200以外のHTTPステータスでもレスポンスが返ることがあり、その本文をそのまま保存すると、エラーページをファイルとして保存してしまうことがあるためです。
最初に押さえたい結論
Goでファイルダウンロードを実装するなら、最初から次の方針で考えるのが安全です。
- 学習用なら
http.Get + os.Create + io.Copy - 実務用なら
http.Client + timeout + status確認 + 一時ファイル保存 - 大きいファイルならメモリに全読み込みせずストリーム処理
- 失敗時に壊れたファイルを残したくないなら、一時ファイルからリネーム
- ダウンロード元が遅い、止まる可能性があるなら
context.WithTimeoutも検討
この方針を先に持っておくと、必要以上に複雑化せず、かつ事故も減らせます。
最小コードだけでは足りない理由
見た目は短く書ける処理ですが、実際には見落としやすい論点がいくつもあります。
特に初心者がつまずきやすいのは、成功判定と保存失敗時の扱いです。
HTTPステータス未確認のまま保存する問題
たとえばURLが間違っていると、404のHTMLが返ることがあります。
しかし http.Get 自体は通信に成功していれば err == nil になることがあり、そのまま io.Copy すると「画像を保存したつもりがHTMLだった」という状態になります。
そのため、resp.StatusCode を確認し、通常は 200 OK のときだけ保存を続ける実装が基本です。
Close忘れによるリソースリーク
resp.Body.Close と file.Close は省略しないほうが安全です。
少量の検証コードでは問題が見えにくいですが、繰り返し実行するCLIやサーバー処理では、ファイルディスクリプタ不足や接続の使い回し不全につながることがあります。
全読み込みによるメモリ圧迫
初心者向け記事では、本文をいったん全部メモリに読み込んでから保存する例も見かけます。
ただし、大きなファイルではこの方法は不利です。
Goでファイルダウンロードをするなら、io.Copy でストリームのまま保存するほうが自然で、サイズが大きいファイルでも扱いやすいです。
安全寄りの実装で押さえるポイント
ここからは、実際に使いやすい実装に寄せるための考え方を整理します。
http.Client を使う理由
http.Get は手軽ですが、実務では http.Client を使うことが多いです。
理由は、タイムアウト設定やリダイレクト制御などを持たせやすいからです。
GoのHTTPクライアントでは、デフォルトのクライアントに総合タイムアウトがないまま使われることがあり、通信先次第で長時間待ち続ける可能性があります。
そのため、CLIツールやバッチ処理ではタイムアウトを明示したほうが安心です。
context.WithTimeout を使う場面
処理単位で期限を持たせたいときは、context.WithTimeout が便利です。
たとえば「この1本のダウンロードは30秒以内」と決めたいなら、リクエストにコンテキストを紐づけておくと、期限超過でキャンセルできます。
複数ダウンロードをまとめる処理でも、親コンテキストからキャンセルを伝播できるため、運用しやすくなります。
一時ファイル保存のメリット
ダウンロード途中で失敗したとき、中途半端なファイルが完成品として残ると厄介です。
そのため、いったん sample.zip.part のような一時ファイルへ保存し、最後まで成功したら正式ファイル名へ変更する形が実務ではよく選ばれます。
この方法なら、失敗時に「壊れた完成ファイル」が見える状態を避けやすいです。
実装パターン別の選び方
Goでのファイルダウンロードは、用途によって実装の重さを変えるのがコツです。
学習・検証向けの実装
まず動作確認したいだけなら、最小形で十分です。
- 単発実行
- 小さなファイル
- 社内検証やローカル確認
- エラー処理は最低限でもよい
この用途なら、短いサンプルで概念を理解するのが先です。
CLIツール向けの実装
コマンドラインツールとして配布するなら、次の要素が重要です。
- タイムアウト
- ステータスコード確認
- 保存先ディレクトリの存在確認
- 一時ファイル利用
- 上書き可否
- 進捗表示
利用者が失敗理由を把握しやすいよう、エラー文も整理しておくと親切です。
業務処理・バッチ向けの実装
定期実行や自動処理では、再実行前提の設計が重要です。
- 冪等性
- 途中失敗時のクリーンアップ
- リトライ方針
- ログ出力
- サイズや形式の検証
- ダウンロード先の権限管理
単に「保存できる」だけでは足りず、失敗時の挙動まで含めて設計する必要があります。
実装前に決めておきたい判断項目
コードを書く前に、何を仕様として決めるかで迷いやすいです。
先に整理しておくと、後から修正しにくい部分を減らせます。
| 判断項目 | 主な選択肢 | 判断の目安 |
|---|---|---|
| 保存ファイル名 | 固定名 / URLから生成 / 指定入力 | CLIなら引数指定が扱いやすい |
| 上書き | 許可 / 禁止 / 確認 | 自動処理なら明示設定が安全 |
| 成功判定 | 200のみ / 2xx許可 | API仕様に応じて決める |
| 失敗時ファイル | 残す / 削除する | 通常は削除が無難 |
| タイムアウト | なし / 全体 / 段階別 | 外部通信なら設定推奨 |
| ログ | 標準出力 / 標準エラー / ファイル | CLIは標準エラー分離が便利 |
こうした観点を持つだけで、コードの見通しがかなり良くなります。
具体的な実装観点
ここでは、記事を読んだあとに実際に書けるよう、コード化の前提になる実装観点を整理します。
保存先ファイルの作成方法
os.Create は、ファイルがなければ作成し、あれば切り詰めて上書きする挙動が基本です。
そのため、「既存ファイルがあってもよい」用途には向きます。
一方で、上書きを避けたいなら os.OpenFile でフラグを細かく指定する方法が向いています。
たとえば既存ファイルがあると失敗させたい場合は、作成フラグの選び方を変える必要があります。
io.Copy の使いどころ
io.Copy は、レスポンスボディからファイルへそのまま流し込めるので、Goのダウンロード処理では定番です。
バイト列をいったん全部 []byte に持たずに済むため、大きなファイルでも扱いやすいのが利点です。
特に「画像」「zip」「PDF」「バイナリ配布物」などは、文字列として扱わずストリームで保存するほうが自然です。
Content-Typeを信じすぎない視点
レスポンスヘッダに Content-Type が含まれていても、それだけで安全とは言い切れません。
配布元の設定ミスで期待と異なる形式が返ることもあります。
そのため、重要なファイルを扱うときは次のように考えると堅実です。
- ステータスコード確認
- 必要なら
Content-Type確認 - 必要ならサイズ確認
- 保存後に拡張子や形式を別途検証
「ヘッダが合っているから大丈夫」と決めつけないほうが事故は減ります。
よくあるつまずきポイント
Goでファイルをダウンロードし始めた方が、特につまずきやすい点を整理します。
err == nil なのに失敗している状態
これは非常によくあります。
通信自体は成功しているので http.Get のエラーは出ませんが、実際には404や403などで目的のファイルは取れていないという状態です。
このときは resp.StatusCode を見ないと判断できません。
保存できたが開けないファイル
保存処理が完了しても、ファイルが正常とは限りません。
たとえば次のような原因があります。
- HTMLのエラーページを保存した
- 途中で切れた
- 圧縮ファイルの拡張子だけ合っていて中身が違う
- 認証ページを保存した
ファイルが存在することと、内容が正しいことは別問題です。
途中失敗で壊れたファイルが残る状態
完成ファイル名で直接書き込み始めると、途中失敗時に不完全なファイルが残ります。
利用者から見ると「あるのに使えない」状態になるため、問い合わせ原因にもなりやすいです。
この点で、一時ファイル運用は見た目以上に効果があります。
進捗表示が必要な場面
CLIツールでは、進捗表示があるだけで使い勝手がかなり上がります。
特に数十MB以上のファイルや、ネットワークが不安定な環境では、「止まっているのか、進んでいるのか」が見えないと不安になりやすいです。
進捗表示の実装イメージ
Goでは、読み込み時にバイト数を数えるラッパーを作り、その値を定期的に表示する形が一般的です。
考え方としては、resp.Body をそのまま io.Copy に渡すのではなく、読み込んだサイズをカウントする Reader で包んでから保存します。
進捗率が正しく出ない場面
進捗率を出すには、総サイズが分かっている必要があります。
ただし、サーバーが Content-Length を返さないこともあります。
この場合は、百分率ではなく「何MB受信したか」だけを出すほうが現実的です。
そのため、進捗表示は次の2種類で考えると分かりやすいです。
| 表示形式 | 向いている場面 | 注意点 |
|---|---|---|
| %表示 | Content-Length がある |
サイズ不明時は使えない |
| 受信量表示 | サイズ不明でも使える | 終了予定は見えにくい |
タイムアウト設定の考え方
Goでファイルダウンロードを書くとき、タイムアウトは軽視されがちです。
ですが、業務で止まり続ける処理は想像以上に困ります。
タイムアウトなしのリスク
通信先が応答しない、極端に遅い、途中で不安定になると、処理がぶら下がることがあります。
バッチやCLIでは、ユーザーから見ると「固まった」ように見えるため、体験が悪くなります。
全体タイムアウトと処理単位タイムアウト
ざっくり分けると、考え方は2つあります。
- クライアント全体に時間制限を設ける
- 個別リクエストに
context.WithTimeoutを付ける
単発ダウンロードなら前者でも十分なことがあります。
一方で、複数の処理をまとめて制御したいなら、コンテキストを使うほうが柔軟です。
迷ったときの実務寄りの目安
絶対的な正解はありませんが、迷うなら次のように考えやすいです。
- 検証用スクリプトなら短めでもよい
- 配布物ダウンロードなら回線事情を見て少し長め
- バッチなら監視しやすい制限時間を設ける
- ユーザー操作を伴うCLIは、待てる長さを意識する
大切なのは「タイムアウト値そのもの」より、「無制限にしない」ことです。
ファイル名の決め方
「どの名前で保存するか」は地味ですが重要です。
URL末尾をそのまま使う方法
簡単なのは、URLの最後のパス要素をファイル名として使う方法です。
ただし、このやり方には注意点があります。
- クエリ付きURLだと扱いにくい
- 末尾が空の場合がある
- 意味のない名前になることがある
- 同名ファイル衝突が起きる
ユーザー指定を優先する方法
CLIなら、保存名は引数で指定できる形が分かりやすいです。
自動処理でも、保存名ルールを明示しやすくなります。
現場感としては、URL自動推定に頼り切るより、指定可能にしておくほうがトラブルが少ないです。
上書き・追記・排他の違い
保存処理では「書けるか」だけでなく、「どう書くか」を区別する必要があります。
上書き保存
もっとも単純です。
同じ名前があれば消して作り直すイメージで、os.Create が向いています。
既存ファイルがあると失敗させる保存
誤上書きを防ぎたい場面では、この挙動が向いています。
たとえば手動ダウンロードツールで既存成果物を消したくない場合です。
追記保存
通常のファイルダウンロードでは、追記はあまり使いません。
中断再開のような高度な処理をしない限り、追記は壊れたファイルを作る原因になりやすいです。
そのため、初心者の段階では「通常のダウンロードに追記は使わない」と覚えておくと整理しやすいです。
中断再開が必要な場合の考え方
大きなファイルを扱うと、「途中から再開したい」と考えることがあります。
ただし、ここは最小実装とは別物です。
単純な追記では不十分な理由
中断位置から再開するには、保存済みサイズとサーバー側の対応が一致している必要があります。
HTTPのRangeリクエストに対応していない配布元では、期待どおりに再開できないことがあります。
そのため、中断再開は「ファイルに追記するだけ」と考えないほうが安全です。
まずは再取得で十分な場面
社内ツールや小〜中サイズの配布物なら、中断再開よりも「失敗したら削除して最初からやり直す」ほうが簡単で保守しやすいです。
必要性が明確になってから再開機能を入れるほうが、実装コストに見合いやすいです。
実務で使いやすい実装方針
ここまでを踏まえると、実務では次の形がバランス良いです。
推奨しやすい基本方針
http.Clientを使う- 必要に応じて
context.WithTimeout StatusCodeを確認する- 一時ファイルへ保存する
- 成功後に正式名へ変更する
- エラー時は一時ファイルを削除する
- 大きなファイルも
io.Copyでストリーム保存する
この構成なら、学習用の延長で理解しやすく、それでいて実用性も高いです。
実際に組み込むときの優先順位
全部を最初から盛り込むと重く感じるなら、次の順で加えると進めやすいです。
| 優先度 | 追加したい要素 | 理由 |
|---|---|---|
| 高 | ステータスコード確認 | 誤保存を防ぎやすい |
| 高 | Body.Close と File.Close |
基本的な安全性 |
| 高 | タイムアウト | ぶら下がり防止 |
| 中 | 一時ファイル保存 | 壊れた完成ファイル防止 |
| 中 | エラーメッセージ整理 | 利用者が原因を把握しやすい |
| 低 | 進捗表示 | 使い勝手向上 |
| 低 | 再開機能 | 要件が明確なときのみ |
この記事の内容で判断しやすくなること
読者目線でいうと、このキーワードで本当に知りたいのは「Goでどう書くか」だけではありません。
実際には、次の判断ができるかどうかが重要です。
- 最小コードで十分か
- 実務で何を追加すべきか
- 失敗時にどこを見ればよいか
- 保存方法をどう選ぶべきか
- 大きなファイルでも安全に扱えるか
Goのファイルダウンロードは、標準ライブラリ中心で組めるぶん、設計の良し悪しがそのまま出やすい分野です。
短いサンプルを写すだけでなく、どこまで守りを入れるべきかを理解しておくと、あとで作り直しにくくなります。
まとめ
Goでファイルをダウンロードする基本は、net/http で取得し、os.Create で保存先を作り、io.Copy で書き込む形です。
まずはこの流れを理解すれば、単純なダウンロード処理は十分に実装できます。
ただし、実際に使える形へ近づけるには、HTTPステータス確認、レスポンスボディのクローズ、タイムアウト設定、一時ファイル保存といった要素が重要です。
特に「通信は成功したが目的のファイルではない」「途中失敗で壊れたファイルが残る」といった問題は起こりやすいため、最小コードの先まで理解しておく価値があります。
迷ったときは、まずはシンプルな実装で流れをつかみ、その後に http.Client、context.WithTimeout、一時ファイル運用を足していく進め方がおすすめです。
「とりあえず動く実装」から「安心して使える実装」へ段階的に育てることが、Goでのファイルダウンロードでは最も現実的な進め方です。