GoでGin Webフレームワークをゼロから再実装


[![Group177.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/7b5262c9-7ba6-4fa8-9ee4-7330af27c8d6.png)](https://leapcell.io/?lc_t=d_gogin)



**[Leapcell: サーバレスウェブホスティングの最良の選択肢](https://leapcell.io/?lc_t=d_gogin)**

# Go言語の`net`パッケージを使ってGinに似たHTTPルータを実装する

## 1. はじめに
現代のウェブ開発において、効率的で柔軟なルーティングシステムはウェブアプリケーションを構築するための核心的なコンポーネントの一つです。Goプログラミング言語は、その高いパフォーマンス、シンプルさ、そして強力な標準ライブラリのおかげで、ウェブ開発の分野で大変好まれています。Goの`net/http`パッケージは、標準ライブラリにおけるHTTPサーバの実装です。これは強力ですが、比較的低レベルなものです。もし、Ginのような軽量ウェブフレームワークのようにルーティングを扱いたい場合、私たちは自分で簡略化されたルータを実装することができます。この記事では、Go言語の`net`パッケージを使って、Ginに似たHTTPサーバを実装する方法について詳細に紹介します。同時に、HTTP関連の知識、一般的なルーティングの実装方法、そしてそれを基にミドルウェアを実装する方法についても掘り下げていきます。

## 2. HTTPの基礎知識の復習

### 2.1 HTTPのリクエストとレスポンス
HTTP（Hypertext Transfer Protocol）は、ハイパーテキストを転送するために使われるプロトコルであり、ウェブアプリケーションの基礎となります。HTTPリクエストは通常、以下の部分で構成されます：
- **リクエストライン**：リクエストメソッド（例えば`GET`、`POST`、`PUT`、`DELETE`など）、リクエストされたURI（Uniform Resource Identifier）、そしてHTTPのバージョンを含みます。
- **リクエストヘッダ**：リクエストに関する追加情報を含み、例えば`User-Agent`、`Content-Type`などがあります。
- **リクエストボディ**：リクエストのデータを含み、通常`POST`または`PUT`リクエストで使用されます。

HTTPレスポンスもいくつかの部分で構成されます：
- **ステータスライン**：HTTPのバージョン、ステータスコード（例えば`200`は成功を示し、`404`は見つからないことを示し、`500`はサーバ内部エラーを示すなど）、そしてステータスメッセージを含みます。
- **レスポンスヘッダ**：レスポンスに関する追加情報を含み、例えば`Content-Type`、`Content-Length`などがあります。
- **レスポンスボディ**：レスポンスのデータを含み、例えばHTMLページ、JSONデータなどがあります。

### 2.2 HTTPメソッド
一般的なHTTPメソッドには以下があります：
- **GET**：リソースを取得するために使用されます。
- **POST**：サーバにデータを送信するために使用され、通常新しいリソースを作成するために使われます。
- **PUT**：リソースを更新するために使用されます。
- **DELETE**：リソースを削除するために使用されます。

異なるHTTPメソッドには異なるセマンティクスがあり、ルーティングシステムを設計する際には、異なるメソッドに応じてリクエストを処理する必要があります。

## 3. 一般的なルーティングの実装方法

### 3.1 静的ルーティング
静的ルーティングは最も単純なルーティング方法であり、固定されたURLパスを特定のハンドラ関数にマッピングします。例えば、`/about`パスを、会社概要ページを表示するハンドラ関数にマッピングするなどです。

### 3.2 動的ルーティング
動的ルーティングでは、URLパスにパラメータを含めることができ、これらのパラメータはハンドラ関数内で取得することができます。例えば、`/users/:id`の`:id`はパラメータであり、特定のユーザーの情報を取得するために使用することができます。

### 3.3 正規表現によるルーティング
正規表現によるルーティングでは、正規表現を使ってURLパスをマッチさせることができます。この方法はより柔軟であり、複雑なルーティングルールを処理することができます。例えば、`.html`で終わるすべてのURLパスを正規表現を使ってマッチさせるなどです。

## 4. 設計アイデア
ルーティング機能を実装するために、私たちは以下のことが必要です：
- HTTPリクエストのパスとメソッドを解析する。
- 異なるパスとメソッドに対するハンドラ関数を保存する。
- 動的ルーティングのパラメータを解析する。
- 404エラーを処理する。

私たちは`map`構造体を使ってルーティングルールを保存します。各パスは異なるHTTPメソッドに対応し、これにより効率的にリクエストをマッチさせることができます。具体的には、ネストされた`map`を使用します。外側の`map`のキーはHTTPメソッド、内側の`map`のキーはURLパス、値は対応するハンドラ関数です。

## 5. コード実装

```go
package main

import (
	"fmt"
	"net/http"
	"strings"
)

// Router構造体はルーティングルールを保存するために使用されます
type Router struct {
	routes map[string]map[string]http.HandlerFunc
}

// NewRouter関数は新しいルータインスタンスを作成します
func NewRouter() *Router {
	return &Router{
		routes: make(map[string]map[string]http.HandlerFunc),
	}
}

// Handleメソッドはルートを登録するために使用されます
func (r *Router) Handle(method, path string, handler http.HandlerFunc) {
	if _, ok := r.routes[method];!ok {
		r.routes[method] = make(map[string]http.HandlerFunc)
	}
	r.routes[method][path] = handler
}

// ServeHTTPメソッドはHTTPリクエストを解析し、対応するハンドラ関数を呼び出します
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	methodRoutes, ok := r.routes[req.Method]
	if!ok {
		http.NotFound(w, req)
		return
	}

	handler, ok := methodRoutes[req.URL.Path]
	if!ok {
		// 動的ルーティングを処理する
		for route, h := range methodRoutes {
			if params := matchDynamicRoute(route, req.URL.Path); params != nil {
				req.URL.Query().Set("params", strings.Join(params, ","))
				h(w, req)
				return
			}
		}
		http.NotFound(w, req)
		return
	}

	handler(w, req)
}

// matchDynamicRoute関数は動的ルートをマッチさせるために使用されます
func matchDynamicRoute(route, path string) []string {
	routeParts := strings.Split(route, "/")
	pathParts := strings.Split(path, "/")

	if len(routeParts) != len(pathParts) {
		return nil
	}

	var params []string
	for i, part := range routeParts {
		if strings.HasPrefix(part, ":") {
			params = append(params, pathParts[i])
		} else if part != pathParts[i] {
			return nil
		}
	}

	return params
}

func main() {
	router := NewRouter()

	// 静的ルートを登録する
	router.Handle("GET", "/", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintf(w, "Hello, world!")
	})

	// 動的ルートを登録する
	router.Handle("GET", "/hello/:name", func(w http.ResponseWriter, req *http.Request) {
		params := req.URL.Query().Get("params")
		name := strings.Split(params, ",")[0]
		fmt.Fprintf(w, "Hello, %s!", name)
	})

	http.ListenAndServe(":8080", router)
}

```

### コードの説明
- `Router`構造体：ルーティングルールを保存するために使用され、ネストされた`map`を使用して異なるHTTPメソッドとURLパスに対応するハンドラ関数を保存します。
- `NewRouter`関数：新しいルータインスタンスを作成するために使用されます。
- `Handle`メソッド：ルートを登録するために使用され、HTTPメソッド、URLパス、そしてハンドラ関数を`Router`構造体に保存します。
- `ServeHTTP`メソッド：HTTPリクエストを解析するために使用されます。まず、リクエストされたHTTPメソッドが存在するかをチェックし、次にURLパスがマッチするかをチェックします。一致する静的ルートがない場合、動的ルートのマッチを試みます。
- `matchDynamicRoute`関数：動的ルートをマッチさせるために使用され、URLパスのパラメータが一致するかをチェックします。

## 6. 実行とテスト
コードを`main.go`として保存し、次のコマンドで実行します：
```sh
go run main.go
```
その後、以下のURLにアクセスします：
- `http://localhost:8080/`は`"Hello, world!"`を返します
- `http://localhost:8080/hello/Go`は`"Hello, Go!"`を返します
- その他のパスを訪問すると`404 Not Found`を返します

## 7. ミドルウェアの実装
ミドルウェアは、リクエストを処理する前または後に実行される関数です。これはログ記録、認証、エラー処理などに使用することができます。私たちのルータでミドルウェアを実装するのは非常に簡単です。私たちは`http.HandlerFunc`を受け取り、新しい`http.HandlerFunc`を返す関数型を定義するだけです。

```go
// Middlewareはミドルウェア関数型です
type Middleware func(http.HandlerFunc) http.HandlerFunc

// Loggerは簡単なログ記録用のミドルウェアです
func Logger(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, req *http.Request) {
		fmt.Printf("Received %s request for %s\n", req.Method, req.URL.Path)
		next(w, req)
	}
}

func main() {
	router := NewRouter()

	// 静的ルートを登録し、ミドルウェアを適用する
	router.Handle("GET", "/", Logger(func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintf(w, "Hello, world!")
	}))

	// 動的ルートを登録し、ミドルウェアを適用する
	router.Handle("GET", "/hello/:name", Logger(func(w http.ResponseWriter, req *http.Request) {
		params := req.URL.Query().Get("params")
		name := strings.Split(params, ",")[0]
		fmt.Fprintf(w, "Hello, %s!", name)
	}))

	http.ListenAndServe(":8080", router)
}

```

### コードの説明
- `Middleware`型：`http.HandlerFunc`を受け取り、新しい`http.HandlerFunc`を返すミドルウェア関数型を定義します。
- `Logger`関数：簡単なログ記録用のミドルウェアで、リクエストを処理する前にリクエストメソッドとURLパスを出力します。
- `main`関数では、各ルートのハンドラ関数に`Logger`ミドルウェアを適用しています。

## 8. まとめ
このチュートリアルでは、Go言語の`net/http`パッケージを使って簡単なウェブルータを実装する方法を示しました。同時に、HTTP関連の知識、一般的なルーティングの実装方法、そしてそれを基にミドルウェアを実装する方法についても紹介しました。あなたはこの基礎の上で機能を拡張することができます。例えば：
- `PUT`、`DELETE`などのより多くのHTTPメソッドをサポートすること。
- より複雑なミドルウェア関数を追加すること、例えば認証やレート制限など。
- より高度なパラメータ解析を実装すること、例えば正規表現によるルーティング。

このようにすることで、私たちはHTTPサーバの低レベルの動作原理を習得するだけでなく、独自のウェブフレームワークをカスタマイズすることができ、Go言語の高いパフォーマンスと柔軟性を楽しむことができます。

# [Leapcell: サーバレスウェブホスティングの最良の選択肢](https://leapcell.io/?lc_t=d_gogin)

最後に、Goサービスをデプロイするための最高のプラットフォームである**[Leapcell](https://leapcell.io/?lc_t=d_gogin)** をおすすめします。

[![brandpic7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/46690539-3c5c-474b-b5b1-29e147d4d662.png)](https://leapcell.io/?lc_t=d_gogin)


### 🚀 好きな言語で構築する
JavaScript、Python、Go、またはRustで簡単に開発できます。  

### 🌍 無料で無制限のプロジェクトをデプロイする
使用した分だけ支払います — リクエストがなければ、請求はありません。  

### ⚡ 使った分だけ支払い、隠された費用はありません
アイドル料金はなく、シームレスにスケーリングできます。  

[![Frame3-withpadding2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/19b8da7a-0611-4827-87cb-462eb33e3c15.png)](https://leapcell.io/?lc_t=d_gogin)


[📖 ドキュメントを参照する](https://docs.leapcell.io/)  

🔹 Twitterでフォローしてください：[@LeapcellHQ](https://x.com/LeapcellHQ)