💻

VS Code と Docker で始める!Go 開発入門 - 2025年10月版

に公開

VS Code と Docker で始めるモダン Go 開発入門 - 2025年10月版

はじめに

この記事では、Go プログラミングをこれから始める開発者に向けて、開発に役立つ「Go 開発コンテナ」のセットアップ方法を解説します。この開発コンテナ環境(dvc-go-gemini)を使い、Go 言語で作成されたサンプルアプリ goapp001 を開発していくシナリオを通じて、Go における基本的なアプリの構成要素や開発プラクティスについて解説します。Go プログラミング入門レベルの開発者が、実践的なアプリ開発の第一歩を踏み出すためのガイドとなることを目的とします。

この記事を読むことで、Go 向けの開発コンテナに関して以下の項目について理解を深めることができます。

  • Go 開発コンテナの利点
  • Go のバージョン管理手法
  • Go の開発ツールとモジュールの管理
  • 様々なデバッグ方法への対応

goapp001 は、環境に応じた設定切り替え、構造化ロギング、テスト、Docker 化など、現代的なアプリに求められる基本的な要素を網羅しています。この記事を読むことで、以下の項目について理解を深めることができます。

  • 作成する Go アプリの設計思想
  • Go モジュールによる依存関係の管理
  • zerolog を利用した効率的なログ出力
  • viper を活用した柔軟な設定管理
  • Go の標準テスト機能とテーブル駆動テスト
  • Docker を使った開発・テスト・本番環境の構築

コードは、GitHub で公開しています。

なぜ Go 開発コンテナを使うのか

新しいプログラミング言語やフレームワークを学ぶ際、環境構築は最初のハードルになりがちです。Go も例外ではありません。Go 本体のインストールに加え、リンターやデバッガーなどの開発ツール、プロジェクトで利用するライブラリ(モジュール)の管理など、やるべきことは多岐にわたります。

開発コンテナは、こうした環境構築の手間を大幅に削減するための強力なソリューションです。開発コンテナを利用する主なメリットは以下の通りです。

  1. 環境の再現性
  2. 環境の分離
  3. 参照環境としての役割

環境の再現性というのは、Dockerfiledevcontainer.json といった構成ファイルで開発環境をコードとして定義(IaC、Infrastructure as Code)できることを指します。これにより、誰が実行しても同じ環境を正確に再現できます。新しいメンバーがプロジェクトに参加する際も、数コマンドで開発を始められます。

環境の分離というのは、開発環境がコンテナとしてローカルマシンのホスト OS から隔離されるため、ローカルマシンの環境をクリーンに保てます。プロジェクトごとに異なるバージョンの Go やツールが必要になった場合でも、互いに影響を与えることなく管理できます。

参照環境としての役割というのは、プロジェクトで推奨される「参照開発環境」として開発コンテナを用意しておくことで、チーム全体の開発標準を統一しやすくなります。

もし、お使いのマシンのコンピュータリソース(CPU やメモリ)が十分にあるなら、開発コンテナをそのまま主要な開発環境として利用することもできます。

DevContainer の設定ファイル

それでは、具体的な設定ファイルを見ていきましょう。ここでは、VS Code の Dev Containers 機能で利用する標準的なファイル構成を例に挙げます。dvc-go-gemini フォルダをワークスペースとする開発コンテナを用意するとして、.devcontainer フォルダに必要な設定ファイルを置きます。

dvc-go-gemini/
├── .devcontainer/ ... 開発コンテナ用フォルダ
│   ├── Dockerfile
│   ├── compose.yaml
│   └── devcontainer.json
├── .gemini/ ... Gemini 設定用フォルダ
├── .gitignore
└── goapp001/ ... 開発するアプリのサンプル

Dockerfile は、開発コンテナのベースイメージや、インストールするソフトウェアを定義するものです。

compose.yaml は、コンテナの構成(ビルド方法、使用するボリュームなど)を定義するものです。使用するには Docker Compose が必要です。

devcontainer.json は、開発コンテナの全体的な設定を定義するものです。

なお、これ以降、dvc-go-gemini フォルダのパスを ${PROJECT_DIR} と表記することにします。また、これ以降紹介するコマンドについて、開発コンテナや Docker 関連のコマンドについては、${PROJECT_DIR} をカレントフォルダ(カレントディレクトリ)として実行するものとします。Go アプリの goapp001 関連のコマンドについては、${PROJECT_DIR}/goapp001 をカレントフォルダとして実行するものとします。

ターミナルでカレントフォルダを変更するには cd コマンドを使います。たとえば、/workspace/dvc-go-geminidvc-go-gemini フォルダのパスだとしたら、次のようにします。

PROJECT_DIR=/workspace/dvc-go-gemini
cd ${PROJECT_DIR}

また、.gemini については、Gemini CLI を使っている場合は、ホームにあるものをコピーしておきます。使っていない場合はフォルダを作成しておきます。この作業を bash のコマンドで表現すると次のようになります。

if [ -e "${HOME}/.gemini" ]; then cp -a "${HOME}/.gemini" ${PROJECT_DIR}/; fi
if [ ! -e ${PROJECT_DIR}/.gemini ]; then mkdir ${PROJECT_DIR}/.gemini; fi

Dockerfile

まずは、コンテナの設計図である Dockerfile について説明します。ここでは、次のような内容で作成します。

FROM hiro345g/dvc:go-202509

RUN npm install -g @google/gemini-cli

FROM hiro345g/dvc:go-202509 では、ベースイメージとして hiro345g/dvc:go-202509 を指定しています。このイメージには、Go の開発に必要なツール一式があらかじめ含まれています。

RUN npm install -g @google/gemini-cliは、Google の Gemini CLI をインストールしています。Go の開発とは直接関係ありませんが、ここでは例としてインストールしています。今後の開発では、こういった AI ツールの利用は必須となるでしょう。このように、プロジェクトで必要なツールを Dockerfile に記述することで、開発環境に含めることができます。

compose.yaml

次に、Docker Compose の設定ファイル compose.yaml について説明します。このファイルで、提供するサービス、サービスを実現するコンテナについての設定、コンテナのビルド方法、使用するストレージやネットワークといったコンピュータリソースを定義します。

name: dvc-go-gemini
services:
  dvc-go-gemini:
    build: .
    image: dvc-go-gemini:202509
    container_name: dvc-go-gemini
    hostname: dvc-go-gemini
    tty: true
    user: 1000:1000
    volumes:
      # 開発コンテナで使う作業用フォルダ
      - type: bind
        source: ..
        target: /workspaces/dvc-go-gemini
      # Gemini CLI の設定用フォルダ
      - type: bind
        source: ../.gemini
        target: /home/node/.gemini
      - type: volume
        source: gopath-data
        target: /go

volumes:
  gopath-data:
    name: dvc-go-gemini-gopath-data

各設定について説明します。

build: .: .devcontainer ディレクトリにある Dockerfile を使ってコンテナイメージをビルドします。

image: dvc-go-gemini:202509: イメージ名を compose.yaml へ指定することで、このプロジェクトで使用している Docker イメージの名前が明確になります。そのため、Docker イメージの管理がしやすくなります。

container_name: dvc-go-gemini: コンテナ名を dvc-go-gemini と指定することで、自分が指定したコンテナ名にすることができます。docker container コマンドを実行するときなど、コンテナ名がわかっていた方が作業がしやすくなります。

