Go標準ライブラリだけでWebサーバーを作る - net/httpの基本から実践まで

Go標準ライブラリだけでWebサーバーを作る - net/httpの基本から実践まで

2025/12/13に公開

「Goを勉強し始めたけど、Web開発って何から始めればいい?」「フレームワーク使わなくても大丈夫?」と迷っていませんか?

実は、Go言語の標準ライブラリには本番でも使えるWebサーバー機能が揃っています。GinやEchoを使わなくても、net/httpパッケージだけでWebアプリが作れます。

本記事では、Go標準ライブラリを使ったWeb開発の基礎を一緒に学んでいきましょう。

Note

動作確認環境:

  • Go 1.22以上(パターンルーティング機能を使用)

なぜ標準ライブラリから始めるのか

Batteries Included思想

Go言語は「Batteries Included(電池付き)」という設計思想を持っています。外部ライブラリなしで多くのことができるという考え方です。

net/httpパッケージだけで、こんなことができます。

  • HTTPサーバーの起動・管理
  • ルーティング(URLとハンドラーの紐付け)
  • リクエスト・レスポンスの処理
  • 静的ファイルの配信

標準ライブラリを使うメリット

メリット説明
依存関係が少ない外部パッケージ不要でバイナリサイズが小さい
安定性が高いGo公式チームがメンテナンス
学習効果が高いHTTPの仕組みを深く理解できる
移行が簡単フレームワークへの移行時に基礎知識が役立つ
Tip

僕も最初はフレームワークから入りましたが、標準ライブラリを学び直したことで理解が深まりました。「なぜこの機能があるのか」が分かるようになります。

プロジェクトの準備

この記事では、1つのプロジェクトを段階的に拡張していきます。最終的なディレクトリ構成はこうなります。

myapp/ ├── go.mod ├── main.go ├── static/ │ ├── css/ │ │ └── style.css │ └── js/ │ └── main.js └── templates/ ├── layout.html ├── home.html └── about.html

まずはプロジェクトを作成しましょう。

ターミナル
mkdir myapp cd myapp go mod init myapp

Step 1: 最小限のWebサーバー

まずは、最もシンプルなWebサーバーを作ってみましょう。

main.go
package main import ( "fmt" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, World!") }) http.ListenAndServe(":8080", nil) }

実行方法:

ターミナル
go run main.go

ブラウザで http://localhost:8080 にアクセスすると、「Hello, World!」が表示されます。

Step 1: Hello World表示

コードの解説

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, World!") })
  • http.HandleFunc: URLパスとハンドラー関数を紐付ける
  • "/": ルートパス(すべてのリクエストにマッチ)
  • w http.ResponseWriter: レスポンスを書き込むためのインターフェース
  • r *http.Request: リクエスト情報を持つ構造体
http.ListenAndServe(":8080", nil)
  • :8080: ポート8080でサーバーを起動
  • nil: デフォルトのServeMuxを使用

Step 2: ルーティングを追加

パターンルーティング

Go 1.22から、http.ServeMuxパターンマッチングが使えるようになりました。外部ルーターなしで柔軟なルーティングが書けます。

main.go
package main import ( "fmt" "net/http" ) func main() { mux := http.NewServeMux() // 基本的なルート mux.HandleFunc("GET /", homeHandler) mux.HandleFunc("GET /about", aboutHandler) // パスパラメータ mux.HandleFunc("GET /users/{id}", userHandler) // HTTPメソッド指定 mux.HandleFunc("POST /users", createUserHandler) fmt.Println("Server starting on :8080") http.ListenAndServe(":8080", mux) } func homeHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "ホームページへようこそ!") } func aboutHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "このサイトについて") } func userHandler(w http.ResponseWriter, r *http.Request) { // パスパラメータを取得 id := r.PathValue("id") fmt.Fprintf(w, "ユーザーID: %s", id) } func createUserHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "ユーザーを作成しました") }

書き方

Go 1.22以降では、こんなパターンが使えます。

// HTTPメソッド + パス mux.HandleFunc("GET /users", listUsers) // GETのみ mux.HandleFunc("POST /users", createUser) // POSTのみ // パスパラメータ mux.HandleFunc("GET /users/{id}", getUser) // {id}で値を取得 mux.HandleFunc("GET /posts/{slug...}", getPost) // {slug...}で残りのパス全体を取得 // 末尾スラッシュ mux.HandleFunc("GET /api/", apiRoot) // /api/ と /api/xxx にマッチ mux.HandleFunc("GET /api", apiExact) // /api のみにマッチ
Important

Go 1.22未満のバージョンでは、"GET /users"のようなメソッド指定やパスパラメータは使えません。go versionで確認してください。

Tip

POSTリクエストはブラウザから直接確認できません。ターミナルからcurlコマンドを使いましょう:

ターミナル
curl -X POST http://localhost:8080/users

Step 3: HTMLテンプレートを追加

実際のWebアプリケーションでは、HTMLを返すことが多いですよね。Goにはhtml/templateパッケージが用意されています。

テンプレート作成

まず、テンプレート用のディレクトリを作成します。

ターミナル
mkdir templates

3つのテンプレートファイルを作成しましょう。最初はインラインスタイルで作成し、Step 4で外部CSSに切り替えます。

templates/layout.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{.Title}}</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } nav { margin-bottom: 20px; } nav a { margin-right: 15px; } </style> </head> <body> <nav> <a href="/">ホーム</a> <a href="/about">About</a> </nav> <main> {{template "content" .}} </main> </body> </html>
templates/home.html
{{define "content"}} <h1>{{.Title}}</h1> <p>{{.Message}}</p> {{if .Items}} <ul> {{range .Items}} <li>{{.}}</li> {{end}} </ul> {{end}} {{end}}
templates/about.html
{{define "content"}} <h1>{{.Title}}</h1> <p>このサイトはGo標準ライブラリだけで作られています。</p> <h2>使用技術</h2> <ul> <li>Go {{.GoVersion}}</li> <li>net/http</li> <li>html/template</li> </ul> {{end}}

