【Go入門】Goで自作フレームワークを作りたい① ~最低限の機能を持つフレームワーク作成編~

# 本記事で書くこと
ルーティング機能を提供し、GETおよびPOSTリクエストを処理することができるシンプルなHTTPサーバーフレームワークを作成から公開、挙動確認するまでの手順について記載します。

# 対象者
Goで簡単なフレームワークを作ってみたい人。


# 事前準備
## 1. リポジトリの作成
githubで自作フレームワーク用のリポジトリを作成します。
今回は「fw-sample」という名前にします。

## 2. ローカルリポジトリの設定
ローカルに「fw-sample」ディレクトリを作成し、そのディレクトリに移動します。
その後以下のコマンドを実行して、Gitリポジトリを初期化し、リモートリポジトリを追加します。

```shell
git init
git remote add origin https://github.com/ユーザー名/リポジトリ名.git
```
## 3. 正しく設定されているか確認
一旦うまく設定できているか適当なファイルを追加してみて確認してみます。

```
git add .
git commit -m "initial commit"
git push -u origin main
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3933650/8dcaf4aa-7925-470a-9e96-3e9935d54a31.png)

以下のようになればOK。
```shell
$ git push -u origin main
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 746 bytes | 746.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/k-tsurumaki/fw-sample.git
 * [new branch]      main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.
```

# フレームワーク作成
いったんGETとPOSTリクエストのみ処理することができるサンプルを実装してみます。
以下の内容の`fw-sample.go`ファイルを作成します。

```fw-sample.go
package fwsample

import (
	"errors"
	"net/http"
)

// アプリケーション全体を管理する構造体
type App struct {
	Router *Router
}

// ルーティング処理を担当する構造体
type Router struct {
	routingTable map[string]map[string]func(http.ResponseWriter, *http.Request)
}

// ルーティングテーブルにハンドラを追加する関数
func (r *Router) add(method, path string, handler func(http.ResponseWriter, *http.Request)) error {
	if r.routingTable[method] == nil {
		r.routingTable[method] = make(map[string]func(http.ResponseWriter, *http.Request))
	}
	if r.routingTable[method][path] != nil {
		return errors.New("handler already exists")
	}
	r.routingTable[method][path] = handler
	return nil
}

func (r *Router) Add(method, path string, handler func(http.ResponseWriter, *http.Request)) error {
	if method != http.MethodGet && method != http.MethodPost {
		return errors.New("unsupported method")
	}
	return r.add(method, path, handler)
}


// GETメソッドのハンドラを追加する関数
func (r *Router) Get(path string, handler func(http.ResponseWriter, *http.Request)) error {
	return r.Add(http.MethodGet, path, handler)
}

// POSTメソッドのハンドラを追加する関数
func (r *Router) Post(path string, handler func(http.ResponseWriter, *http.Request)) error {
	return r.Add(http.MethodPost, path, handler)
}

// ServeHTTPメソッドを実装することで、http.Handlerインターフェースを満たすようになる
// これにより、http.ListenAndServeに渡すことができるようになる
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if handlers, ok := a.Router.routingTable[r.Method]; ok {
            if handler, ok := handlers[r.URL.Path]; ok {
                handler(w, r)
                return
            }
        }
		// ルーティングにマッチしなかった場合は404エラーを返す
		http.NotFound(w, r)
	})
	handler.ServeHTTP(w, r)
}

// App構造体の新しいインスタンスを作成し、初期化する関数
func New() *App {
	return &App{
		Router: &Router{
			routingTable: make(map[string]map[string]func(http.ResponseWriter, *http.Request)),
		},
	}
}

// アプリケーションを指定したアドレスで起動する関数
func (a *App) Run(addr string) error {
	return http.ListenAndServe(addr, a)
}


```

### コード説明
```go
type App struct {
	Router *Router
}

type Router struct {
	routingTable map[string]map[string]func(http.ResponseWriter, *http.Request)
}