hostname: dvc-go-gemini: ホスト名を dvc-go-gemini と指定することで、コンテナへアタッチしたときに表示されるプロンプトをこの値にすることができます。ターミナルで作業するときに、コンテナを間違うことがなくなるので、設定しておくのが良いです。

user: 1000:1000: 開発コンテナでは一般ユーザーを使うことが推奨されています。ここで用意してある開発コンテナでは node ユーザーのユーザー ID が 1000、グループ ID が 1000 なので 1000:1000 を指定しています。

volumes: ホストマシンのファイルシステムとコンテナのファイルシステムを接続(マウント)します。

volumes については3つのマウント指定をしています。

  • プロジェクト用フォルダ
  • Gemini CLI 設定用フォルダ
  • Go のシステム用フォルダ

プロジェクト用フォルダは、source: ..target: /workspaces/dvc-go-gemini により、プロジェクトのルートディレクトリがコンテナ内の /workspaces/dvc-go-gemini にマウントされ、ホストマシン上のファイルをコンテナ内から直接編集できます。

Gemini CLI 設定用フォルダは、$HOME/.geminidvc-go-gemini/.gemini へコピーしておくことで、ホスト側で使えていた Gemini CLI 用の設定をそのまま使えるようになります。

Go のシステム用フォルダは、開発コンテナの /go フォルダにマウントされます。このフォルダには Go のパッケージなどがインストールされるので、gopath-data という名前付きボリュームに割り当てています。これにより、docker compose down などでコンテナを削除しても、ダウンロードしたパッケージがキャッシュされているこのフォルダは維持されます。そのため、2回目以降の起動が高速になります。

devcontainer.json

最後に、VS Code が開発コンテナを認識し、設定を適用するための devcontainer.json について説明します。

{
  "name": "dvc-go-gemini",
  "dockerComposeFile": "./compose.yaml",
  "service": "dvc-go-gemini",
  "postCreateCommand": "echo '127.0.0.1 host.docker.internal' | sudo tee -a /etc/hosts",
  "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
  "remoteUser": "node",
  "shutdownAction": "none",
  "remoteEnv": {
    "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
  },
  "forwardPorts": [6080, 5901],
  "portsAttributes": {
    "6080": {
      "label": "novnc"
    },
    "5901": {
      "label": "tigervnc"
    }
  },
  "customizations": {
    "vscode": {
      "extensions": ["golang.go"]
    }
  }
}

dockerComposeFileservice: compose.yaml ファイルと、その中で定義されている dvc-go-gemini サービスを使用することを指定します。

postCreateCommand: コンテナが作成された後に実行されるコマンドです。ここでは、コンテナ内からホストマシンにアクセスするための設定を行っています。なお、ここで指定しているものは、google.gemini-cli-vscode-ide-companion 拡張機能が、開発コンテナ内からホストマシン上の Gemini CLI バックエンドプロセスと通信するための設定です。この拡張機能を使わない場合や、host.docker.internal を別の目的で使用したい場合は、この行はコメントアウトまたは削除してください。また、この設定については、google.gemini-cli-vscode-ide-companion 拡張機能のバージョンアップによって不要になるかもしれません。

workspaceFolder: VS Code で開くワークスペースのパスを指定します。

remoteUser: コンテナ内でコマンドを実行するデフォルトのユーザーを指定します。

customizations.vscode.extensions: 開発コンテナに自動的にインストールする VS Code 拡張機能を指定します。golang.go を指定することで、Go の開発に必須の拡張機能が利用可能になります。

これらのファイルを用意することで、VS Code と DevContainers 拡張機能を使って、誰でも簡単に Go の開発環境を立ち上げることができます。

Go のバージョン管理

Go 言語は後方互換性が非常に高いことで知られていますが、それでも古いプロジェクトのメンテナンスや、最新機能の試用、複数バージョンでのテストなど、開発現場では複数の Go のバージョンを管理する必要が出てきます。

Go のバージョン管理には、大きく分けて3つの方法があります。

  1. ネイティブの Go ツールチェーン機能を利用する方法A
  2. mise-en-place のような統合環境マネージャーを利用する方法B
  3. 開発コンテナーを利用する方法C

以下で、それぞれの方法の長所と短所を解説します。

方法A: ネイティブの Go ツールチェーン機能

Go 本体が提供する公式なバージョン管理機能を利用する方法です。これには2つの主要な仕組みがあります。

go install による特定バージョンのインストール:

go install golang.org/dl/go1.x.y@latest を実行すると、go1.x.y という名前のラッパーコマンドがインストールされます。その後 go1.x.y download を実行すれば、対応するGoのSDKがダウンロードされ、準備が整います。

# Go 1.24.5 をインストール
go install golang.org/dl/go1.24.5@latest
go1.24.5 download

# インストールしたバージョンでコマンドを実行
go1.24.5 test ./...

この方法を採用するメリットは次のとおりです。

  • 公式にサポートされており、信頼性が高いこと
  • CI/CD スクリプトなどで、go1.24.5 test のようにバージョンを明示的に指定して実行できること

この方法を採用するデメリットは次のとおりです。

  • プロジェクトの Go バージョンを更新する際、スクリプトや Makefile にハードコードされた go1.24.5 のような記述に対して、go1.25.0 のように書き換える必要があり、メンテナンスコストが高くつくことがあります。

GOTOOLCHAIN による自動切り替え (Go 1.21+):

Go 1.21から、go.mod ファイルに toolchain ディレクティブを記述できるようになりました。また、go.mod ファイルにはモジュールが必要とする Go の最小バージョンを指定できる go ディレクティブも記述できます。

go.mod
module example.com/hello
go 1.24
toolchain go1.24.5

go コマンドは、この toolchain ディレクティブ(または環境変数 GOTOOLCHAIN)を見て、指定されたバージョンの Go がローカルになければ自動的にダウンロードして使用します。これにより、開発者はプロジェクトごとに Go のバージョンを意識的に切り替える必要がなくなります。

これらについてのドキュメントは公開されています。

go ディレクティブは、単に使用可能なバージョンを表すだけでなく、このバージョン以降の Go ツールチェインでなければモジュールを使用できないという必須要件になっています。

ここの例では、このモジュールを使用するには Go 1.24 の機能が必要であることを示しています。

通常、go mod initgo mod tidyなどのコマンドを実行すると、現在使用しているGoのバージョンでこのディレクティブが自動的に追加または更新されます。

このディレクティブの役割は次のとおりです。

  • 言語機能の互換性: モジュールが使用するGoの言語機能や標準ライブラリのAPIの最小バージョンを宣言
  • 最小要件: このバージョン以上の Go ツールチェインが必要
  • 暗黙的なツールチェインの指定(Go 1.21 以降): toolchain ディレクティブが省略されている場合、go ディレクティブの値から初期値が決定

Go 1.21 で導入された toolchain ディレクティブは、モジュールのビルドや実行に使用される特定の Go ツールチェインのバージョンを明示的に指定します。

go ディレクティブに続いて、go<Version> の形式で、パッチバージョンを含めるフォーマットで指定することが推奨されます。ここの例では go1.24.5 としています。

この例では、モジュールは Go 1.24 の機能に依存しつつ、ビルドには Go 1.24.5 のツールチェインを使用することになります。実際の開発では、go 1.22toolchain go1.24.5 のように、少しバージョンが離れてしまうことが多いでしょう。

toolchain ディレクティブの導入の背景は、ビルドの一貫性と再現性の向上のためです。toolchain を指定することで、開発者や CI/CD 環境において、正確に同じ Go コンパイラとツールを使ってビルドされることが保証され、ビルドの再現性が向上します。

