Journal

Goはなぜ「フレームワークを書かない」で済むのか

Goの話になると、「標準ライブラリだけで十分です」と言う人がいます。あれは半分スローガンで、半分は普通に事実です。PythonやNode.jsだと、先にフレームワーク名から考える場面が多いですが、Goは逆です。まず net/http で書いて、足りなくなったところだけ後から足せます。

最近も、30分で書いた小さなCLIをまだ使っています。こういう雑な道具が残りやすいのは、Goが最初から過剰な約束を持ち込まないからだと思います。HTTPの小さなサーバーも同じです。ルーティング、JSON、ログ出力くらいなら、標準ライブラリだけで気持ちよく終わります。

先に標準ライブラリを触っても形になります

この記事用に、最小のHTTPサーバーを Docker で確認しました。Go 1.25 の ServeMux はメソッド付きルートをそのまま書けるので、昔よりかなり素直です。これくらいなら、フレームワークを選ぶ会議より先にコードが動きます。

docker run --rm -v "$PWD":/src -w /src golang:1.25 go test ./...
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"
)

type response struct {
    Message string `json:"message"`
}

func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
    })
}

func main() {
    mux := http.NewServeMux()

    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        _, _ = w.Write([]byte("ok"))
    })

    mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        _ = json.NewEncoder(w).Encode(response{Message: "hello"})
    })

    log.Fatal(http.ListenAndServe(":8080", logging(mux)))
}

フレームワークが不要なのではなく、後回しにできます

ここで大事なのは、Goにはフレームワークがいらないと言いたいわけではないことです。認証、ORM、OpenAPI、管理画面まで欲しくなれば、別の道具を足せば済みます。ただ、疎通確認用のAPIや社内向けの薄いサービスで、最初から大きな枠組みを背負う必要はあまりありません。

このあたりはAIとも相性がいいです。handler を一つ増やす、JSON を返す、ログを差し込む。そのくらいのボイラープレートはAIに任せても困りません。人間は、エンドポイントをどう切るか、失敗をどう返すか、どこまでを公開面に出すかを考えるほうが大事です。フレームワークの流儀を覚えるより、そちらのほうがずっと面白いです。

Goは今のC言語にかなり近いです

私はGoを現代のC言語みたいなものだと思っています。メモリ管理の顔つきは違いますが、OSに近い層を触るときの素直さが似ています。ファイル、ソケット、標準入力、プロセス。このあたりを変に飾らず扱える言語は、結局長く残ります。

Web開発の流行はすぐ変わります。それでも net/http の小さなサーバーは、数年後に見返してもまだ読めます。自分のPCや自分のサービスを自分で触りたいなら、この鈍くなさはかなり助かります。少なくとも私は、新しい抽象化を足す前に、まず標準ライブラリだけでどこまで行けるかを試します。

Previous