シンプルで高速なWebサーバーを構築するなら、Goは本当に良い選択肢だと思います。標準ライブラリのnet/httpパッケージを使えば、複雑なフレームワークに依存することなく、わずか数行のコードで本格的なHTTPサーバーを立ち上げられるんですよね。この記事では、Goでのサーバー開発の基本から応用まで、実装しながら一緒に学んでいきましょう。
Goでサーバーを選ぶ理由
Goがサーバーサイド開発に向いているのは、言語設計そのものに理由があります。実際のプロジェクトで感じた優位性も含めて、以下の特徴が挙げられます:
- 軽量で高速:コンパイル型言語で実行速度が速く、メモリ効率も優れているため、本番環境のリソース消費を抑えられます
- 並行処理が得意:ゴルーチンにより数千の同時接続を効率的に処理できるので、スケーラビリティに優れています
- 標準ライブラリが充実:net/httpだけで本格的なサーバーが構築可能で、余分な依存関係を減らせます
- デプロイが簡単:単一のバイナリファイルとしてコンパイルされるため、サーバーへのデプロイが非常にスムーズです
PythonのFlaskやNode.jsのExpressも確かに優秀ですが、Goは性能と開発効率のバランスに優れていると実感しています。
最小限のHTTPサーバーを構築する
まず、Goでの最もシンプルなサーバー実装から始めてみましょう。
package main
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
http.HandleFunc("/", helloHandler)
http.ListenAndServe(":8080", nil)
}
このコードの流れを説明していきますね:
- helloHandler:クライアントからのリクエストに対応するハンドラー関数。
http.ResponseWriterにデータを書き込みます - http.HandleFunc():指定したパスとハンドラーを紐付ける関数。複数回呼び出して複数のエンドポイントを登録できます
- http.ListenAndServe():8080番ポートでサーバーを起動し、接続待ち受け状態になります
実行するには go run main.go を実行し、ブラウザで http://localhost:8080 にアクセスすれば「Hello, World!」が表示されます。シンプルですが、これが全ての基礎になるんです。
複数のエンドポイントを処理する
実際のプロジェクトでは、複数のパスに対応する必要が出てきます。以下の例を見てください:
package main
import (
"fmt"
"net/http"
)
func homeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "ホームページです")
}
func aboutHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "このサイトについて")
}
func apiHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"message": "API response"}`)
}
func main() {
http.HandleFunc("/", homeHandler)
http.HandleFunc("/about", aboutHandler)
http.HandleFunc("/api/data", apiHandler)
http.ListenAndServe(":8080", nil)
}
複数のエンドポイントを登録することで、それぞれのパスに異なるレスポンスを返せます。ここで気をつけるべきは、JSONレスポンスの場合はContent-Typeヘッダーを適切に設定すること。これを忘れると、クライアント側で正しく解析されない場合があります。調べてみたら、ブラウザはこのヘッダーを参考にしてレスポンスの扱いを判断しているんです。
HTTPメソッドの判定
RESTful APIを構築する際は、GET、POST、PUT、DELETEなどのメソッドを区別する必要があります。ここが結構重要なポイントです。
package main
import (
"fmt"
"net/http"
)
func dataHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
fmt.Fprintf(w, "データを取得しました")
case "POST":
fmt.Fprintf(w, "データを保存しました")
case "PUT":
fmt.Fprintf(w, "データを更新しました")
case "DELETE":
fmt.Fprintf(w, "データを削除しました")
default:
http.Error(w, "メソッドが許可されていません", http.StatusMethodNotAllowed)
}
}
func main() {
http.HandleFunc("/data", dataHandler)
http.ListenAndServe(":8080", nil)
}
リクエストのr.MethodプロパティでHTTPメソッドを判定し、適切なレスポンスを返します。この辺りは初心者がうっかり見落とすんですが、無効なメソッドに対してはhttp.StatusMethodNotAllowed(405エラー)を返すのがベストプラクティスです。こうすることで、クライアント側もサーバーの意図を正確に理解できるようになります。
クエリパラメータとボディの処理
現場のサーバーでは、クライアントから送られてくるデータを処理する場面が避けられません。
package main
import (
"fmt"
"net/http"
)
func searchHandler(w http.ResponseWriter, r *http.Request) {
// クエリパラメータを取得
query := r.URL.Query().Get("q")
if query == "" {
http.Error(w, "クエリパラメータが必要です", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "検索キーワード: %s", query)
}
func formHandler(w http.ResponseWriter, r *http.Request) {
// フォームデータを解析
r.ParseForm()
name := r.FormValue("name")
email := r.FormValue("email")
fmt.Fprintf(w, "名前: %s, メール: %s", name, email)
}
func main() {
http.HandleFunc("/search", searchHandler)
http.HandleFunc("/form", formHandler)
http.ListenAndServe(":8080", nil)
}
r.URL.Query().Get()でクエリパラメータを、r.FormValue()でPOSTフォームデータを取得できます。最初ここで失敗したんですが、常にバリデーションを行い、必要なデータが実際に存在するか確認することが重要です。空の値がきた場合の処理も想定しておくと、本番環境での思わぬエラーを防げます。
静的ファイルの配信
HTMLやCSSなどの静的ファイルをサーブする場合は、http.FileServer()を使用します。
package main
import (
"net/http"
)
func main() {
// 静的ファイルの配信
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("./public"))))
// 動的コンテンツ
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
http.ServeFile(w, r, "./index.html")
})
http.ListenAndServe(":8080", nil)
}
http.StripPrefix()を使うことで、URLパスから接頭辞を削除し、正しいファイルパスにマッピングできるんですね。これにより /public/style.css は ./public/style.css から読み込まれます。公式ドキュメントを読んでみたら、このアプローチは一般的で、多くのプロジェクトで採用されているパターンのようです。
エラーハンドリングとステータスコード
適切なHTTPステータスコードを返すことは、APIの信頼性に関わってくるポイントです。
package main
import (
"encoding/json"
"net/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// 例: IDが見つからない場合
id := r.URL.Query().Get("id")
if id == "" {
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": "IDが必要です"})
return
}
// 成功時
w.WriteHeader(http.StatusOK)
user := User{ID: 1, Name: "Taro"}
json.NewEncoder(w).Encode(user)
}
func main() {
http.HandleFunc("/user", getUserHandler)
http.ListenAndServe(":8080", nil)
}
w.WriteHeader()でステータスコードを明示的に設定できます。一般的なコードは以下の通りです:
- 200 OK:成功した場合に返す基本的なコード
- 400 Bad Request:不正なリクエストであることをクライアントに伝える
- 404 Not Found:リソースが見つからないことを示す
- 500 Internal Server Error:サーバー側のエラーが発生した時に使用
ロギングとデバッグ
本番環境ではリクエストログを記録することが本当に重要です。以下はシンプルなロギング機能の例になります:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next(w, r)
elapsed := time.Since(start)
log.Printf("%s %s %s (%.3fs)", r.Method, r.URL.Path, r.RemoteAddr, elapsed.Seconds())
}
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello!")
}
func main() {
http.HandleFunc("/", loggingMiddleware(helloHandler))
log.Println("サーバーが起動しました: http://localhost:8080")
http.ListenAndServe(":8080", nil)
}
このアプローチでは、ミドルウェアを使用してすべてのリクエストをログに記録し、実行時間も測定しています。デバッグや性能改善に非常に役立つんです。実際のプロジェクトで本当にお世話になった機能で、どの処理が遅いのか、どのエンドポイントがよくアクセスされるのか、これで一目瞭然になります。
まとめ
GoのHTTPサーバー開発について、ゼロから実装できるレベルまで学習しました。以下が要点です:
- net/httpパッケージを使えば、複雑なフレームワークなしでサーバーを構築可能だと分かりました
- http.HandleFunc()でエンドポイントを定義し、複数のパスに対応できます
- HTTPメソッドの判定でRESTful APIを実装する際の基本となります
- クエリパラメータとフォームデータの処理はGoの標準機能で十分対応可能です
- 静的ファイルの配信はhttp.FileServer()を活用することで簡潔に実装できます
- 適切なステータスコードを返し、エラーハンドリングを意識することが重要です
- ロギング機能でデバッグと性能監視を実現し、本番環境での問題解決に役立ちます
Goでのサーバー開発は習得が容易でありながら、スケーラビリティにも優れているんですよね。これらの基本を習得すれば、本番レベルのWebサーバーを構築できるようになります。
よくある質問
複数のハンドラーを同じパスに登録するにはどうしたらいい?
Goの標準ライブラリでは、同じパスに複数のハンドラーを登録することはできません。ただし、1つのハンドラーの中でr.Methodを使ってメソッドを判定し、異なる処理を実行する方法があります。より複雑なルーティングが必要な場合は、Gorilla Muxなどのルーティングライブラリを導入するのがおすすめです。
サーバーを停止するにはどうしたらいい?
ターミナルで Ctrl+C を押すことで、http.ListenAndServe()が停止します。本番環境でより制御が必要な場合は、http.Server構造体を直接使用し、Shutdown()メソッドで計画的にサーバーをシャットダウンできます。
JSONのマーシャリング時に、特定のフィールドを除外したい場合は?
構造体のフィールドにjson:"-"というタグを付けることで、そのフィールドはJSONの出力から除外されます。また、json:"fieldName,omitempty"のようにomitemptyを指定すれば、ゼロ値の場合は出力されません。
大きなファイルのアップロードを処理するときに注意することは?
r.ParseForm()を呼ぶ前に、r.ParseMultipartForm(maxSize)を使ってメモリに読み込むサイズを制限することが重要です。これにより、メモリ使用量を抑制し、サーバーの安定性を保つことができます。
複数のサーバーを同時に起動するにはどうしたらいい?
ゴルーチンを使用して、複数のhttp.ListenAndServe()を並行実行できます。例えば、go http.ListenAndServe(":8080", nil)とgo http.ListenAndServe(":8081", nil)を同時に実行し、複数ポートでサーバーを起動することが可能です。