main.goを更新

Step 2で作成したmain.goを、テンプレートを使うように更新しましょう。

main.go
package main import ( "fmt" "html/template" "net/http" "runtime" ) func main() { mux := http.NewServeMux() // ルート mux.HandleFunc("GET /", homeHandler) mux.HandleFunc("GET /about", aboutHandler) mux.HandleFunc("GET /users/{id}", userHandler) fmt.Println("Server starting on :8080") http.ListenAndServe(":8080", mux) } func homeHandler(w http.ResponseWriter, r *http.Request) { tmpl, err := template.ParseFiles( "templates/layout.html", "templates/home.html", ) if err != nil { http.Error(w, "テンプレートエラー", http.StatusInternalServerError) return } data := map[string]any{ "Title": "Goで作るWebサイト", "Message": "標準ライブラリだけで作りました!", "Items": []string{"シンプル", "高速", "安全"}, } tmpl.Execute(w, data) } func aboutHandler(w http.ResponseWriter, r *http.Request) { tmpl, err := template.ParseFiles( "templates/layout.html", "templates/about.html", ) if err != nil { http.Error(w, "テンプレートエラー", http.StatusInternalServerError) return } data := map[string]any{ "Title": "About", "GoVersion": runtime.Version(), } tmpl.Execute(w, data) } func userHandler(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") fmt.Fprintf(w, "ユーザーID: %s", id) }

テンプレート構文の基本

構文説明
{{.Field}}フィールドの値を表示{{.Title}}
{{if .Cond}}...{{end}}条件分岐{{if .Items}}...{{end}}
{{range .Slice}}...{{end}}ループ{{range .Items}}{{.}}{{end}}
{{template "name" .}}別テンプレートを呼び出し{{template "content" .}}
{{define "name"}}...{{end}}テンプレートを定義{{define "content"}}...{{end}}

サーバーを起動して、ホームページとAboutページを確認してみましょう。

Step 3: HTMLテンプレート表示

Step 4: 静的ファイル配信を追加

CSS、JavaScript、画像などの静的ファイルを配信できるようにしましょう。

静的ファイルを準備

ターミナル
mkdir -p static/css static/js
static/css/style.css
/* 基本スタイル */ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; line-height: 1.6; color: #333; background-color: #f9f9f9; } /* ナビゲーション */ nav { margin-bottom: 30px; padding-bottom: 15px; border-bottom: 1px solid #ddd; } nav a { margin-right: 20px; text-decoration: none; color: #00add8; font-weight: 500; } nav a:hover { text-decoration: underline; } /* 見出し */ h1 { color: #00add8; margin-bottom: 20px; } h2 { color: #555; border-bottom: 2px solid #00add8; padding-bottom: 5px; } /* リスト */ ul { padding-left: 20px; } li { margin-bottom: 8px; } /* フォーム */ form { max-width: 500px; } form p { margin-bottom: 15px; } label { font-weight: 500; color: #555; } input, textarea { width: 100%; padding: 10px; margin-top: 5px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } input:focus, textarea:focus { outline: none; border-color: #00add8; } button { background-color: #00add8; color: white; padding: 12px 24px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; } button:hover { background-color: #008cba; }
static/js/main.js
console.log('Go Web App loaded!');

静的ファイル配信を追加

Step 3のコードに、静的ファイル配信の設定を追加します。main関数を以下のように更新してください。

func main() { mux := http.NewServeMux() // 静的ファイルの配信を追加 fs := http.FileServer(http.Dir("static")) mux.Handle("GET /static/", http.StripPrefix("/static/", fs)) // ルート(Step 3と同じ) mux.HandleFunc("GET /", homeHandler) mux.HandleFunc("GET /about", aboutHandler) mux.HandleFunc("GET /users/{id}", userHandler) fmt.Println("Server starting on :8080") http.ListenAndServe(":8080", mux) }

解説

fs := http.FileServer(http.Dir("static"))
  • http.Dir("static"): staticディレクトリをファイルシステムとして扱う
  • http.FileServer: ファイルを配信するハンドラーを作成
mux.Handle("GET /static/", http.StripPrefix("/static/", fs))
  • http.StripPrefix: URLから/static/を取り除いてからファイルを探す
  • /static/css/style.cssへのリクエスト → static/css/style.cssを返す

layout.htmlを更新

静的ファイルを参照するように、Step 3で作成したtemplates/layout.htmlを更新しましょう。

templates/layout.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{.Title}}</title> <link rel="stylesheet" href="/static/css/style.css"> </head> <body> <nav> <a href="/">ホーム</a> <a href="/about">About</a> <a href="/contact">お問い合わせ</a> </nav> <main> {{template "content" .}} </main> <script src="/static/js/main.js"></script> </body> </html>

CSSが適用されて、見た目が整いました。

Step 4: CSS適用後の表示

Step 5: フォーム処理を追加

最後に、ユーザーからの入力を受け取るお問い合わせフォームを追加しましょう。

contact.html作成

他のページと同様に、テンプレートファイルとして作成します。

templates/contact.html
{{define "content"}} <h1>{{.Title}}</h1> <form method="POST" action="/contact"> <p> <label>お名前:</label> <input type="text" name="name" required> </p> <p> <label>メールアドレス:</label> <input type="email" name="email" required> </p> <p> <label>メッセージ:</label> <textarea name="message" rows="5" required></textarea> </p> <button type="submit">送信</button> </form> {{end}}

contactハンドラーを追加

Step 4のコードに、お問い合わせフォーム用のルートを追加します。main関数に以下を追加してください。

// main関数に追加 mux.HandleFunc("GET /contact", contactFormHandler) mux.HandleFunc("POST /contact", contactSubmitHandler)

そして、以下のハンドラー関数を追加します。

func contactFormHandler(w http.ResponseWriter, r *http.Request) { tmpl, err := template.ParseFiles( "templates/layout.html", "templates/contact.html", ) if err != nil { http.Error(w, "テンプレートエラー", http.StatusInternalServerError) return } data := map[string]any{ "Title": "お問い合わせ", } tmpl.Execute(w, data) } func contactSubmitHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "フォームの解析に失敗しました", http.StatusBadRequest) return } name := r.FormValue("name") email := r.FormValue("email") message := r.FormValue("message") fmt.Fprintf(w, "ありがとうございます、%sさん!\n", name) fmt.Fprintf(w, "メール: %s\n", email) fmt.Fprintf(w, "メッセージ: %s\n", message) }