また、このディレクティブで指定されたツールチェインがローカルに存在しない場合は、go コマンドは指定されたバージョンのツールを含む SDK を自動的にダウンロードして使用するということです。ただし、環境変数 GOTOOLCHAIN が未設定か auto となっている場合で、それ以外ではこの動作は抑制されます。

特定のバグ修正やセキュリティパッチが適用されたバージョン(例: go1.24.5)でビルドしたいが、言語要件は引き続き最新のメジャーバージョン(例: go 1.24)で十分、といった場合への対応として、こういったディレクティブが必要となりました。

toolchainディレクティブを省略した場合、goディレクティブで指定されたバージョン(例: go 1.22)がツールチェインとしても暗黙的に使用されます(例: toolchain go1.22)。この場合、goコマンドはgo1.22.x系の最新のパッチバージョンをダウンロードして使用します。

以上の説明からわかるように、ほとんどのプロジェクトでは、go ディレクティブのみを記述し、toolchain を省略しても問題ありません。

ただし、その場合は go 1.24.5 のようにパッチバージョンを含めるのが安全で推奨されています。これにより、Go 1.24.5 の言語機能と、自動的に選択される Go 1.24.x 系のツールチェインが使用されます。

商用サービスの開発などで、特定の理由(厳密なビルドの再現性や、特定のバグが修正されたパッチバージョンの強制など)がない限りは、toolchain ディレクティブは省略しても大丈夫です。

GOTOOLCHAIN 環境変数

GOTOOLCHAIN 環境変数を設定すると、go.mod 内の gotoolchain ディレクティブよりも優先され、特定の Go ツールチェインを強制的に使用できます。

例えば、一時的にGo 1.21.9 を使ってテストする場合は次のようにします。

GOTOOLCHAIN=go1.21.9 go test ./...

現在の設定を確認するには、次のコマンドを使います。

cat $(go env GOROOT)/go.env|grep GOTOOLCHAIN

実行例は次のとおりです。

$ cat $(go env GOROOT)/go.env|grep GOTOOLCHAIN
GOTOOLCHAIN=auto

CI/CD 環境での注意点:

開発者のローカル環境では便利な自動ダウンロード機能ですが、CI/CD(継続的インテグレーション/継続的デリバリー)のパイプラインにおいては、意図しないバージョンの Go が使われてしまう原因となり、ビルドの再現性を損なう可能性があります。

CI/CD 環境では、使用する Go のバージョンを Docker イメージやセットアップスクリプトで厳密に固定するのが一般的です。このような環境で go コマンドが go.mod を見て勝手に新しいツールチェーンをダウンロードするのを防ぐため、GOTOOLCHAIN=local という環境変数を設定することが推奨されています。

例えば、CI/CD 用の Docker コンテナを起動する compose.yaml では、次のように environment: で指定すると良いでしょう。

docker/cicd/compose.yaml
services:
  test:
    image: goapp001-cicd:1.24.5
    volumes:
      - type: bind
        source: ${LOCAL_WORKSPACE_FOLDER:-../..}
        target: /app
    environment:
      GOTOOLCHAIN: ${GOTOOLCHAIN:-local}

この設定により、go コマンドは go.modtoolchain ディレクティブを無視し、CI/CD 環境にインストールされている Go のバージョンを常に使用するようになります。これにより、CI/CD パイプラインの安定性が保たれます。

なお、バージョン定義の単一ソース(Single Source of Truth, SSOT)化をしたい場合は、コンテナ内部に用意された go のバージョンではなく、go.mod の指定を優先する必要があるので、GOTOOLCHAIN の値は auto とします。そうしておけば、コンテナ内で使われる go のツールは go.mod で指定したものになります。

ちなみに、執筆者の経験からすると、SSOT 化は苦労のわりに維持と運用が大変なことが多いです。基本は go.mod で良いのですが、Docker、DevContainer については Dockerfile 内で指定した Go のバージョンを優先する方式の方が現実的には多くのユースケースでは適していると考えています。バージョン指定が必要なファイルについては、README.mdGEMINI.md に記載して管理し、きちんと開発者が認識可能な状態にしておけば良いでしょう。

方法B: 統合環境マネージャー (miseなど)

mise (mise-en-place) のようなサードパーティ製のツールを使い、Go だけでなく、Node.js、Python、Ruby といったプロジェクトで必要なすべてのツールを単一の設定ファイル (mise.toml) で一元管理する方法です。

mise.toml
[tools]
go = "1.24.5"
node = "20.10.0"

こういったツールを使う場合の長所は次のとおりです。

  • 統合性: Go 以外のツールもまとめてバージョン管理できるため、特に多言語プロジェクトでの環境再現性が劇的に向上します。
  • 柔軟性: mise.toml のバージョンを書き換えるだけで、プロジェクトで使うツール全体のバージョンを更新でき、方法Aのようなスクリプト修正の手間がありません。
  • 高いパフォーマンスと透過性: mise は Rust で実装されており高速に動作します。また、シェルの PATH を直接書き換える方式(PATH アクティベーション)を採用しているため、IDE などが Go の SDK パスを認識しやすく、ツール連携の問題が起きにくいです。

一方で、Go のバージョン管理のために外部ツールを別途用意、その設定ファイルを管理する必要がでてくるという短所もあります。

方法C: 開発コンテナ

ここまで見てきたように、ローカル環境で Go のバージョンを管理するには様々な方法と、それに伴う IDE 連携などの課題が存在します。

開発コンテナ (Dev Containers) は、これらの問題を根本的に解決する強力なアプローチです。

開発コンテナを使うと、Go の SDK や各種ツール、環境変数など、プロジェクトに必要な環境全体がコンテナイメージとしてコード化されます。VS Code も開発コンテナをアタッチして直接コンテナ上で開発作業ができるため、ローカルマシンの環境に一切依存しません。

  • 開発コンテナの利点:
    • ローカルの Go のバージョンや GOPATH を汚さない。
    • IDE との連携問題が発生しない。
    • プロジェクトに必要なツール一式(Go, Node.js, DB クライアント等)を Dockerfile.devcontainer.json で定義し、チーム全員で完全に同じ環境を共有可能。

本記事で解説している開発コンテナは、まさにこのアプローチを実践するものであり、Go のバージョン管理における様々な課題を回避し、最も再現性の高い開発環境を実現するためのベストプラクティスと言えるでしょう。

ただし、開発コンテナの利用には高性能な PC が必要で、ネイティブ環境よりも負荷が高くなり安定しない傾向があります。そのため、現実的な選択としては、方法A か方法B のどちらかを選択することになります。将来的には方法C を選択する時代が来るかもしれませんが、現時点ではまだ採用できる環境は少ないという印象です。

Go の開発ツールとモジュールの管理

モジュール管理: Go Modules

現在の Go 開発では、Go Modules を使った依存関係の管理が主流です。プロジェクトのルートディレクトリに go.mod ファイルと go.sum ファイルを置くことで、プロジェクトが依存するモジュールとそのバージョンを管理します。

  • go.mod: プロジェクトのモジュールパスと、依存するモジュールの一覧が記述されます。
  • go.sum: 依存モジュールのチェックサムが記録され、依存関係の完全性を保証します。

依存関係の追加と整理:

新しいモジュールを Go アプリのプロジェクトに追加する際の推奨される方法は、まずソースコードに import 文を記述することです。

import "github.com/spf13/viper"

