Go HTTP サービス
Go標準ライブラリに含まれる
net/httpパッケージは機能が充実しており、サードパーティ製のフレームワークを使用することなく、本番環境向けのWebサービスを構築することができます。
チームが「Webフレームワークを導入せず、標準ライブラリのみを使用する」と決めた場合、GinやEchoと同じように、わかりやすいルートやミドルウェアを記述できるでしょうか?このレッスンでは、GoのHTTPサービスに関する中核となる技術をすべて習得します。
1. 学習内容
http.ListenAndServeサービスを起動しますHandlerインターフェースおよびHandlerFuncアダプターServeMuxルート登録- Go 1.22 ルーティング機能の強化:メソッド + パスパターン + パスパラメータ
- JSON レスポンスのエンコーディング
- リクエストパラメータの解析(クエリパラメータ、フォームパラメータ、パスパラメータ)
- 静的ファイルサービス
2. バックエンドエンジニアの実話
(1) 課題:シンプルなAPI――Ginを採用しましたが、3か月後、アップグレードの途中で行き詰まってしまいました。
アリスはバックエンドチームのメンバーで、ユーザー管理用のREST APIを設定する必要があります:
「Gin フレームワークを使って、GET /users、POST /users、GET /users/:id の 3 つのルートを作成しました。しかし、3 か月後に Go 1.22 がリリースされ、標準ライブラリにメソッドおよびパスパラメータのネイティブサポートが追加されました。そこで Gin への依存関係を削除したいのですが、そのためにはすべてのハンドラのシグネチャを変更しなければなりません――gin.Context から http.ResponseWriter への変更です。上司は『たった1つの依存関係をなくすためだけに、何百行ものコードをリファクタリングする価値はない』と言いました。」
当時の彼女の決断:
// Gin 依存関係のバージョン(3か月後に移行したい)
r := gin.Default()
r.GET("/users", listUsers) // gin.Context
r.POST("/users", createUser) // gin.Context
r.GET("/users/:id", getUser) // gin.Context
// 標準ライブラリに移行したい?すべて handler 署名もすべて変更する必要がある!
(2) Go 1.22 向けの解決策:標準ライブラリにおけるネイティブルーティング
// 標準ライブラリのバージョン(Go 1.22+,依存関係は一切不要)
パッケージ main
import (
"encoding/json"
"log"
"net/http"
)
type User struct {
ID int `json:"id"`
Name 文字列 `json:"name"`
}
var users = []User{{ID: 1, Name: "Alice"}}
func main() {
mux := http.NewServeMux()
// Go 1.22 ルーティングの強化:メソッド + パスパターン + パス引数
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /users/{id}", getUser)
log.Println("サービスの開始は :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
// 基準 handler 署名:http.ResponseWriter + *http.Request
func listUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func createUser(w http.ResponseWriter, r *http.Request) {
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
u.ID = len(users) + 1
users = append(users, u)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(u)
}
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // パス引数!
// 検索 ユーザー...
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(User{ID: 1, Name: "Alice"})
}
(3) パフォーマンス:Ginスタイル対標準ライブラリ(Go 1.22)
| 機能 | Gin(サードパーティ製) | 標準ライブラリ(Go 1.22未満) | 標準ライブラリ(Go 1.22以降) |
|---|---|---|---|
| パスパラメータ | :id ✅ |
❌ 手動で解析する必要があります | {id} ✅ |
| メソッドルーティング | ✅ | ❌ ハンドラー内で確認 | "GET /path" ✅ |
| JSONレスポンス | c.JSON() |
ヘッダーを手動で設定 | ヘッダーを手動で設定 |
| 依存関係 | 外部パッケージ 1 つ | 0 | 0 |
| パフォーマンス | わずかに遅い(リフレクション) | ネイティブ | ネイティブ |
net/httpで強化されたルーティング機能は、ほとんどのWebプロジェクトにとって十分です。フレームワーク固有の機能(自動バインディングやバリデーション、充実したミドルウェアエコシステムなど)が必要ない場合は、標準ライブラリを優先してください。
3. HTTPの基礎
▶ サンプル:最もシンプルなHTTPサービス
package main
import (
"fmt"
"log"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}
func main() {
http.HandleFunc("/hello", helloHandler)
log.Println("サービスの開始は :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
テスト:
$ curl "http://localhost:8080/hello?name=Alice"
Hello, Alice!
(1) コアの種類
| 種類 | 説明 |
|---|---|
http.ResponseWriter |
HTTPレスポンスを記述するためのインターフェース |
*http.Request |
URL、ヘッダー、本文、フォームを含むHTTPリクエスト |
http.Handler |
インターフェース:ServeHTTP(w, r) |
http.HandlerFunc |
関数アダプタ:通常の関数をハンドラに変換する |
http.ServeMux |
ルートマルチプレクサ |
(2) ハンドラ・インターフェースの詳細な説明
package main
import (
"fmt"
"log"
"net/http"
)
// 方法 1:実装 Handler インターフェース
type Greeter struct {
Greeting string
}
func (g *Greeter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s, %s!", g.Greeting, r.URL.Path[1:])
}
// 方法 2:HandlerFunc アダプター
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}
func main() {
mux := http.NewServeMux()
// 構造体 Handler
mux.Handler("/greet", &Greeter{Greeting: "Welcome"})
// 関数 Handler(HandlerFunc 自動変換)
mux.HandlerFunc("/hello", helloHandler)
log.Print(http.ListenAndServe(":8080", mux))
}
4. Go 1.22:ルーティング機能の強化
▶ サンプル:メソッド + パスパターン + パスパラメータ
package main
import (
"encoding/json"
"log"
"net/http"
)
type Item struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
var items = []Item{
{ID: 1, Name: "Laptop", Price: 999.99},
{ID: 2, Name: "Mouse", Price: 29.99},
}
func main() {
mux := http.NewServeMux()
// Go 1.22 方法 + パスパターン
mux.HandleFunc("GET /items", listItems)
mux.HandleFunc("POST /items", createItem)
mux.HandleFunc("GET /items/{id}", getItem)
mux.HandleFunc("PUT /items/{id}", updateItem)
mux.HandleFunc("DELETE /items/{id}", deleteItem)
// ワイルドカードサフィックス:パスプレフィックスのマッチング
mux.HandleFunc("GET /items/{path...}", wildcardHandler)
log.Print(http.ListenAndServe(":8080", mux))
}
func listItems(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(items)
}
func createItem(w http.ResponseWriter, r *http.Request) {
var item Item
if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
item.ID = len(items) + 1
items = append(items, item)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(item)
}
func getItem(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// 検索 item...
_ = id
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(items[0])
}
func updateItem(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
_ = id
// 更新ロジック...
w.WriteHeader(http.StatusNoContent)
}
func deleteItem(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
_ = id
// 削除ロジック...
w.WriteHeader(http.StatusNoContent)
}
func wildcardHandler(w http.ResponseWriter, r *http.Request) {
path := r.PathValue("path")
w.Header().Set("Content-Type", "text/plain")
http.Error(w, "Not found: "+path, http.StatusNotFound)
}
(1) Go 1.22におけるルーティングモードの比較
| モード | Go < 1.22 | Go 1.22 以降 | 例 |
|---|---|---|---|
| メソッドマッチング | 未対応(ハンドラー内のif文) | 対応 | "GET /items" |
| パスパラメータ | 未対応 | {name} 構文 |
"GET /items/{id}" |
| ワイルドカード接尾辞 | 未対応 | {path...} |
"GET /static/{file...}" |
| 完全なパス | /items |
"GET /items" |
/items と完全に一致 |
| プレフィックスマッチ | /items/ |
"GET /items/" |
/items/... に一致 |
▶ サンプル:ルートの優先順位
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
// 正確な経路 > プレフィックスパス
mux.HandleFunc("GET /items", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "items list")
})
mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "item %s\n", r.PathValue("id"))
})
mux.HandleFunc("GET /items/featured", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "featured items")
})
log.Print(http.ListenAndServe(":8080", mux))
}
テスト:
$ curl localhost:8080/アイテム
アイテム list
$ curl localhost:8080/アイテム/42
item 42
$ curl localhost:8080/アイテム/featured
featured アイテム # 完全一致が優先される {id} ワイルドカード
5. リクエストとレスポンス
▶ サンプル:クエリパラメータ、フォーム、JSON
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
type Response struct {
Method string `json:"method"`
Path string `json:"path"`
Query interface{} `json:"query,omitempty"`
Form interface{} `json:"form,omitempty"`
JSON interface{} `json:"json,omitempty"`
}
func handler(w http.ResponseWriter, r *http.Request) {
resp := Response{
Method: r.Method,
Path: r.URL.Path,
}
// クエリパラメータ
if r.Method == http.MethodGet {
resp.Query = r.URL.Query()
}
// フォームデータ
if r.Method == http.MethodPost {
contentType := r.Header.Get("Content-Type")
switch {
case contentType == "application/x-www-form-urlencoded":
r.ParseForm()
resp.Form = r.Form
case contentType == "application/json":
var body interface{}
json.NewDecoder(r.Body).Decode(&body)
resp.JSON = body
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
log.Print(http.ListenAndServe(":8080", mux))
}
▶ サンプル:JSONレスポンス用ユーティリティ関数
package main
import (
"encoding/json"
"log"
"net/http"
)
// JSON レスポンスツール関数
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
}
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
func getProduct(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// シミュレーション検索
if id != "1" {
writeError(w, http.StatusNotFound, "product not found")
return
}
writeJSON(w, http.StatusOK, Product{ID: 1, Name: "Laptop", Price: 999.99})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /products/{id}", getProduct)
log.Print(http.ListenAndServe(":8080", mux))
}
sequenceDiagram
participant Client as HTTP クライアント
participant Mux as ServeMux
participant Handler as Handler
Client->>Mux: GET /products/1
Mux->>Mux: ルーティングマッチング
Mux->>Handler: ServeHTTP(w, r)
Handler->>Handler: r.PathValue("id") → "1"
Handler->>Handler: writeJSON(w, 200, product)
Handler-->>Client: HTTP 200 + JSON body
(2) HTTPステータスコードのクイックリファレンス
| コード | 定数 | 用途 |
|---|---|---|
| 200 | http.StatusOK |
成功 |
| 201 | http.StatusCreated |
リソースの作成に成功しました |
| 204 | http.StatusNoContent |
成功したが、レスポンス本文がない |
| 400 | http.StatusBadRequest |
クライアントからのリクエストエラー |
| 401 | http.StatusUnauthorized |
アクセス拒否 |
| 403 | http.StatusForbidden |
権限なし |
| 404 | http.StatusNotFound |
リソースが見つかりません |
| 500 | http.StatusInternalServerError |
サーバー内部エラー |
6. 静的ファイルサービス
パッケージ main
import (
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
// 静的ファイルサービス:/static/ 接頭辞 → ./static/ 目次
mux.Handle("GET /static/", http.StripPrefix("/static/",
http.FileServer(http.Dir("./static"))))
// 単一ファイル
mux.Handle("GET /favicon.ico", http.FileServer(http.Dir("./static")))
log.Print(http.ListenAndServe(":8080", mux))
}
7. 完全な例:RESTスタイルのノートAPI
// notes_api.go
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"sync"
"time"
)
// ---------- Model ----------
type Note struct {
ID int `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ---------- Store ----------
type NoteStore struct {
mu sync.RWMutex
notes map[int]Note
nextID int
}
func NewNoteStore() *NoteStore {
return &NoteStore{
notes: make(map[int]Note),
nextID: 1,
}
}
func (s *NoteStore) Create(title, content string) Note {
s.mu.Lock()
defer s.mu.Unlock()
n := Note{
ID: s.nextID,
Title: title,
Content: content,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
s.nextID++
s.notes[n.ID] = n
return n
}
func (s *NoteStore) List() []Note {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]Note, 0, len(s.notes))
for _, n := range s.notes {
result = append(result, n)
}
return result
}
func (s *NoteStore) Get(id int) (Note, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
n, ok := s.notes[id]
return n, ok
}
func (s *NoteStore) Update(id int, title, content string) (Note, bool) {
s.mu.Lock()
defer s.mu.Unlock()
n, ok := s.notes[id]
if !ok {
return Note{}, false
}
n.Title = title
n.Content = content
n.UpdatedAt = time.Now()
s.notes[id] = n
return n, true
}
func (s *NoteStore) Delete(id int) bool {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.notes[id]
if !ok {
return false
}
delete(s.notes, id)
return true
}
// ---------- API ----------
type NotesAPI struct {
store *NoteStore
}
func NewNotesAPI(store *NoteStore) *NotesAPI {
return &NotesAPI{store: store}
}
func (api *NotesAPI) Register(mux *http.ServeMux) {
mux.HandleFunc("GET /notes", api.ListNotes)
mux.HandleFunc("POST /notes", api.CreateNote)
mux.HandleFunc("GET /notes/{id}", api.GetNote)
mux.HandleFunc("PUT /notes/{id}", api.UpdateNote)
mux.HandleFunc("DELETE /notes/{id}", api.DeleteNote)
}
// ユーティリティ関数
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
}
// ---------- Handlers ----------
func (api *NotesAPI) ListNotes(w http.ResponseWriter, r *http.Request) {
notes := api.store.List()
writeJSON(w, http.StatusOK, notes)
}
func (api *NotesAPI) CreateNote(w http.ResponseWriter, r *http.Request) {
var input struct {
Title string `json:"title"`
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if input.Title == "" {
writeError(w, http.StatusBadRequest, "title is required")
return
}
note := api.store.Create(input.Title, input.Content)
writeJSON(w, http.StatusCreated, note)
}
func (api *NotesAPI) GetNote(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid note ID")
return
}
note, ok := api.store.Get(id)
if !ok {
writeError(w, http.StatusNotFound, "note not found")
return
}
writeJSON(w, http.StatusOK, note)
}
func (api *NotesAPI) UpdateNote(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid note ID")
return
}
var input struct {
Title string `json:"title"`
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
note, ok := api.store.Update(id, input.Title, input.Content)
if !ok {
writeError(w, http.StatusNotFound, "note not found")
return
}
writeJSON(w, http.StatusOK, note)
}
func (api *NotesAPI) DeleteNote(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid note ID")
return
}
if !api.store.Delete(id) {
writeError(w, http.StatusNotFound, "note not found")
return
}
w.WriteHeader(http.StatusNoContent)
}
func main() {
store := NewNoteStore()
api := NewNotesAPI(store)
mux := http.NewServeMux()
api.Register(mux)
log.Println("Note API 開始日: :8080")
log.Println("利用可能なエンドポイント:")
log.Println(" GET /notes — すべてのメモを表示する")
log.Println(" POST /notes — ノートを作成する")
log.Println(" GET /notes/{id} — 個別のノートを取得する")
log.Println(" PUT /notes/{id} — 更新履歴")
log.Println(" DELETE /notes/{id} — メモを削除する")
log.Fatal(http.ListenAndServe(":8080", mux))
}
http.Error(w, msg, code) では Content-Type が JSON に設定されません。JSON エラーを返す場合は、json.NewEncoder(w).Encode(errResp) を使用し、Header() を手動で設定してください。標準ライブラリの http.Error() はプレーンテキストを返します。
❓ よくある質問
Handler と HandlerFunc の違いは何ですか?Handler はインターフェース(ServeHTTP メソッドの実装が必要)であるのに対し、HandlerFunc は関数型アダプタであり、これにより通常の関数が自動的に Handler インターフェースを満たすようになります。この2つは完全に同等であり、mux.Handle("/path", handler)とmux.HandleFunc("/path", handlerFunc)は同じ効果を持ちます。r.PathValue("name") を使用して {name} 型のパスパラメータを取得します。それ以前のバージョンでは、r.URL.Path から手動で解析するか、サードパーティのライブラリを使用する必要があります。パスパラメータの名前は、ルートパターン内の {name} と一致している必要があります。ListenAndServe と ListenAndServeTLS の違いは何ですか?http.ListenAndServeTLS(":443", "cert.pem", "key.pem", mux)。サーバー.Shutdown(ctx) と os/signal を組み合わせて使用し、終了シグナルを捕捉してください。Shutdown は、すべてのアクティブな接続の処理が完了するまで待機してからシャットダウンします。サーバー.Close() は使用しないでください。これを使用すると、現在処理中のリクエストが強制的に終了されてしまいます。nilをhttp.ListenAndServeの2番目の引数として渡すとはどういう意味ですか?nilを渡すことは、http.DefaultServeMux(グローバルデフォルトルーター)が使用されることを示します。グローバルルーティングが乱れるのを防ぐため、特にテスト中にルートを分離する場合は、http.NewServeMux()を明示的に作成することをお勧めします。📖 まとめ
http.ListenAndServeが HTTP サービスを開始しますHandlerインターフェース +HandlerFuncアダプター- Go 1.22 拡張ルーティング:
"METHOD /path/{param}"構文 r.PathValue("name")は、path パラメータを取得します- JSON レスポンス:
Content-Type+json.NewEncoderを設定 - リクエストの解析:クエリパラメータ、フォーム、JSONボディ
- 静的ファイルサービス:
http.FileServer - ステータスコード:
http.Status*定数を使用する
📝 練習問題
-
基本演習(難易度 ⭐):簡単なHTTPサービスを作成し、3つのルートを登録してください。
GET /timeは現在の時刻をJSON形式で返し、GET /healthは{"ステータス": "ok"}を返し、GET /versionはバージョン番号を返します。Go 1.22の拡張ルーティング構文を使用してください。 -
上級問題(難易度 ⭐⭐):ToDoリストAPIを実装してください。要件:(1) CRUD操作をすべて実装すること;(2) Go 1.22のメソッドおよびパスベースのルーティングを使用すること;(3) リクエストとレスポンスはJSON形式とすること;(4) メモリ内ストレージ(map + RWMutexによる保護)を使用すること; (5) 適切なHTTPステータスコードを返すこと。
-
課題(難易度:⭐⭐⭐):URL短縮サービスを実装してください。要件:(1)
POST /shortenは長いURLを受け取り、短縮コード(6文字のランダムな文字列)を返すこと;(2)GET /{code}は元のURLへの301リダイレクトを実行すること; (3) アクセス統計:GET /stats/{code}は訪問回数の数値を返すこと;(4)-raceを使用して並行処理の安全性を検証すること;(5) RWMutexを使用して統計カウンターを保護すること。