```
`App`はアプリケーション全体を管理する構造体で、`Router`を内部に持ち、リクエストのルーティングを処理します。

`Router`はルーティング処理を担当する構造体です。
リクエストのメソッドとパスに基づいてハンドラの登録や実行を行います。


```go
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if handlers, ok := a.Router.routingTable[r.Method]; ok {
            if handler, ok := handlers[r.URL.Path]; ok {
                handler(w, r)
                return
            }
        }
		// ルーティングにマッチしなかった場合は404エラーを返す
		http.NotFound(w, r)
	})
	handler.ServeHTTP(w, r)
}
```

また`App`には`HTTPServer()`を定義しています。
これにより、`http.Handler`インターフェースを満たすようになり、`http.ListenAndServe`に渡すことができるようになります。

`HTTPServer()`内では`Router`の`routingTable`にハンドラが登録されているかを確認します。
登録されている場合はそのハンドラを実行し、登録されていない場合は404 NotFoundを返します。


```go
func (r *Router) add(method, path string, handler func(http.ResponseWriter, *http.Request)) error {
	if r.routingTable[method] == nil {
		r.routingTable[method] = make(map[string]func(http.ResponseWriter, *http.Request))
	}
	if r.routingTable[method][path] != nil {
		return errors.New("handler already exists")
	}
	r.routingTable[method][path] = handler
	return nil
}

func (r *Router) Add(method, path string, handler func(http.ResponseWriter, *http.Request)) error {
	if method != http.MethodGet && method != http.MethodPost {
		return errors.New("unsupported method")
	}
	return r.add(method, path, handler)
}

func (r *Router) Get(path string, handler func(http.ResponseWriter, *http.Request)) error {
	return r.Add(http.MethodGet, path, handler)
}

func (r *Router) Post(path string, handler func(http.ResponseWriter, *http.Request)) error {
	return r.Add(http.MethodPost, path, handler)
}
```
ハンドラの登録は`Router`が持つ`Get`・`Post`関数で行います。
パスとハンドラの組み合わせを`routingTable`に登録します。


```go
func (a *App) Run(addr string) error {
	return http.ListenAndServe(addr, a)
}
```

ユーザ側では`Run`に引数としてアドレス（例：":8080"）を渡して実行することで、HTTPサーバを起動できます。

### 挙動確認
他のプロジェクト内でサーバを立ててみます。
新しくプロジェクトを作成し、以下のコマンドを実行します
```shell
go get github.com/[ユーザ名]/fw-sample
```

以下の内容の`main.go`ファイルを作成し、実行します。
```go
package main

import (
    "log"
    "net/http"
    "fwsample"
)

func main() {
    app := fwsample.New()

    // ルートの登録
    app.Router.Get("/hello", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })

    app.Router.Post("/echo", func(w http.ResponseWriter, r *http.Request) {
        body := make([]byte, r.ContentLength)
        r.Body.Read(body)
        w.Write(body)
    })

    // サーバーの起動
    log.Println("Starting server on :8080")
    if err := app.Run(":8080"); err != nil {
        log.Fatal(err)
    }
}
```

ブラウザから`localhost:8080/hello`にアクセスしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3933650/3f58fa7f-e2f8-4000-aa16-469b552b872d.png)

無事「Hello, World!」が表示されています。

続いてcurlでPOSTリクエストを送ってみます。
```shell
$curl -X POST http://localhost:8080/echo -d "Hello, Echo!"
```

以下のような回答が返ってくれば正しく動作しています。
```shell
Hello, Echo!
```
# 機能の追加
より汎用的なフレームワークにするためには、以下のような機能を追加する必要があります。

### ミドルウェアのサポート
通常のサーバはリクエストの前後でログの記録や認証、CORS設定などの共通処理を行っています。
これらを実装するためにはミドルウェア機能を追加する必要があります。

### パスパラメータのサポート
現在は静的なパス（例：`/hello`）しかサポートしていません。
実際にサーバとして用いるためには、動的なパス（例：`/users/:id`）もサポートする必要があります。

### エラーハンドリングのカスタマイズ
現在はエラー時のレスポンスが固定されていますが、ユーザがエラー時のレスポンスをカスタマイズできるようにすべきです。

他にもインターフェースを導入したりなどいろいろありますが、それについては別の記事で書きたいと思います！