その後、Go アプリのプロジェクトのルートで go mod tidy コマンドを実行します。

go mod tidy

go mod tidy は、ソースコードをスキャンし、必要なモジュールを go.mod に追加し、不要になったモジュールを削除します。これにより、go.mod ファイルを使って、常にコードに必要なパッケージを管理することができます。

依存関係のバージョン更新:

go get コマンドは、主に現在のモジュール(プロジェクト)の 依存関係のバージョンを更新するため に使われます。Go の古いバージョンでは go get はツールのインストールにも使われていましたが、Go 1.16 以降その役割は go install に移管されました。

現在の go get の主な用途は以下の通りです。

# モジュールを最新のパッチバージョンに更新
go get -u github.com/spf13/viper

# モジュールを特定のバージョンに更新
go get github.com/spf13/viper@v1.10.0

今回用意した compose.yaml による開発コンテナを使っている場合、ダウンロードされたモジュールの実体は、先に compose.yaml で設定した名前付きボリューム (gopath-data) にキャッシュされます。これにより、コンテナを破棄・再作成しても、再度モジュールをダウンロードする必要がなく、効率的に開発を進められます。

注意点としては、キャッシュが壊れた場合には、gopath-data ボリュームを初期化する必要があります。そういった場合は、その実体である dvc-go-gemini-gopath-data を削除してから開発コンテナを起動する必要があります。

Go Modules 以前の依存関係管理 (GOPATH):

Go 1.11 で Go Modules が導入される前は、GOPATH という環境変数に基づいた仕組みで依存関係が管理されていました。Go Modules への移行期には、GO111MODULE という環境変数で、どちらのモードで動作させるかを制御していました。

GO111MODULE の値 動作
on 常に Go Modules モードを使用
off 常に従来の GOPATH モードを使用
auto go.mod の有無などに応じて自動判別

Go 1.16 以降、Go Modules はデフォルトで有効になり、GO111MODULE 環境変数は非推奨となりました。現代的な Go の開発でこの変数を意識する必要はほとんどありません。

ただし、非常に古いプロジェクトや、特定のビルド環境では、GO111MODULE=off という設定が残っている場合があります。もしそのようなプロジェクトに関わる機会があれば、それは GOPATH ベースの依存関係管理を行っていることを意味します。その場合は、別途 GOPATH モードでのビルドや依存関係の扱い方について調べることをお勧めします。

開発ツールの管理

デバッガー(delve など)やリンター(golangci-lint など)といった開発ツールも、Go Modules の仕組みを使ってプロジェクトごとにバージョンを管理することが推奨されています。これにより、チーム全員が同じバージョンの開発ツールを使うことが保証されます。

推奨: go.mod の tool ディレクティブ (Go 1.21+):

Go 1.21 から、go.mod ファイル内に tool ディレクティブを記述する方法が、開発ツールを管理するための公式な手法となりました。これにより、ツールの依存関係がプロジェクトのビルドに必要な依存関係から明確に分離されます。

go.mod ファイルでの tool ディレクティブは以下のようになります。

module go.dev.internal/goapp001

go 1.24
toolchain go1.24.5

require (
    // プロジェクトの実行に必要な依存関係
    // 略
)

tool (
  github.com/go-delve/delve/cmd/dlv
  github.com/golangci/golangci-lint/cmd/golangci-lint
)

この方法では、後述する tools.go ファイルを作成する必要がなく、go.mod だけで管理が完結します。

tool ディレクティブにツールを追加するには go get -tool コマンドを使います。

go get -tool github.com/golangci/golangci-lint/cmd/golangci-lint

tool ディレクティブからツールを削除するときは、go mod edit -droptool コマンドを使います。

go mod edit -droptool=github.com/golangci/golangci-lint/cmd/golangci-lint

go.mod ファイルを直接編集してから、go mod tidy コマンドで依存関係を更新することもできます。

tools.go ファイルを使う方法 (Go 1.20 以前):

Go 1.21 より前のバージョンが使われているプロジェクトでは、tools.go というファイルを使って開発ツールを管理するのが一般的でした。現在でも、古いプロジェクトではこの方法が使われています。

この方法では、プロジェクト内に以下のようなファイルを作成します。

//go:build tools

package tools

import (
  _ "github.com/go-delve/delve/cmd/dlv"
)

//go:build tools: このビルドタグにより、このファイルは通常のビルド対象から除外されます。

import 文に _ をつけることで、コード中で直接使わなくても依存関係として go.mod に追加されるようになります。

このファイルを作成または編集した後に go mod tidy を実行すると、これらのツールが依存関係として go.modrequire ディレクティブに記録されます。

ツールの実行:

上記いずれかの方法で go.mod にツールが指定されると、go run を使ってそのツールを利用できます。

go run コマンドは、go.mod に記録されたツールを実行します。

go run github.com/golangci/golangci-lint/cmd/golangci-lint run ./...

ツールのインストール:

go install コマンドは、指定されたバージョンのツールを $GOPATH/bin (または $GOBIN で指定されたディレクトリ) にインストールします。これにより、コマンド名だけでツールを直接実行できるようになります。

Go 1.16 から、go install が実行可能ファイルをインストールするための公式な方法となっています。

go install github.com/golangci/golangci-lint/cmd/golangci-lint

インストール後は、$GOPATH/bin にパスが通っていればコマンド名だけでツールを実行できます。

golangci-lint run ./...

デバッグへの対応

開発コンテナ環境でのデバッグは、いくつかの設定を行うことで、ローカル開発と遜色のない体験を得られます。ここでは、Go のデバッグで最も広く使われている delve を使ったデバッグ方法を解説します。

後で紹介するサンプルの goapp001 では、goapp001.code-workspacelaunch: に、ローカルデバッグ実行用の Launch Go App (Debug) と、リモートでバッグ用の Remote Debug の起動構成を用意してあります。

VS Code 開発コンテナでのローカルデバッグ

VS Code と開発コンテナを組み合わせている場合、デバッグは非常に簡単です。

  1. Go 拡張機能のインストール
  2. .code-workspace 内の launch: 設定追加」もしくは「.vscode/launch.json の作成」
  3. デバッグの開始

devcontainer.json"golang.go" を追加しておけば、コンテナ起動時に自動でインストールされます。

VS Code の「実行とデバッグ」ビューを開き、「launch.json ファイルを作成します」をクリックします。Go を選択すると、デフォルトのデバッグ構成が生成され、.vscode/launch.json ファイルに保存されます。

.vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch Package",
            "type": "go",
            "request": "launch",
            "mode": "auto",
            "program": "${fileDirname}"
        }
    ]
}

ソースコードにブレークポイントを設定し、F5 キーを押すか、デバッグビューの「Launch Package」を実行すると、デバッグが開始されます。ステップ実行、変数の確認、コールスタックの閲覧などが、VS Code の UI 上で直感的に行えます。

なお、筆者は .vscode/launch.json ファイルに設定を保存しておくよりは、VS Code のワークスペースファイルへ設定を保存して Git リポジトリで管理するようにしています。そうした方が、他の開発者と設定共有をしつつ、各自のカスタマイズの自由度を高くできるからです。

リモートデバッグ

VS Code を使わない場合や、実行中のプロセスにアタッチしたい場合は、delve をサーバーモードで起動し、クライアントから接続するリモートデバッグを行います。

  1. delve サーバーの起動
  2. ポートフォワーディング
  3. デバッグクライアントから接続

