🍇
Go標準モジュールでシンプルなHTTPサーバを実装する
はじめに
Go言語は、標準ライブラリだけで非常に強力かつ高速なHTTPサーバを簡単に構築できることで知られています。本記事では、外部フレームワークを一切使わず、Goの標準モジュール(net/httpなど)だけで完結するシンプルなAPIサーバを実装していきます。
最終的に以下のようなサーバを構築します
-
PUT /store/{key}
: 任意のデータをメモリ上に保存 -
GET /store/{key}
: 保存したデータを取得 - 他のメソッド: 405エラー
- パスが不正: 404エラー
この記事は自分の復習を兼ねて段階的に実装しているため、やや冗長な部分もありますがご了承ください 🙇♂️
最小のHTTPサーバーを書いてみる
まずはローカルで"Hello World!"と表示するHTTPサーバを作成します。
main.go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello World!")
})
fmt.Println("サーバー起動中: http://localhost:8000")
http.ListenAndServe(":8000", nil)
}
以下のコマンドを実行した後、ブラウザでhttp://localhost:8000にアクセスすると「Hello World!」と無事表示されました。なお以降の動作確認はすべてCLI上でcurlコマンドを用いて行います。
go run main.go
実行結果
/store/{key}
パスをルーティング
Goのnet/httpパッケージには、フレームワークのようなルーティング機能はありません。ですが、そのぶん非常にシンプルで柔軟な設計ができます。
今回は、以下のようなルールで /store/{key} を処理します:
- keyは英数字のみ、1~10文字
- パス形式:
/store/{key}
- 無効なパスや無効なkeyには404を返す
main.go
package main
import (
"fmt"
"net/http"
+ "regexp"
+ "strings"
)
+var keyPattern = regexp.MustCompile(`^[a-zA-Z0-9]{1,10}$`)
+
+func storeHandler(w http.ResponseWriter, r *http.Request) {
+ if !strings.HasPrefix(r.URL.Path, "/store/") {
+ http.NotFound(w, r)
+ return
+ }
+
+ key := strings.TrimPrefix(r.URL.Path, "/store/")
+
+ if !keyPattern.MatchString(key) {
+ http.NotFound(w, r)
+ return
+ }
+
+ fmt.Fprintf(w, "Accessed /store with key: %s\n", key)
+}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- fmt.Fprintln(w, "Hello World!")
+ if strings.HasPrefix(r.URL.Path, "/store/") {
+ storeHandler(w, r)
+ return
+ }
+ http.NotFound(w, r)
+ })
fmt.Println("サーバー起動中: http://localhost:8000")
http.ListenAndServe(":8000", nil)
}
test:
curl http://localhost:8000/store/hello
# => Accessed /store with key: hello
curl http://localhost:8000/store/invalid!
# => 404 page not found
curl http://localhost:8000/other
# => 404 page not found
curl http://localhost:8000/abcdefghijkl
# => 404 page not found
PUTメソッドでデータを保存
PUT /store/{key}
に対して、任意のバイナリデータを受け取り、メモリ上に保存できるようにします。
main.go
package main
import (
"fmt"
+ "io"
"net/http"
"regexp"
"strings"
+ "sync"
)
var keyPattern = regexp.MustCompile(`^[a-zA-Z0-9]{1,10}$`)
+type MemoryStore struct {
+ data map[string][]byte
+ mu sync.RWMutex
+}
+
+func NewMemoryStore() *MemoryStore {
+ return &MemoryStore{
+ data: make(map[string][]byte),
+ }
+}
+
+func (s *MemoryStore) Put(key string, value []byte) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.data[key] = value
+}
+
+func (s *MemoryStore) Get(key string) ([]byte, bool) {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ val, ok := s.data[key]
+ return val, ok
+}
+
+var store = NewMemoryStore()
func storeHandler(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/store/") {
http.NotFound(w, r)
return
}
key := strings.TrimPrefix(r.URL.Path, "/store/")
if !keyPattern.MatchString(key) {
http.NotFound(w, r)
return
}
- fmt.Fprintf(w, "Accessed /store with key: %s\n", key)
+ switch r.Method {
+ case http.MethodPut:
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ http.Error(w, "Failed to read body", http.StatusInternalServerError)
+ return
+ }
+ store.Put(key, body)
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, "Stored key=%s (status=%d)\n", key, http.StatusOK)
+ default:
+ http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
+ }
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/store/") {
storeHandler(w, r)
return
}
http.NotFound(w, r)
})
fmt.Println("サーバー起動中: http://localhost:8000")
http.ListenAndServe(":8000", nil)
}
test:
curl -X PUT http://localhost:8000/store/hello -d 'Hello, World!'
# => Stored key=hello (status=200)
curl -X POST http://localhost:8000/store/hello
# => Method Not Allowed
GETメソッドでデータを取り出す
保存されたデータを GET /store/{key}
で取得できるようにします。storeHandler関数内のswitch文の中身を以下のように書き換えます。
main.go
switch r.Method {
case http.MethodPut:
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusInternalServerError)
return
}
store.Put(key, body)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Stored key=%s (status=%d)\n", key, http.StatusOK)
+case http.MethodGet:
+ value, ok := store.Get(key)
+ if !ok {
+ http.Error(w, fmt.Sprintf("Key '%s' not found (status=%d)", key, http.StatusNotFound), http.StatusNotFound)
+ return
+ }
+ w.WriteHeader(http.StatusOK)
+ w.Write(value)
default:
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
test:
# 保存
curl -X PUT http://localhost:8000/store/hello -d 'Hello, World!'
# => Stored key=hello (status=200)
# 取得
curl http://localhost:8000/store/hello
# => Hello, World!
# 存在しないkey
curl http://localhost:8000/store/notfound
# => Key 'notfound' not found (status=404)
POST, DELETEなどの不正メソッドと不正パスを処理
最後に、以下の2つのエラーハンドリングを追加します。
-
/store/{key}
に対するPUT・GET以外のメソッドには405を返す -
/store/
以外の全てのパスに対して404を返す
main.go
package main
import (
"fmt"
"io"
"net/http"
"regexp"
"strings"
"sync"
)
var keyPattern = regexp.MustCompile(`^[a-zA-Z0-9]{1,10}$`)
type MemoryStore struct {
data map[string][]byte
mu sync.RWMutex
}
func NewMemoryStore() *MemoryStore {
return &MemoryStore{
data: make(map[string][]byte),
}
}
func (s *MemoryStore) Put(key string, value []byte) {
s.mu.Lock()
defer s.mu.Unlock()
s.data[key] = value
}
func (s *MemoryStore) Get(key string) ([]byte, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
val, ok := s.data[key]
return val, ok
}
var store = NewMemoryStore()
func storeHandler(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/store/") {
http.NotFound(w, r)
return
}
key := strings.TrimPrefix(r.URL.Path, "/store/")
if !keyPattern.MatchString(key) {
- http.NotFound(w, r)
+ http.Error(w, fmt.Sprintf("Invalid key format: '%s'", key), http.StatusNotFound)
return
}
switch r.Method {
case http.MethodPut:
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusInternalServerError)
return
}
store.Put(key, body)
w.WriteHeader(http.StatusOK)
- fmt.Fprintf(w, "Stored key=%s (status=%d)\n", key, http.StatusOK)
+ fmt.Fprintf(w, "Stored key='%s' (status=200)\n", key)
case http.MethodGet:
value, ok := store.Get(key)
if !ok {
- http.Error(w, fmt.Sprintf("Key '%s' not found (status=%d)", key, http.StatusNotFound), http.StatusNotFound)
+ http.Error(w, fmt.Sprintf("Key '%s' not found (status=404)", key), http.StatusNotFound)
return
}
w.WriteHeader(http.StatusOK)
w.Write(value)
default:
- http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
+ http.Error(w, fmt.Sprintf("Method %s not allowed (status=405)", r.Method), http.StatusMethodNotAllowed)
}
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/store/") {
storeHandler(w, r)
return
}
- http.NotFound(w, r)
+ http.Error(w, "Not Found (status=404)", http.StatusNotFound)
})
fmt.Println("サーバー起動中: http://localhost:8000")
http.ListenAndServe(":8000", nil)
}
curl -X PUT http://localhost:8000/store/abc123 -d 'data'
# => Stored key='abc123' (status=200)
curl http://localhost:8000/store/abc123
# => data
curl -X POST http://localhost:8000/store/abc123
# => Method POST not allowed (status=405)
curl http://localhost:8000/store/INVALID_KEY!
# => Invalid key format: 'INVALID_KEY!'
curl http://localhost:8000/unknown/path
# => Not Found (status=404)
おわりに
今回のサンプルは非常にシンプルながら、Goの標準ライブラリだけで以下のことを実現できました。
- ルーティング
- メソッドごとの処理分岐
- エラーハンドリング(404, 405)
- スレッドセーフなメモリデータストア
今後は、JSON形式のAPI対応や認証機能、自動テストなど構築していきたいと思います。
Discussion
こんにちは!
パス(
/store
)とHTTPメソッドの分岐処理を全て一つのハンドラーの中で行われていますが、Go1.22からHTTPメソッドも含めてHandleFunc
のパターン部分で定義できます。(パスについてはそれ以前から可能)詳細はこちらをご覧になると良いと思います。良いGoライフを!
参考記事まで掲載して頂き誠にありがとうございます。
HundleFunc
の方がより簡潔にかけてしまいますね、、積極的にキャッチアップしていこうと思います。