お問い合わせフォームが完成しました。

Step 5: お問い合わせフォーム

フォーム処理のポイント

メソッド説明
r.ParseForm()フォームデータをパース(POST必須)
r.FormValue("key")フォームの値を取得
r.URL.Query().Get("key")クエリパラメータを取得
r.PostFormValue("key")POSTのみから取得
Warning

ユーザー入力は必ずバリデーションしてください。この例では簡略化していますが、本番環境では適切な入力チェックが必要です。

Step 6: リファクタリングで整理する

Step 1〜5で作成したコードは動きますが、本番向けにもう少し改善してみましょう。

テンプレートの事前読み込み

現在のコードでは、リクエストごとにtemplate.ParseFilesを呼んでいます。

// 改善前:毎回テンプレートを読み込む(非効率) func homeHandler(w http.ResponseWriter, r *http.Request) { tmpl, err := template.ParseFiles( "templates/layout.html", "templates/home.html", ) // ... }

起動時に一度だけ読み込むように変更しましょう。

// 改善後:起動時に一度だけ読み込む var templates *template.Template func init() { templates = template.Must(template.ParseGlob("templates/*.html")) }

共通関数の抽出

各ハンドラーで同じテンプレート描画処理を繰り返しています。

// 改善前:同じ処理が各ハンドラーに散らばっている func homeHandler(w http.ResponseWriter, r *http.Request) { tmpl, err := template.ParseFiles(...) if err != nil { http.Error(w, "テンプレートエラー", http.StatusInternalServerError) return } tmpl.Execute(w, data) } func aboutHandler(w http.ResponseWriter, r *http.Request) { tmpl, err := template.ParseFiles(...) // 同じ処理 if err != nil { http.Error(w, "テンプレートエラー", http.StatusInternalServerError) return } tmpl.Execute(w, data) }