delve サーバーを起動するには、デバッグ対象のアプリを dlv debug または dlv exec コマンドで起動します。

# dlv を起動し、デバッグサーバーをポート 2345 でリッスンさせる
dlv debug --headless --listen=:2345 --api-version=2

ポートフォワーディングするには、devcontainer.jsonforwardPorts にデバッグ用のポート(この例では 2345)を追加しておくか、VS Code の「ポート」タブで手動でフォワードします。これにより、ホストマシンからコンテナ内のデバッグサーバーにアクセスできるようになります。

デバッグ用のクライアントから接続するには、別のターミナルや、VS Code のデバッグ構成(request: "attach")を使って、localhost:2345 に接続します。

// `dvc-go-gemini.code-workspace` の `launch:` または 
// `launch.json` での起動構成例
{
  "name": "Remote Debug",
  "type": "go",
  "request": "attach",
  "mode": "remote",
  "remotePath": "${workspaceFolder}",
  "port": 2345,
  "host": "127.0.0.1"
}

この方法は、CI/CD 環境でテストが失敗した際に、コンテナに入ってデバッグするといったシナリオでも役立ちます。

ここまでで、Go の開発環境についての説明はおしまいです。

アプリの要件と設計

せっかく Go の開発環境を用意したので、Go で簡単なアプリを作成して動かしてみましょう。また、Go アプリ作成時に知っておきたい基本的な機能の実装について理解しましょう。

アプリの要件

goapp001 は、次の要件を満たすシンプルなコマンドラインアプリとして設計されています。

項目 要件
設定の外部化 ログレベルやログファイルのパスなどの設定を、コードから分離し、設定ファイルや環境変数で変更できること。
環境に応じた動作変更 dev (開発) 環境と prod (本番) 環境で、ログの出力先を切り替えられること。
構造化ロギング ログを JSON 形式で出力し、機械的な処理を容易にすること。
テストカバレッジ 主要なロジックが単体テストでカバーされていること。
コンテナ化 Docker コンテナとしてアプリを実行できること。

なお、「環境に応じた動作変更」については、次の要件も満たします。

  • dev 環境では、コンソールとログファイルの両方に出力すること
  • prod 環境では、ログファイルにのみ出力すること

設計

上記の要件を満たすため、goapp001 は次のパッケージ構成となっています。

パッケージ名 役割/概要
main アプリのエントリーポイント。設定とロガーを初期化と、主要ロジックの実行。
config viper を利用して設定を管理するパッケージ。
logger zerolog を利用してロガーを初期化・提供するパッケージ。
app アプリの主要なビジネスロジック

この構成は、Go言語などでよく見られる、責務を分離し管理しやすくするための一般的なパッケージ構成パターンに基づいています。

  • main: 最小限の初期化と実行制御に徹します。
  • config: 設定の読み込みと提供に特化します。
  • logger: ロギング機能の抽象化と初期化を担います。
  • app: アプリの中核となる処理(ビジネスロジック)を集約します。

また、設定 (config) とロガー (logger) は、sync.Once を利用したシングルトンパターンで実装されており、アプリ全体で単一のインスタンスを安全に共有します。

フォルダ構成

フォルダ構成は次のとおりです。

goapp001/
├── GEMINI.md ... Gemini CLI 向けドキュメント
├── README.md ... 開発者向けドキュメント
├── app.go ... アプリの処理
├── app_test.go ... app.go のテスト用コード
├── conf/ ... 設定ファイル用フォルダ
│   └── config.yaml
├── config/ ... 設定関連の処理用
│   ├── config.go
│   └── config_test.go
├── docker/ ... Docker 用フォルダ
│   ├── README.md
│   ├── cicd/
│   ├── demo/
│   └── prod/
├── go.mod ... Go モジュール用ファイル
├── go.sum ... Go モジュールロック用ファイル
├── goapp001.code-workspace ... VS Code ワークスペースファイル
├── log/ ... ログ出力用フォルダ
│   └── app.log
├── logger/ ... ログ処理用
│   └── logger.go
├── main.go ... メイン処理
└── sample/ ... サンプル用フォルダ
    ├── config.dev.yaml ... 開発向け設定ファイル
    ├── config.prod.yaml ... 運用向け設定ファイル
    └── config.yaml ... デフォルト設定ファイル

アプリ開発の準備

アプリ開発をするにあたって必要な準備をします。cd コマンドでカレントフォルダを ${PROJECT_DIR}/goapp001 とします。

cd ${PROJECT_DIR}/goapp001

それから cp コマンドで設定ファイルをサンプルからコピーすることと、go mod download で必要なモジュールをダウンロードしておきます。

cp sample/config.yaml conf/
go mod download

準備ができたら VS Code で goapp001.code-workpsace を開いて起動します。

code ./goapp001.code-workpsace

Go モジュールによる依存関係管理

この Go のプロジェクトでは、プロジェクトが依存するモジュールについては Go モジュールを使って管理します。そのため、go.mod ファイルを作成する必要があります。

go.mod

go.mod は、プロジェクトのモジュールパス、使用する Go のバージョン、そして直接的・間接的に依存するモジュールとそのバージョンを定義します。

goapp001/go.mod(抜粋)
module go.dev.internal/goapp001

go 1.24

toolchain go1.24.5

require (
    github.com/rs/zerolog v1.34.0
    github.com/spf13/viper v1.21.0
    github.com/stretchr/testify v1.11.1
)

// ... (indirect dependencies)

tool (
    github.com/go-delve/delve/cmd/dlv
    github.com/golangci/golangci-lint/cmd/golangci-lint
)

module には、このプロジェクトのモジュールパスを定義します。通常は、他のモジュールと衝突しないようなリポジトリのパスなどが使われます。ここでは、go.dev.internal/goapp001 としました。

go には、このコードが期待する Go の最小バージョンを指定します。

toolchain には、使用する Go のツールチェインのバージョンを指定します。

require には、プロジェクトが直接依存するモジュールと、そのバージョンをリストします。goapp001 では、次のモジュールを使用しています。

  • ロギングライブラリの zerolog
  • 設定管理ライブラリの viper
  • テストのアサーションライブラリの testify

tool には、go run で実行可能な開発ツールを指定します。ここではデバッガの dlv とリンターの golangci-lint を指定しています。tool を指定することで、チーム内で使用するツールを統一できます。

go.sum

go.sum は、依存関係にあるモジュール(直接・間接含む)のバージョンと、その内容のハッシュ値(チェックサム)を記録するロックファイルです。これにより、go buildgo test を実行する際に、依存モジュールが改ざんされていないことを保証し、ビルドの再現性を高めます。

goapp001/go.sum (抜粋)
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
(略)

アプリのコアロジック

main.go: エントリーポイント

main.go はアプリの起動処理を受け持ちます。責務を最小限に保ち、設定とロガーを初期化して、主要なロジック (Run 関数) を呼び出すことに専念します。

goapp001/main.go
package main

import (
    "go.dev.internal/goapp001/config"
    "go.dev.internal/goapp001/logger"
)

func main() {
    // Initialize config and logger
    cfg := config.GetConfig()
    log := logger.GetLogger()

    log.Info().Msgf("Starting application with config: %+v", cfg)

    Run(log)
}

app.go: 主要ロジック

app.go には、アプリの本体である Run 関数が定義されています。Run 関数は zerolog.Logger を引数として受け取り、依存性を注入(Dependency Injection)する形になっています。これにより、テスト時にログの出力をキャプチャすることが容易になります。

