メインコンテンツまでスキップ

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-debian12CA 証明書、タイムゾーン情報、非 root ユーザーを含む最小のイメージ。静的バイナリ向け
gcr.io/distroless/base-debian12glibc を含む。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/aspnetscratch / distroless / alpine

上の表を補う対応関係です。

.NET (C#)Go
dotnet publish -c Releasego build (最適化は常に有効で、構成の区別はない)
-r linux-x64 --self-containedGOOS=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 などに配備します。

参考資料​