Goのビルドとデプロイ
.NET (C#) の経験がある開発者向けに、シングルバイナリ、クロスコンパイル、Dockerマルチステージビルド、CI/CDを解説します。dotnet publish に相当する作業が go build の 1 コマンドで済み、ランタイムのインストールが不要な静的バイナリを、極小のコンテナイメージに載せてデプロイできるのが Go の大きな強みです。ここでは、ビルドフラグ、バージョン埋め込み、クロスコンパイル、コンテナ化、GitHub Actions から Azure へのデプロイまでの流れを整理します。
1. このドキュメントで扱うこと
- go build のフラグ (-ldflags など)
- CGO_ENABLED と静的リンク
- 極小Dockerイメージの作り方
- GitHub Actions / Azure へのデプロイ
go build のフラグ (-ldflags など)
go build は、指定したパッケージ (通常は main パッケージ) から実行ファイルを 1 つ生成します。よく使うフラグは次のとおりです。
| フラグ | 内容 |
|---|---|
-o パス | 出力ファイル名またはディレクトリを指定する |
-trimpath | バイナリからビルド環境のファイルパスを取り除く。再現性が上がり、ローカルのパスも漏れない |
-ldflags '...' | リンカーへのフラグ。-s -w でバイナリを小さくし、-X で文字列変数を埋め込む |
-tags 名前 | ビルドタグを有効にする (//go:build 制約の切り替え) |
-race | データ競合検出を有効にする (主にテスト用。本番ビルドには通常使わない) |
-buildvcs=false | バージョン管理情報の埋め込みを無効にする |
-ldflags の主な使い方です。
-sはシンボルテーブルを、-wは DWARF デバッグ情報を省きます。バイナリは小さくなりますが、デバッガーでの調査は難しくなります。-X パッケージのインポートパス.変数名=値は、文字列型の変数の値をリンク時に設定します。バージョンやコミットハッシュの埋め込みに使います。対象はconstではなくvarで、初期化式を持たないか、定数の文字列でなければなりません。mainパッケージの変数はmain.versionと書きます。
CGO_ENABLED と静的リンク
Go のバイナリは、通常の Go コードだけであれば依存ライブラリなしの静的バイナリになります。ただし、cgo (C ライブラリの呼び出し) が有効だと、C ライブラリ (glibc など) に動的リンクされることがあります。CGO_ENABLED=0 を指定すると cgo を使わず、純粋な Go のみでビルドします。
- 標準ライブラリの
netやos/userには cgo を使う実装があり、Linux では cgo が有効なビルドで libc への動的リンクになることがあります。CGO_ENABLED=0では純 Go の実装に切り替わります。 - C コンパイラーがない環境や、異なる OS / アーキテクチャ向けのクロスコンパイルでは、cgo は既定で無効になります。
- SQLite の
mattn/go-sqlite3のように cgo を必須とするライブラリは、CGO_ENABLED=0ではビルドできません。cgo を使わない実装 (modernc.org/sqliteなど) を選ぶか、glibc を含むイメージで動かす必要があります。
極小Dockerイメージの作り方
静的バイナリなら、ビルド用イメージと実行用イメージを分けるマルチステージビルドで、実行イメージを極小にできます。
| 実行用イメージ | 特徴 |
|---|---|
scratch | 完全に空。CA 証明書、タイムゾーン情報、/etc/passwd などが必要なら自分で COPY する |
gcr.io/distroless/static-debian12 | CA 証明書、タイムゾーン情報、非 root ユーザーを含む最小のイメージ。静的バイナリ向け |
gcr.io/distroless/base-debian12 | glibc を含む。cgo を使う動的リンクのバイナリ向け |
alpine | シェルとパッケージマネージャーを持つ小さなイメージ。musl libc のため cgo 利用時は注意が必要 |
通常は distroless/static の :nonroot タグが扱いやすい選択です。シェルがないためデバッグしにくい場合は、シェルを含む :debug タグを一時的に使います。
GitHub Actions / Azure へのデプロイ
典型的な流れは、テスト、コンテナイメージのビルド、レジストリへのプッシュ、デプロイの順です。Azure では、Azure Container Registry (ACR) にイメージを置き、Azure Container Apps、AKS、App Service for Containers などで実行します。
- GitHub Actions から Azure への認証は、長期間有効なシークレットではなく、OpenID Connect (OIDC) によるフェデレーション認証 (
azure/login) を使います。 - イメージにはコミット SHA などの不変なタグを付け、
latestだけでデプロイしないようにします。 - Go のバイナリは起動が速く、メモリー使用量も小さいため、コンテナのスケールアウトと相性が良好です。
2. .NET との対応表
| .NET (C#) | Go |
|---|---|
| dotnet publish (self-contained / AOT) | go build (デフォルトで静的バイナリ) |
| RID 指定 | GOOS / GOARCH |
| mcr.microsoft.com/dotnet/aspnet | scratch / distroless / alpine |
上の表を補う対応関係です。
| .NET (C#) | Go |
|---|---|
dotnet publish -c Release | go build (最適化は常に有効で、構成の区別はない) |
-r linux-x64 --self-contained | GOOS=linux GOARCH=amd64 go build |
PublishSingleFile | 既定でシングルバイナリ |
PublishTrimmed / Native AOT | 不要 (-ldflags '-s -w' でシンボル情報を除去する程度) |
AssemblyInformationalVersion / -p:Version= | -ldflags '-X main.version=...' |
Deterministic / PathMap | -trimpath |
dotnet publish の出力ディレクトリ | -o bin/app |
mcr.microsoft.com/dotnet/sdk (ビルド) と aspnet (実行) | golang (ビルド) と distroless/static (実行) |
dotnet build のインクリメンタルビルド | ビルドキャッシュ (go env GOCACHE) が自動で効く |
#if DEBUG / 条件付きコンパイル | ビルドタグ (//go:build) と -tags |
3. C# 開発者がつまずきやすいポイント
ランタイムのインストールもサイズ最適化の設定も不要
.NET では、フレームワーク依存、自己完結、Single File、Trimming、Native AOT と選択肢が多く、組み合わせで挙動が変わります。Go は、いつでも自己完結のネイティブバイナリです。そのため、実行環境に Go をインストールする必要はなく、コピーして実行するだけです。
クロスコンパイルは環境変数だけ
別 OS / アーキテクチャ向けのバイナリは、GOOS と GOARCH を指定するだけで、ビルドしているマシン上で作れます。対応する組み合わせは go tool dist list で確認できます。WSL や CI の Linux 上で Windows 向けの .exe を作る場合も、追加の SDK は不要です。
cgo が有効だと静的バイナリにならない
意図せず cgo が有効になり、distroless/static や scratch で exec format error や no such file or directory (動的リンカーが見つからない) で起動しない問題が起きます。コンテナ向けのビルドでは CGO_ENABLED=0 を明示するのが安全です。ldd bin/app や file bin/app で動的リンクかどうかを確認できます。
-X で値が入らない
-X は次の場合に効きません。静かに無視されるため、気づきにくい点に注意してください。
- 対象が
constである、または文字列以外の型である - 変数名のパスが誤っている (
main以外のパッケージは、完全なインポートパスで指定する) - 初期化式が関数呼び出しなど、定数でない
// 動く: 文字列の var
var version = "dev"
// 動かない: const は書き換えられない
const version = "dev"
scratch では CA 証明書とタイムゾーンがない
scratch は文字どおり空のため、HTTPS クライアントが証明書検証に失敗したり、time.LoadLocation("Asia/Tokyo") がエラーになったりします。distroless/static はこれらを含みます。scratch を使う場合は、ビルダーから /etc/ssl/certs/ca-certificates.crt を COPY し、タイムゾーンは import _ "time/tzdata" でバイナリに埋め込めます (バイナリが約 450 KB 増えます)。
シェルがないイメージではデバッグと HEALTHCHECK が難しい
distroless にはシェルや curl がないため、docker exec でログインしたり、HEALTHCHECK CMD curl ... を書いたりできません。ヘルスチェックはオーケストレーター (Kubernetes の probe、Azure Container Apps の probe) に任せます。調査が必要なときは :debug タグを使います。
ビルドキャッシュを活かす Dockerfile の順序
go.mod と go.sum だけを先にコピーして go mod download を実行し、その後にソースをコピーします。依存関係が変わらない限り、ダウンロードの層が再利用されます。これは dotnet restore を先に実行する、.NET の Dockerfile の定石と同じ考え方です。
バージョンやコミット情報は自分で埋め込む
AssemblyInformationalVersion のような仕組みは標準では動きません。リリース時に、git describe の結果などを -ldflags -X で渡します。なお、Go 1.18 以降は runtime/debug.ReadBuildInfo() で VCS のリビジョンなどを読み取れますが、タグ名のようなバージョン文字列は、-X で渡す方が簡単です。
4. コード例
例 1: バージョンを埋め込む (C# との比較)
C#:
// .csproj で埋め込む
// dotnet publish -c Release -p:Version=1.2.3
var version = typeof(Program).Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
Console.WriteLine(version);
Go:
package main
import (
"fmt"
"runtime/debug"
)
// リンク時に -ldflags "-X main.version=..." で上書きされる
var (
version = "dev"
commit = "unknown"
)
func main() {
fmt.Println("version:", version, "commit:", commit)
// VCS 情報が埋め込まれている場合は読み取れる
if info, ok := debug.ReadBuildInfo(); ok {
for _, s := range info.Settings {
if s.Key == "vcs.revision" {
fmt.Println("vcs.revision:", s.Value)
}
}
}
}
VERSION=$(git describe --tags --always --dirty)
COMMIT=$(git rev-parse --short HEAD)
CGO_ENABLED=0 go build \
-trimpath \
-ldflags "-s -w -X main.version=${VERSION} -X main.commit=${COMMIT}" \
-o bin/app ./cmd/app
例 2: クロスコンパイル
# 使えるターゲットの一覧
go tool dist list
# Linux (amd64)
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o bin/app-linux-amd64 ./cmd/app
# Linux (arm64: Graviton や Apple Silicon 上の Docker など)
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o bin/app-linux-arm64 ./cmd/app
# Windows (amd64)
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o bin/app-windows-amd64.exe ./cmd/app
# macOS (arm64)
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o bin/app-darwin-arm64 ./cmd/app
.NET の dotnet publish -r linux-x64 --self-contained に相当しますが、SDK やランタイムパックの追加インストールは不要です。
例 3: Makefile
APP := app
PKG := ./cmd/app
VERSION ?= $(shell git describe --tags --always --dirty)
COMMIT ?= $(shell git rev-parse --short HEAD)
LDFLAGS := -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT)
.PHONY: build test lint docker clean
build:
CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -o bin/$(APP) $(PKG)
test:
go test -race -cover ./...
lint:
golangci-lint run
docker:
docker build --build-arg VERSION=$(VERSION) --build-arg COMMIT=$(COMMIT) -t $(APP):$(VERSION) .
clean:
rm -rf bin
Makefile のレシピは、行頭をタブで字下げする必要があります。
例 4: マルチステージ Dockerfile (distroless)
# syntax=docker/dockerfile:1
# --- ビルドステージ ---
FROM --platform=$BUILDPLATFORM golang:1.25 AS build
WORKDIR /src
# 依存関係を先に取得してレイヤーキャッシュを活かす
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
ARG TARGETOS
ARG TARGETARCH
ARG VERSION=dev
ARG COMMIT=unknown
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -trimpath \
-ldflags "-s -w -X main.version=${VERSION} -X main.commit=${COMMIT}" \
-o /out/app ./cmd/app
# --- 実行ステージ ---
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]
--platform=$BUILDPLATFORMとTARGETOS/TARGETARCHにより、docker buildx build --platform linux/amd64,linux/arm64で、ビルドは速いホストのアーキテクチャのまま、複数アーキテクチャのイメージを作れます。scratchを使う場合は、次のように CA 証明書を明示的にコピーします。
FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]
.dockerignore に .git、bin、ローカルの設定ファイルなどを入れると、ビルドコンテキストが小さくなります。
C# で同じことをする Dockerfile は、mcr.microsoft.com/dotnet/sdk でビルドし、mcr.microsoft.com/dotnet/aspnet にコピーする形です。ランタイムを含むため、実行イメージは Go より大きくなります。
例 5: GitHub Actions で ACR にイメージをプッシュし Azure Container Apps に配備する
name: deploy
on:
push:
branches: [main]
permissions:
id-token: write # OIDC によるフェデレーション認証に必要
contents: read
env:
ACR_NAME: myregistry
IMAGE: myregistry.azurecr.io/app
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- run: go vet ./...
- run: go test -race -cover ./...
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Log in to ACR
run: az acr login --name ${{ env.ACR_NAME }}
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ env.IMAGE }}:${{ github.sha }}
build-args: |
VERSION=${{ github.sha }}
COMMIT=${{ github.sha }}
- name: Deploy to Azure Container Apps
run: |
az containerapp update \
--name my-app \
--resource-group my-rg \
--image ${{ env.IMAGE }}:${{ github.sha }}
リソース名やシークレット名は例です。実際の環境に合わせて変更してください。テストと lint の詳細は ツールチェーンと静的解析 を参照してください。
5. まとめ
go buildは、既定でランタイム不要のシングルバイナリを作ります。構成ごとの publish オプションは不要です。- 本番向けには
CGO_ENABLED=0 go build -trimpath -ldflags '-s -w -X main.version=...'が基本形です。バージョンはvarの文字列変数に-Xで埋め込みます。 - クロスコンパイルは
GOOSとGOARCHの指定だけで行えます。 - Docker は、
golangイメージでビルドし、distroless/static(またはscratch) にバイナリだけを載せるマルチステージ構成にします。go.modを先にコピーして依存関係のキャッシュを活かします。 scratchでは CA 証明書とタイムゾーンを自分で用意します。distrolessにはシェルがないため、ヘルスチェックはオーケストレーターに任せます。- GitHub Actions からは、OIDC で Azure に認証し、ACR にコミット SHA のタグでプッシュして、Container Apps などに配備します。
参考資料
- Go Documentation
- A Tour of Go
- Effective Go
- cmd/go: Compile packages and dependencies
- cmd/link (ldflags)
- cmd/cgo
- runtime/debug.ReadBuildInfo
- Docker: Multi-stage builds
- GoogleContainerTools/distroless
- Deploy to Azure Container Apps with GitHub Actions
- Authenticate to Azure from GitHub Actions with OpenID Connect