goapp001/app.go
package main

import "github.com/rs/zerolog"

// Run executes the main application logic. It logs several messages at different levels
// to demonstrate the logging setup.
func Run(logger zerolog.Logger) {
    logger.Info().Msg("Application is running.")

    logger.Debug().Msg("This is a debug message from app.go.")
    logger.Info().Msg("This is an info message from app.go.")
    logger.Warn().Str("user", "Gopher").Msg("This is a warning message from app.go.")

    logger.Info().Msg("Application finished.")
}

この Run 関数は、アプリの主要ロジックにおけるロギングの実行を担っています。

外部から初期化・設定済みの zerolog.Logger インスタンスを受け取り、それを使用してアプリの動作状況を段階的に記録します。これは、ロギングの実装をビジネスロジックから分離する依存性注入の一般的なパターンです。

ログの出力内容とレベル:

関数内では、以下のコードに見られるように、処理の開始から終了まで、さまざまなログレベルでメッセージが出力されます。

ログレベル コード例 目的・用途
Info logger.Info() アプリの開始終了、主要な処理の進行状況など、追跡に必要な一般的な情報を記録
Warn logger.Warn() エラーではないが、注意が必要な状態や、想定外の挙動を記録
Debug logger.Debug() 開発時や詳細なトラブルシューティングに使用

Debug レベルものは、通常、本番環境では出力されないように設定します。

zerolog の特徴と利点:

このコードで利用されている zerolog は、Go 言語における構造化ロギングライブラリです。

構造化ロギングの機能により、次のようなログ出力が実現できます。

  • ログは、単なる文字列ではなく JSON形式 などの構造化された形式で出力されます。
  • これにより、logger.Warn().**Str("user", "Gopher")** のように、メッセージに加えて、処理に関連するメタデータ(例: user: Gopher)を簡単に含めることができます。
  • 構造化されているため、ログ集積・解析ツール(Elasticsearch、Splunk など)での検索や解析が容易になります。

また、ログレベルによる制御もできます。Info, Debug, Warn などのレベルを使い分けることで、アプリの実行環境(開発、ステージング、本番)に応じて必要な重要度のメッセージだけをフィルタリングして出力できます。

ロガーのセットアップ (logger/logger.go)

logger/logger.go は、zerolog を用いたロガーのセットアップを行います。GetLogger 関数はシングルトンパターンで実装されており、アプリ全体で一意のロガーインスタンスを共有します。

goapp001/logger/logger.go
package logger

import (
    // ...
)

var (
    once   sync.Once
    logger zerolog.Logger
)

// GetLogger returns the singleton logger instance.
func GetLogger() zerolog.Logger {
    once.Do(func() {
        cfg := config.GetConfig()

        // ... (ログレベルのパース)

        // ... (ログファイルの準備)

        var output io.Writer = file
        env := os.Getenv("APP_ENV")
        if env != "prod" {
            consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr}
            output = zerolog.MultiLevelWriter(consoleWriter, file)
        }

        logger = zerolog.New(output).With().Timestamp().Logger().Level(level)
        logger.Info().Msg("Logger initialized successfully.")
    })
    return logger
}

この実装の重要な点は、環境変数 APP_ENV の値に応じて出力先を切り替えている部分です。

  • APP_ENV"prod" でない場合: zerolog.MultiLevelWriter を使い、コンソール (os.Stderr) とログファイルの両方に出力します。
  • APP_ENV"prod" の場合: ログファイルにのみ出力します。

これにより、開発中はコンソールで手軽にログを確認しつつ、本番環境ではファイルに永続化するという、一般的な要件を実現しています。

テストへの対応方法

Go は標準で強力なテスト機能を提供します。goapp001 では、testing パッケージと stretchr/testify ライブラリを組み合わせてテストを記述しています。

app_test.go: 主要ロジックのテスト

app_test.go では、Run 関数の振る舞いをテストします。Run 関数がロガーを引数に取る設計になっているため、テスト内で bytes.Buffer に書き込むためのロガーを注入できます。これにより、実際に出力されたログの内容を検証することが可能です。

// goapp001/app_test.go
package main

import (
    "bytes"
    "strings"
    "testing"

    "github.com/rs/zerolog"
    "github.com/stretchr/testify/assert"
)

func TestRun(t *testing.T) {
    // Setup: Create a buffer to capture log output and a logger that writes to it.
    var logBuffer bytes.Buffer
    testLogger := zerolog.New(&logBuffer).Level(zerolog.DebugLevel)

    // Execute the Run function with the test logger.
    Run(testLogger)

    // Get the captured log output as a string.
    logOutput := logBuffer.String()

    // Assert: Check if the output contains the expected log messages.
    assert.True(t, strings.Contains(logOutput, "Application is running."), "...")
    // ... (他のアサーション)
}

config_test.go: 設定読み込みのテスト

config_test.go では、設定読み込みロジックが正しく動作することを様々なシナリオでテストしています。

  • 設定ファイルから正しく値が読み込まれるか
  • 環境変数が設定ファイルの値を正しく上書きするか
  • 設定ファイルが存在しない場合にデフォルト値が使われるか

テストケースごとに t.Run を使ってサブテストを定義し、resetSingleton 関数でシングルトンの状態をリセットすることで、各テストが独立して実行されるように工夫されています。

goapp001/config/config_test.go
package config

import (
    // ...
)

// resetSingleton is used to reset the sync.Once and the config instance for testing.
func resetSingleton() {
    once = sync.Once{}
    cfg = nil
    viper.Reset()
}

func TestGetConfig(t *testing.T) {
    t.Run("loads config from file", func(t *testing.T) {
        // ...
    })

    t.Run("overrides with environment variables", func(t *testing.T) {
        resetSingleton()
        // ...
        t.Setenv("APP_LEVEL", "env_level")

        config := GetConfig()
        assert.Equal(t, "env_level", config.Level)
    })

    // ... (他のテストケース)
}

環境切替えに対応する設定管理 (config/config.go)

config/config.go は、viper ライブラリを使って設定を管理します。viper は、ファイル、環境変数、デフォルト値など、複数のソースから設定を読み込み、優先順位に従ってマージしてくれる強力なライブラリです。

// goapp001/config/config.go
package config

import (
    // ...
)

// Config holds all configuration for the application.
type Config struct {
    Level   string `mapstructure:"level"`
    LogDir  string `mapstructure:"logdir"`
    LogFile string `mapstructure:"logfile"`
}

// ... (シングルトンの実装)

// loadConfig reads configuration from file and environment variables.
func loadConfig() {
    v := viper.New()

    // 1. Set default values
    v.SetDefault("level", "info")
    // ...

    // 2. Load from config file
    v.SetConfigName("config")
    v.AddConfigPath("conf")
    v.SetConfigType("yaml")
    if err := v.ReadInConfig(); err != nil {
        // ... (エラーハンドリング)
    }

    // 3. Load from environment variables
    v.SetEnvPrefix("APP")
    v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
    v.AutomaticEnv()

    // Unmarshal config
    if err := v.Unmarshal(&cfg); err != nil {
        log.Fatal().Err(err).Msg("Failed to unmarshal config")
    }
}

viper の設定読み込みは以下の優先順位で行われます。

  1. 環境変数: APP_ プレフィックスを持つ環境変数 (例: APP_LEVEL) が最も優先されます。
  2. 設定ファイル: conf/config.yaml ファイル。
  3. デフォルト値: SetDefault で設定された値。

