【Go】比較検証 : GoアプリをDockerでビルド・デプロイし、Python/Node.jsと比較する

Go

こんにちは、かつコーチです。

これでGo編は20本目、最終回です。

これまで入門・基本文法・並行処理・CLIツール・Web APIと積み上げてきましたが、最後は本番運用に欠かせないデプロイの話で締めくくります。

Docker編で解説したマルチステージビルド(「マルチステージビルドでイメージを軽量化する」で詳しく解説しています)は、実はGoと非常に相性のいい手法です。

この記事では、Goアプリを軽量な単一バイナリのDockerイメージとしてビルドする手順を示します。

そのうえで、同じAPIをPython・Node.jsで作った場合とイメージサイズ・起動速度を比較検証します。

読み終える頃には、「なぜインフラ担当者がGoを評価するのか」が数値で実感できるはずです。

Goアプリをマルチステージビルドでコンテナ化する

比較対象のシンプルなAPIを用意する

比較を公平にするため、3言語とも「/healthにアクセスするとOKを返すだけ」の最小限のAPIで揃えます。

// main.go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "OK")
	})
	http.ListenAndServe(":8080", nil)
}

Go用のマルチステージDockerfileを書く

Docker編で扱った「ビルド用ステージと実行用ステージを分ける」という考え方を、そのままGoに適用します。

# ステージ1:ビルド用
FROM golang:1.23 AS builder

WORKDIR /app
COPY go.mod ./
COPY main.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

# ステージ2:実行用
FROM scratch

COPY --from=builder /app/server /server

EXPOSE 8080
ENTRYPOINT ["/server"]

ここで重要なのがCGO_ENABLED=0です。

Goはデフォルトでcgo(Cのライブラリを呼び出す仕組み)が有効になっており、glibcに動的リンクされたバイナリが生成されます。

FROM scratch(何も含まない完全に空のベースイメージ)で実行するには、外部ライブラリに一切依存しない静的リンクバイナリが必要です。

docker build -t go-api:multistage .
docker run -d -p 8080:8080 go-api:multistage
curl http://localhost:8080/health
OK

Python・Node.js版も同じ構成で用意する

比較用に、同じ/healthエンドポイントをFlask(Python)とExpress(Node.js)でも用意します。

それぞれ公式の軽量イメージを使ってDockerfileを組みます。

# Python版(Flask)
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]
# Node.js版(Express)
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js .

FROM node:20-slim
WORKDIR /app
COPY --from=builder /app .
CMD ["node", "server.js"]

PythonもNode.jsも、実行時にインタプリタ本体(Python本体・Node.jsランタイム)を同梱する必要があります。

そのため、Goのように「ビルド成果物のバイナリだけ」というわけにはいきません。

イメージサイズ・起動速度を比較する

イメージサイズの比較結果

docker imagesで、それぞれのイメージのサイズを確認します。

docker images | grep -E "go-api|python-api|node-api"
イメージベースサイズ(目安)
go-api:multistagescratch約8MB
go-api:distrolessgcr.io/distroless/static約15MB
python-api:slimpython:3.12-slim約150MB
node-api:slimnode:20-slim約180MB

Goのscratchベースイメージは、PythonやNode.jsの10分の1以下のサイズに収まっています。

Goはコンパイルによって、OS非依存の単一バイナリを生成する言語です。

一方でPython・Node.jsは、インタプリタ本体をコンテナ内に持ち込む必要があります。

この差は、言語設計そのものの違いによるものです。

起動速度の比較

イメージサイズだけでなく、コンテナ起動から実際にリクエストを処理できるまでの速さも比較してみます。

docker run -d -p 8080:8080 go-api:multistage
curl http://localhost:8080/health

Goのコンテナは、起動直後にcurlを打ってもほぼ即座にOKが返ってきました。

一方、Node.js版のコンテナで同じように起動直後にcurlを打ったところ、次のようなエラーに遭遇しました。

curl: (7) Failed to connect to localhost port 8080: Connection refused

Node.jsランタイムの初期化とExpressの起動処理に、体感で数百ミリ秒〜1秒程度の時間がかかっており、その間はまだポートが開いていなかったのです。

Goの場合、コンパイル済みのバイナリがOSのプロセスとしてそのまま立ち上がるだけなので、こうした「ランタイムの起動待ち」がほとんど発生しません。

オートスケールでコンテナを頻繁に起動・終了するような環境では、この起動速度の差がそのままレスポンス品質の差につながります。

よくあるつまずきポイント・エラー対処

scratchイメージでno such file or directoryが出た

これはGoとDockerを組み合わせたときに、非常によく遭遇するエラーです。

筆者も初めてFROM scratchを試したとき、まさにこれで詰まりました。

❌Before:CGO_ENABLED=0を付けずにビルドする

FROM golang:1.23 AS builder
WORKDIR /app
COPY . .
RUN go build -o server .

FROM scratch
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
docker run go-api:broken
standard_init_linux.go:228: exec user process caused: no such file or directory

一見「ファイルが見当たらない」というエラーに見えますが、実際にはserverバイナリ自体はコンテナ内に存在しています。

原因は、golang:1.23イメージ内でビルドしたバイナリが、glibcに動的リンクされていたことです。

そのglibc自体が、scratchイメージには存在しないためです。

✅After:CGO_ENABLED=0で静的リンクバイナリにする

FROM golang:1.23 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

FROM scratch
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]

CGO_ENABLED=0を付けて再ビルドしたところ、無事にコンテナが起動しました。

「ファイルはあるのに存在しないと言われる」という一見矛盾したエラーメッセージに戸惑いましたが、原因は動的リンクという、Goの裏側の仕組みにありました。

scratchdistrolessのような最小構成イメージを使う場合は、CGO_ENABLED=0をセットにして覚えておくと、このエラーを未然に防げます。

まとめ

この記事のポイント

  • Goアプリはマルチステージビルド+CGO_ENABLED=0で、数MB〜十数MBの極小イメージにできる
  • Python・Node.jsはインタプリタ本体を同梱する必要があり、イメージサイズは数百MB規模になりやすい
  • Goのコンテナはランタイム起動待ちがほとんどなく、起動直後からリクエストを処理できる
  • scratchイメージでno such file or directoryが出たら、まずCGO_ENABLED=0を疑う

Go編(全20本)を振り返って

入門・環境構築から始まり、変数や制御構文といった基本文法を学びました。

そこからインターフェース・エラーハンドリング、goroutineとchannelによる並行処理へと進みました。

さらにflagパッケージを使ったCLIツール開発、net/httpによるWeb API開発、testingパッケージによるテストと、ここまで積み上げてきました。

最後にDockerでのデプロイと他言語との比較検証をもって、Go編は完結です。

「シンプルさと高速性を両立する」というGoの設計思想は、コードの書きやすさだけでなく、こうしてコンテナイメージのサイズや起動速度という運用面の数値にもはっきり表れています。

次に読むべき記事

  • testingパッケージでユニットテストを書く基本
  • マルチステージビルドでイメージを軽量化する(Docker編)

Go編はこれで完結です。

次はモバイル開発のSwift編に進みます。

ここまでお読みいただき、ありがとうございました。

タグ: Go, 中級者向け, 比較検証

タイトルとURLをコピーしました