共通関数にまとめましょう。

// 改善後:共通関数に抽出 func renderTemplate(w http.ResponseWriter, tmplName string, data map[string]any) { err := templates.ExecuteTemplate(w, tmplName, data) if err != nil { http.Error(w, "テンプレートエラー", http.StatusInternalServerError) } } func homeHandler(w http.ResponseWriter, r *http.Request) { data := map[string]any{...} renderTemplate(w, "layout.html", data) // シンプルに }

サーバー設定の強化

http.ListenAndServeではタイムアウトが設定できません。

// 改善前:タイムアウトなし http.ListenAndServe(":8080", mux)

http.Server構造体を使いましょう。

// 改善後:タイムアウト設定 server := &http.Server{ Addr: ":8080", Handler: mux, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, } log.Fatal(server.ListenAndServe())

リファクタリングまとめ

改善前改善後メリット
毎回template.ParseFilesinit()で一度だけ読み込みパフォーマンス向上
各ハンドラーで同じ処理renderTemplateに共通化コードの重複削除
http.ListenAndServehttp.Server構造体タイムアウト設定可能

完成コード

Step 1〜6を適用した最終的なコードです。

Tip

完成コードは以下のGitHubリポジトリからダウンロードできます。

techarm/blog-code-examples/go-web-standard-library

ファイル構成

myapp/ ├── go.mod ├── main.go ├── static/ │ ├── css/ │ │ └── style.css │ └── js/ │ └── main.js └── templates/ ├── layout.html ├── home.html ├── about.html └── contact.html

main.go

main.go
package main import ( "fmt" "html/template" "log" "net/http" "runtime" "time" ) var templates *template.Template func init() { templates = template.Must(template.ParseGlob("templates/*.html")) } func main() { mux := http.NewServeMux() // 静的ファイルの配信を追加 fs := http.FileServer(http.Dir("static")) mux.Handle("GET /static/", http.StripPrefix("/static/", fs)) // ルート mux.HandleFunc("GET /", homeHandler) mux.HandleFunc("GET /about", aboutHandler) mux.HandleFunc("GET /users/{id}", userHandler) mux.HandleFunc("GET /contact", contactFormHandler) mux.HandleFunc("POST /contact", contactSubmitHandler) log.Println("Server starting on http://localhost:8080") server := &http.Server{ Addr: ":8080", Handler: mux, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, } log.Fatal(server.ListenAndServe()) } func renderTemplate(w http.ResponseWriter, tmplName string, data map[string]any) { err := templates.ExecuteTemplate(w, tmplName, data) if err != nil { http.Error(w, "テンプレートエラー", http.StatusInternalServerError) } } func homeHandler(w http.ResponseWriter, r *http.Request) { data := map[string]any{ "Title": "Goで作るWebサイト", "Message": "標準ライブラリだけで作りました!", "Items": []string{"シンプル", "高速", "安全"}, } renderTemplate(w, "layout.html", data) } func aboutHandler(w http.ResponseWriter, r *http.Request) { data := map[string]any{ "Title": "About", "GoVersion": runtime.Version(), } renderTemplate(w, "layout.html", data) } func userHandler(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") fmt.Fprintf(w, "ユーザーID: %s", id) } func contactFormHandler(w http.ResponseWriter, r *http.Request) { data := map[string]any{ "Title": "お問い合わせ", } renderTemplate(w, "layout.html", data) } func contactSubmitHandler(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, "フォームの解析に失敗しました", http.StatusBadRequest) return } name := r.FormValue("name") email := r.FormValue("email") message := r.FormValue("message") fmt.Fprintf(w, "ありがとうございます、%sさん!\n", name) fmt.Fprintf(w, "メール: %s\n", email) fmt.Fprintf(w, "メッセージ: %s\n", message) }

完成したアプリケーションの画面です。

完成版: Webアプリケーション

まとめ

この記事では、Go標準ライブラリnet/httpを使ったWeb開発の基礎を学びました。

学んだこと:

  • 最小限のWebサーバーの作り方
  • Go 1.22のパターンルーティング
  • HTMLテンプレートの使い方
  • 静的ファイルの配信
  • フォーム処理

標準ライブラリだけでも、けっこういろんなことができますよね。

Tip

次回予告: 同じ標準ライブラリを使ってREST APIを構築する方法を解説します。JSONの送受信やCRUD操作の実装を一緒に学んでいきましょう。

参考リンク

この記事はいかがでしたか?

もしこの記事が参考になりましたら、
高評価をいただけると大変嬉しいです!

皆様からの応援が励みになります。ありがとうございます! ✨