この仕組みにより、開発者は conf/config.yaml をベースに使いつつ、CI/CD 環境や本番環境では環境変数で値を上書きするという柔軟な運用が可能になります。

ドキュメント

README.md には、プロジェクトの概要、セットアップ方法、実行方法、テスト方法など、開発者が最初に知るべき情報を記載しています。

GEMINI.md には、AI アシスタント(Gemini)がプロジェクトを理解し、開発を支援するための指示書です。プロジェクトの概要、ビルドやテストの方法、コーディング規約などを記述しています。

今後は、AI アシスタントの利用が標準的になるでしょうから、こういったドキュメントを用意しておくことが開発効率に影響してきます。また、AI アシスタント用のドキュメントと、開発者向けのドキュメントは重なる部分が多いですが、対象者が違うため両方とも必要となります。こういったドキュメントのメンテナンスを AI アシスタントは得意とするので、ドキュメント作成は積極的に AI アシスタントに任せると良いでしょう。

Docker の利用

docker ディレクトリには次の3つの環境に対応した Dockerfilecompose.yaml が含まれています。

  • cicd (テスト用)
  • demo (デモ用)
  • prod (本番用)

テスト用は、CI/CD パイプラインで go test ./... を実行するための環境です。

開発用は、ローカルのソースコードをコンテナにマウントして実行します。これにより、コードの変更が即座に反映され、迅速な開発サイクルが実現します。

本番用は、マルチステージビルドを利用して、Go のソースコードを含まない最小限の実行イメージ (distroless) を作成します。これにより、セキュリティが向上し、イメージサイズが大幅に削減されます。

このように、Docker の利用については、目的と用途によって使い分けが必要になります。一言で「開発に Docker を利用する」といっても、Docker についての知識の深さによってイメージするものが変わってきます。いずれにせよ、Docker を使うと様々なアプリ実行環境を手軽に用意して利用することができるようになります。積極的に活用したいところです。

テスト用の Docker

この設定は、CI/CD 環境やローカルの開発環境でテストを実行するときのものです。

テスト用の compose.yaml は次のようになります。

docker/cicd/compose.yaml
name: goapp001-cicd
services:
  test:
    build:
      context: ../../
      dockerfile: ./docker/cicd/Dockerfile
    image: goapp001-cicd:1.24.5
    container_name: goapp001-cicd
    hostname: goapp001-cicd
    environment:
      GOTOOLCHAIN: ${GOTOOLCHAIN:-local}

Docker イメージをビルドせずに、ソースコードの修正をしたものをテストしたい場合はバインドマウントが必要です。そのときに使う compose-bind.yaml も次のように用意しておきます。

docker/cicd/compose-bind.yaml
name: goapp001-cicd
services:
  test:
    volumes:
      - type: bind
        source: ${LOCAL_WORKSPACE_FOLDER:-../../..}/goapp001
        target: /app

テスト用の Dockerfile は次のようになります。go test コマンドを実行します。

docker/cicd/Dockerfile
FROM golang:1.24.5-bullseye
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
CMD ["go", "test", "./..."]

最初に Docker イメージをビルドします。

docker compose -f docker/cicd/compose.yaml build

Docker でテストを実行するには、次のコマンドを実行します。

docker compose -f docker/cicd/compose.yaml run --rm test

実行例は次のようになります。

$ docker compose -f docker/cicd/compose.yaml run --rm test
ok      go.dev.internal/goapp001        0.002s
ok      go.dev.internal/goapp001/config 0.003s
?       go.dev.internal/goapp001/logger [no test files]

コードを修正した場合は、再度 Docker イメージをビルドします。コードの修正が反映されずにビルドされてしまう場合は、次のように --no-cache をつけて、ビルドキャッシュを無効化してビルドします。

docker compose -f docker/cicd/compose.yaml build --no-cache

もしくは、バインドマウントをしながら Docker でテストを実行するために、次のコマンドを実行します。

docker compose \
  -f docker/cicd/compose.yaml \
  -f docker/cicd/compose-bind.yaml \
  run --rm test

デモ用の Docker

この設定は、ローカルの開発環境やデモ環境でアプリを実行するためのものです。ローカルのソースコードをマウントするため、ソースコードの変更の反映がしやすいです

デモ用の compose.yaml は次のようになります。

demo/compose.yaml
name: goapp001-demo
services:
  app:
    build:
      context: ../../
      dockerfile: ./docker/demo/Dockerfile
    image: goapp001-demo:1.24.5
    container_name: goapp001-demo
    hostname: goapp001-demo
    volumes:
      - type: bind
        source: ${LOCAL_WORKSPACE_FOLDER:-../..}
        target: /app
    environment:
      - APP_ENV=${APP_ENV:-dev}

デモ用の Dockerfile は次のようになります。go run コマンドを実行します。

demo/Dockerfile
FROM golang:1.24.5-bullseye
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
CMD ["go", "run", "."]

最初に Docker イメージをビルドします。

docker compose -f docker/demo/compose.yaml build

アプリを実行する方法は次のとおりです。デフォルトは dev モードで起動します。

docker compose -f docker/demo/compose.yaml run --rm app

実行例は次のとおりです。

$ docker compose -f docker/demo/compose.yaml run --rm app
{"level":"info","time":"2025-10-19T06:32:01Z","message":"Configuration loaded successfully. Log level: info"}
6:32AM INF Logger initialized successfully.
6:32AM INF Starting application with config: &{Level:info LogDir:log LogFile:app.log}
6:32AM INF Application is running.
6:32AM INF This is an info message from app.go.
6:32AM WRN This is a warning message from app.go. user=Gopher
6:32AM INF Application finished.

compose.yaml では APP_ENV=dev がデフォルトで設定されていますが、実行時に変数を指定することで上書きできます。

次の例では prod モードで実行しています。

docker compose -f docker/demo/compose.yaml run --rm -e APP_ENV=prod app

prod モードの場合は、動作確認をするには log フォルダに出力されているログファイルの内容を確認します。これまでの手順に従っている場合は app.log にログが出力されています。

実行例は次のとおりです。

$ cat log/app.log
{"level":"info","time":"2025-10-19T06:21:35Z","message":"Logger initialized successfully."}
{"level":"info","time":"2025-10-19T06:21:35Z","message":"Starting application with config: &{Level:info LogDir:log LogFile:app.log}"}
{"level":"info","time":"2025-10-19T06:21:35Z","message":"Application is running."}
{"level":"info","time":"2025-10-19T06:21:35Z","message":"This is an info message from app.go."}
{"level":"warn","user":"Gopher","time":"2025-10-22T06:21:35Z","message":"This is a warning message from app.go."}
{"level":"info","time":"2025-10-19T06:21:35Z","message":"Application finished."}

本番用の Docker

この設定は、distroless ベースイメージを使用したマルチステージビルドにより、最小限でセキュアな本番イメージをビルドするためのものです。

本番用の compose.yaml は次のようになります。バインドマウントを使わずに、Docker イメージをそのまま実行するようにしてあります。そのままだとログファイルがコンテナ内で肥大化するため、実際に使う場合はログの管理をどうするか決めて利用する必要があります。

docker/prod/compose.yaml
name: goapp001-prod

services:
  app:
    build:
      context: ../../
      dockerfile: ./docker/prod/Dockerfile
    image: goapp001-prod:1.24.5
    container_name: goapp001-prod
    hostname: goapp001-prod
    environment:
      - APP_ENV=${APP_ENV:-prod}

本番用の Dockerfile は次のようになります。マルチステートビルドを使って go build コマンドで実行ファイルを生成し、それを最終イメージへコピーして実行できるようにしています。

使用するベースイメージについて、実行時のタグにある debian11 を考慮して、ビルド時は Debian 11 のコードネームである bullseye を含むイメージを使うようにしています。

docker/prod/Dockerfile
# ---- Build Stage ----
FROM golang:1.24.5-bullseye AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /goapp .

# ---- Final Stage ----
FROM gcr.io/distroless/static-debian11
WORKDIR /app
ENV APP_ENV prod
COPY --from=builder /goapp .
COPY conf/ ./conf/
CMD ["/app/goapp"]

本番用の Docker の環境設定:

本番用の Docker イメージは、Dockerfile 内の ENV 命令により、デフォルトで APP_ENV=prod に設定されています。

docker/prod/compose.yaml もこの環境変数を設定します。compose.yaml の値は Dockerfile のデフォルト値を上書きするため、イメージを再ビルドすることなく簡単に環境を変更できます。例えば、デバッグのために本番イメージを開発モードで実行したい場合、compose.yaml の値を dev に変更できます。

compose.yaml で指定されている APP_ENV の値は、compose.yaml と同じフォルダに .env ファイルがある場合は、そのファイル内で指定した値で上書き可能です。そのため、compose.yamlAPP_ENV の値を変更する一番簡単な方法は、docker/prod/.env を次のように用意してからコンテナを起動するということになります。

echo "APP_ENV=dev" > docker/prod/.env

この後の本番用の Docker イメージの動作確認について、コンソール出力がないと確認ができないので、この docker/prod/.env を用意した状態にしておきます。

本番用の Docker の使用方法:

本番用の Docker を使用するには、次の手順を踏みます。

  1. イメージのビルド
  2. サービスの起動

これにより、アプリが実行されます。

docker compose -f docker/prod/compose.yaml build
docker compose -f docker/prod/compose.yaml up

なお、今回のアプリでログを確認するには dev モードで起動するのが手軽です。実行例は次のようになります。

$ echo "APP_ENV=dev` > `docker/prod/.env`
$ docker compose -f docker/prod/compose.yaml up
[+] Running 1/1
 ✔ Container goapp001-prod  Recreated                                                                                                                                                                                                               0.1s 
Attaching to goapp001-prod
goapp001-prod  | {"level":"info","time":"2025-10-19T06:40:03Z","message":"Configuration loaded successfully. Log level: info"}
goapp001-prod  | 6:40AM INF Logger initialized successfully.
goapp001-prod  | 6:40AM INF Starting application with config: &{Level:info LogDir:log LogFile:app.log}
goapp001-prod  | 6:40AM INF Application is running.
goapp001-prod  | 6:40AM INF This is an info message from app.go.
goapp001-prod  | 6:40AM WRN This is a warning message from app.go. user=Gopher
goapp001-prod  | 6:40AM INF Application finished.
goapp001-prod exited with code 0

最終行にあるように code 0 で終了していたら成功です。

アプリの実行は終了していますが、コンテナの削除などはされていません。docker compose down コマンドでサービスを終了することで、Docker が使用したコンピュータリソースの解放ができます。

docker compose -p goapp001-prod down

その他のファイル

その他のファイルについて、簡単に説明しておきます。

sample フォルダには、devprod 環境で利用できる設定ファイルのサンプルが置いてあります。conf/config.yaml の内容を更新するときの参考になるはずです。

dev 用は次のとおりです。

sample/config.dev.yaml
level: debug
logdir: log
logfile: dev.log

prod 用は次のとおりです。

sample/config.prod.yaml
level: info
logdir: log
logfile: production.log

初期設定は次のとおりです。

sample/config.yaml
level: info
logdir: log
logfile: app.log

.gitignore は、Git の管理対象から除外するファイル(ログファイルなど)を指定するものです。

log/*
conf/config.yaml
!.gitkeep

goapp001.code-workspace は、VS Code のワークスペースを定義するファイルです。プロジェクトを開いた際に推奨される拡張機能や設定を指定することができます。ここでは、Go の開発で使われる拡張機能の指定などをしています。

{
  "folders": [
    {
      "path": "."
    }
  ],
  "settings": {
    // Enable the Go language server for autocompletion, navigation, etc.
    "go.useLanguageServer": true,
    // Format code on save.
    "editor.formatOnSave": true,
    "[go]": {
      "editor.defaultFormatter": "golang.go"
    }
  },
  "extensions": {
    "recommendations": [
      // Recommend the official Go extension.
      "golang.go"
    ]
  },
  "launch": {
    "version": "0.2.0",
    "configurations": [
      {
        "name": "Launch Go App (Debug)",
        "type": "go",
        "request": "launch",
        "mode": "debug",
        "program": "${workspaceFolder}",
        "env": {
          // APP_ENV can be set here, for example:
          // "APP_ENV": "development"
        }
      },
      {
        "name": "Remote Debug",
        "type": "go",
        "request": "attach",
        "mode": "remote",
        "remotePath": "${workspaceFolder}",
        "port": 2345,
        "host": "127.0.0.1"
      }
    ]
  }
}

ローカルでのデバッグ実行時に利用する Launch Go App (Debug) の起動構成は、前に説明した、自動生成されるものとは少し違っています。こちらでは、環境変数 APP_ENV を指定できるようにしてあります。また、mode はデバッグ用に固定し、programmain.go を含むフォルダのパスを ${workspaceFolder} というワークスペースのフォルダを表す変数で指定しました。

まとめ

本記事では、VS Code と Docker (Dev Containers) を活用して、再現性が高く、チームで共有しやすいモダンな Go 開発環境を構築する方法を解説しました。

環境構築編のポイントは次のとおりです。

  • Dev Containers の利点: なぜ開発コンテナが Go 開発の最初のハードルを下げ、チーム開発を加速させるのかを説明しました。
  • 環境のコード化: .devcontainer フォルダ内の devcontainer.json, compose.yaml, Dockerfile を通じて、開発環境をコードとして管理する方法を示しました。
  • Go のバージョンとツールの管理: Go 本体のバージョン管理戦略や、go mod を使ったリンターやデバッガーなどの開発ツールのバージョンをプロジェクトに固定する方法を解説しました。
  • デバッグ: VS Code のデバッガーを開発コンテナ内で利用し、ローカル開発と遜色のないデバッグ体験を実現する手順を紹介しました。

実践アプリ編については、サンプルアプリ goapp001 を通じて、Go での実践的なアプリケーション設計パターンを学びました。ポイントは次のとおりです。

  • 関心の分離: config, logger, app のように責務をパッケージに分割する設計
  • 柔軟な設定管理: viper を利用し、設定ファイルと環境変数で動作を切り替える方法
  • 構造化ロギング: zerolog を用いて、解析しやすい JSON 形式のログを出力する実装
  • テスト: 標準の testing パッケージとテーブル駆動テストを組み合わせたテストコードの実装
  • Docker 化: 開発用、テスト用、そして distroless イメージを活用した本番用のマルチステージビルドまで、用途に応じた Docker 環境の構築方法

Go での開発を始める際、環境構築は面倒な作業になりがちですが、本記事で紹介した開発コンテナ(Dev Containers)のアプローチを取り入れることで、その手間を大幅に削減し、より本質的なアプリケーション開発に集中できるようになります。この記事が、あなたの Go 開発の第一歩を力強くサポートできれば幸いです。

Discussion