🌐

【Go】net/httpパッケージだけでwebサーバーを実装したい

2024/12/02に公開

最近golangを勉強しています。結構ハマってます(悪い意味の方でなく笑)。
具体的に自分が好きなポイントの一つは、goがbattery includedな言語である点です。

しかしながら、フレームワークを含め、サードパーティライブラリは多く存在し、多くの開発現場ではそれらが駆使されています。

「...標準だけでなんとかならんものかなぁ??」

webアプリケーションの根幹部分と言えばwebサーバーだ!ということで、ピュアなgolangでどのようにしてwebサーバーを実装するのかが気になり、調べつつ簡易的に実装してみたので、記事に残すことにしました。
バージョン1.22で強化されたルーティング周りの機能を活用しており、その辺がこの記事の目玉かなと思います。

数多くあるフレームワークを差し置いて標準パッケージだけで実装したくなったその経緯から述べていきます。

net/httpの使い方についてあまり知らない方も読めるように説明をちょこちょこ入れているつもりですが、理解を深めるためにおすすめの記事や書籍を最後にリンクとして貼っておりますので、そちらもご確認ください。

目次

  1. webサーバーを実装する選択肢
  2. 実装&解説
  3. 感想

1. webサーバーを実装する選択肢

人気のフレームワークが多い件(初学者は若干困惑)

初めに述べたように、大体の機能は標準パッケージで賄えます。
とはいえ、利便性や求める機能要件から、実際の開発現場では有名どころのサードパーティパッケージが使用されることと思います。

それに、フレームワークも多く存在します。
有名どころでいえば、

  • gin(⭐️79.2k)
  • echo(⭐️30k)
  • beego(⭐️31.7k)
  • Fiber(⭐️34.1k)

あたりでしょうか。多くの企業さんだと、何かしらのフレームワークを使って開発していることが多いようです。⭐️はgithubスター数です(2024/12/1時点)。

ただ、そこで思ったのが、
フレームワーク、どれを使えばいいのかわからん... ということです。
rubyなら確実にruby on railsですし、reactならremixかnext.jsがよく使われます。
ただ、例えば上にあげたようなgoフレームワーク4つは、どれも人気であることが伺えます。今のところginが抜きん出てるようですが。

学び始めた初学者にとっては、まず「開発するならどれでやればいいんだよ!!」と言った壁にぶつかります。
そこで、各フレームワークを簡単に調べてみることにしました。それぞれのgithubに書いてある文言や公式ドキュメントから特徴を把握してみます。


gin

What is Gin?
Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API, but with performance up to 40 times faster than Martini. If you need smashing performance, get yourself some Gin.

訳: Gin は Golang で書かれた Web アプリケーションフレームワークです。martini に似たAPIを持ちながら、httprouter のおかげでそれより40倍以上も速いパフォーマンスがあります。良いパフォーマンスと生産性が必要であれば、Gin が好きになれるでしょう。
出典: https://gin-gonic.com/docs/

様々なミドルウェアが用意されており、直感的なルーティングが可能です。
標準のnet/httpとも互換性があり、軽量でかつ高速な処理を可能にします。

echo

High performance, extensible, minimalist Go web framework
出典 : https://echo.labstack.com/

以下にechoのFeaturesから抜粋します。

  • 最適化されたルーター
  • HTTP/2 サポート
  • スケーラブルなRestfulAPI
  • 組み込みミドルウェア

beego

Beego is used for rapid development of enterprise application in Go, including RESTful APIs, web apps and backend services.
It is inspired by Tornado, Sinatra and Flask. beego has some Go-specific features such as interfaces and struct embedding.
訳 : Beegoは、Goでエンタープライズアプリケーション(RESTful API、Webアプリ、バックエンドサービスなど)を迅速に開発するために使用されるフレームワークです。
出典 : https://github.com/beego/beego

主に、ルーティングやORM、ロガー等が提供されているようです。

https://qiita.com/scrpgil/items/551195b17b91d7652347

Fiber

Fiber is an Express inspired web framework built on top of Fasthttp, the fastest HTTP engine for Go. Designed to ease things up for fast development with zero memory allocation and performance in mind.

訳:Fiberは、Goの高速HTTPエンジンであるFasthttpの上に構築された、Expressに触発されたWebフレームワークです。高速な開発をサポートし、メモリアロケーションゼロと高いパフォーマンスを考慮して設計されています。
出典: https://github.com/gofiber/fiber

Fasthttpをラップして高速な処理を実現しているようです。


ruby on railsを今まで使ってきた身としては、フレームワークと言えばORM、サーバー、色々込み込みのものを想像してしまいますが、golangのwebアプリケーションフレームワークはどれも軽量(〜中規模)なものがほとんどであることが伺えます。

そして、ざっくりみただけですが、どのフレームワークも押しているのはwebサーバーであるという印象を受けました。
ルーティング処理、ミドルウェア、リクエスト処理.....など、どれも押しているポイントは似たようなものに感じます。(そんなに単純なものでないのは承知していますが🫡)

さらにはフレームワークという位置付けよりかはルーティングライブラリとして、

  • chi
  • gorilla/mux
  • julienschmidt/httprouter

などもあるようです。

こういった調べ物をしていると、標準のwebサーバー(net/http)の機能では不十分/さらなる高パフォーマンスが必要であり、それを解決するためにフレームワークやルーティングライブラリが開発されていったんだなということを感じます。

「標準パッケージは不十分なのかな...それならどれを選べばいいんだろう」と余計に迷いました

アップデートによるnet/httpの強化

標準パッケージでなかなか辛かったことの一つは、ルーティングだったようです。

「Go言語プログラミングエッセンス」(mattnさん著)にて、以下のような記述がありました。

net/httpは巷のWebアプリケーションフレームワークのように、GETやPOSTといったメソッドを制限したハンドラを登録したり、パラメータを受け取れたりするようなルーティング機能を持っていません。すべて自分で実装する必要があります。

https://gihyo.jp/book/2023/978-4-297-13419-8

また、「詳解Go言語Webアプリケーション開発」(清水 陽一郎さん著)には以下のような記述がありました。

問題になるのが標準パッケージのhttp.ServeMux型のルーティング設定の表現力の乏しさです。http.ServeMux型のルーティングの場合、次のようなルーティングの定義が乏しいです。

  • 「/users/10」のようなURLに含まれたパスパラメータの解釈
  • 「GET /users」「POST /users」といったHTTPメソッドの違いによるHTTPハンドラーの実装の切り替え

https://book.mynavi.jp/manatee/c-r/books/detail/id=131170

そこで、goのバージョンアップデートに伴う、net/httpパッケージの強化を知りました。
以下は、2024/02/07にリリースされた1.22のリリースノートです。
ルーティング周りが一層便利になったようです。

https://tip.golang.org/doc/go1.22#enhanced_routing_patterns

HTTP routing in the standard library is now more expressive. The patterns used by net/http.ServeMux have been enhanced to accept methods and wildcards.

訳 : 標準ライブラリの HTTP ルーティングの表現力が高まりました。net/http.ServeMuxで使用されるパターンがが拡張され、メソッドとワイルドカードを受け入れるようになりました。
出典:https://tip.golang.org/doc/go1.22#enhanced_routing_patterns

このアップデートにより、以下のようなことが可能になりました。

  • HTTPメソッドの指定
  • URLパスのワイルドカード指定
  • パスパラメータの取得

goの技術書は色々ほど読みましたが、この話は最近のものすぎて載っていませんでした。
上記3点については、2章の「実装&解説」にて簡単に紹介します。

net/httpという選択肢

できれば標準で完結させたい

1.22におけるhttp.ServeMuxの拡張により、ルーティングライブラリやフレームワークで補っていた標準パッケージであるnet/httpの痛い点が解決され、標準パッケージが提供する機能でも十分に開発できるのではないか?と考えます。

以下の記事では、このアップデートに言及しており、「標準だけでも十分」であることを匂わせます。

https://future-architect.github.io/articles/20240202a/

https://bmf-tech.com/posts/自作HTTPルーターから新しいServeMuxへ#14-ServeMuxのパフォーマンスについて

ginやecho、chiなど、多くのユーザーがいるようなフレームワーク・サードパーティライブラリが急にメンテ終了になることはそうそうないと思いますし、技術スタックに組み込むのは自然&効率的に開発するためには必要です。

しかし、最近gorilla/muxが一時アーカイブとなりメンテナンスが終了することがありました。
※その後、なんだかんだ(?)復帰したようです👇

https://gorilla.github.io/blog/2023-07-17-project-status-update/

復帰されたにせよ、一時は、「え、乗り換えたほうがいい?」と困惑した開発者も多くいたことでしょう。
githubのイシューに、そのようなやり取りがありました。

https://github.com/weaveworks/common/issues/272

こういうこともあり得る話なので、外部への依存は減らしたいです。開発効率&外部への依存というこのバランスは難しいものですが、net/httpでwebサーバーを十分に実装できるならそっちを選択したいものです。

パフォーマンスも良い

net/httpは、パフォーマンスも優れているようです。
先ほども載せさせていただいた記事では、http.ServeMuxchigorillaの3つでベンチマークをとって計測されていました。

https://future-architect.github.io/articles/20240202a/

パスパラメータが5〜10個と多くなるほど、少し劣るようですが、パスパラメータが存在しない&1個の場合は http.ServeMux > chi > gorillaとなるようです。
しかしながらそんなにパスパラメータが付加されているパスはそうないと思います。

このような点から、net/httpでwebサーバーを実装し、battery includedなgolangの強みを活かす開発に魅力を感じられます。

「じゃぁ、簡単にちょっと書いてみようかな」ということで2章ではコードを交えつつ、新機能を紹介します。

2. 実装&解説

構成

ハンドラー、ミドルウェア、マルチプレクサ(ルーティング)の大きく3つを実装していきます。
以下のコードでは、以下のような機能が実装されています。

ハンドラ

  • helloHandler
    • GETメソッド
    • /greet/hello/名前
    • 「Hello!,名前」という文字列が返る
  • byeHandler
    • GETメソッド
    • /greet/bye/名前
    • 「Bye!,名前」という文字列が返る
  • customGreetHandler
    • POSTメソッド
    • /greet/custom
    • json形式で{"greet" : 挨拶 , "name": 名前}を受け取り、「(挨拶)、(名前)!」の文字列が返る
  • fetchUser
    • 正確にはハンドラ関数(後述)
    • GETメソッド
    • /user/id
    • kakkkyと返されます。idに何を指定しても、一人しかいません(適当)。
  • addUser
    • 正確にはハンドラ関数(後述)
    • POSTメソッド
    • /user/id
    • json形式で{"id":id , "name" : 名前}を受け取り、user was added , id : id , name : 名前といったレスポンスが返ってきます。

ミドルウェア

  • loggingMiddleware
    • 受け付けた時刻、メソッド、パスをログに出力します
    • 例 : 2024/12/01 22:03:35 POST => /user/1

マルチプレクサ

マルチプレクサという言葉には、耳馴染みない方も多いのではないでしょうか。自身も、この言葉が出てきた時には全くわかりませんでした。
簡単に、ルーティングハンドラという理解でいいと思っています。
マルチプレクサは、登録された「リクエストURLパスーハンドラ」に従って、リクエストを振り分けます。以下はそれを表した図です。

今回は、親マルチプレクサの下に子マルチプレクサをぶら下げ、そこからハンドラに繋げる形にしています。
こうすることによって、可視性を上げるためにルーティングのグループ化を図っています。

追記:2024/12/06

上の件については、記事を書きましたので以下をご覧ください。

https://zenn.dev/yuta_kakiki/articles/47e1ff9a1784b3

マルチプレクサに関する話は、以下の記事をご覧ください。

https://zenn.dev/hsaki/books/golang-httpserver-internal/viewer/httpoverview

https://qiita.com/huji0327/items/c85affaf5b9dbf84c11e

実装

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
)

// ハンドラ
type helloHandler struct{}

func (hh *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	name := r.PathValue("name")
	fmt.Fprintf(w, "Hello!,%q\n", name)
}

// ハンドラ
type byeHandler struct{}

func (bh *byeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	name := r.PathValue("name")
	fmt.Fprintf(w, "Bye!,%q\n", name)
}

// ハンドラ
type customGreetHandler struct {
	Greet string `json:"greet"`
	Name  string `json:"name"`
}

func (cgh *customGreetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if err := json.NewDecoder(r.Body).Decode(&cgh); err != nil {
		fmt.Fprintf(w, "decode err : %v", err)
	}
	fmt.Fprintf(w, "%v!,%q\n", cgh.Greet, cgh.Name)
}

// ハンドラ関数
func fetchUser(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "kakkky")
}

// ハンドラ関数
func addUser(w http.ResponseWriter, r *http.Request) {
	type userData struct {
		ID   int    `json:"id"`
		Name string `json:"name"`
	}
	var user userData
	if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
		fmt.Fprintf(w, "decode err : %v", err)
	}
	fmt.Fprintf(w, "user was added , id : %d , name : %q", user.ID, user.Name)
}

// ロガーミドルウェア
func loggingMiddleware(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		h.ServeHTTP(w, r)
		method := r.Method
		path := r.URL.Path
		log.Printf("%v => %v", method, path)
	})
}

// ルーティングを初期化する
func initRoutes() *http.ServeMux {
	// 親マルチプレクサを用意
	mux := http.NewServeMux()
	{
		// 子マルチプレクサ
		greetMux := http.NewServeMux()
		greetMux.Handle("GET /greet/hello/{name...}", loggingMiddleware(&helloHandler{}))
		greetMux.Handle("GET /greet/bye/{name...}", loggingMiddleware(&byeHandler{}))
		greetMux.Handle("POST /greet/custom", loggingMiddleware(&customGreetHandler{}))
		// 子マルチプレクサを親マルチプレクサに登録
		mux.Handle("/greet/", greetMux)
	}

	{
		// 子マルチプレクサ
		userMux := http.NewServeMux()
		userMux.Handle("GET /user/{id}", loggingMiddleware(http.HandlerFunc(fetchUser)))
		userMux.Handle("POST /user/{id}", loggingMiddleware(http.HandlerFunc(addUser)))
		// 子マルチプレクサを親マルチプレクサに登録
		mux.Handle("/user/", userMux)
	}

	return mux
}

func main() {
	// ルーティングが登録されたマルチプレクサ
	mux := initRoutes()

	// サーバーを起動
	http.ListenAndServe(":8080", mux)
}

解説

いくつかのハンドラー、ミドルウェア、マルチプレクサの順にコードを抜粋しつつ解説していきます。

ハンドラ

helloHandlerについて

type helloHandler struct{}

func (hh *helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // パスパラメータを取得
	name := r.PathValue("name")
	fmt.Fprintf(w, "Hello!,%q\n", name)
}

net/httpパッケージでは、ハンドラーがhttp.Handlerインターフェースを満たす必要があります。

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

helloHandler構造体は、ServeHTTPメソッドを持っているので、http.Handlerを満たしているといえます。この型を満たすことで、マルチプレクサにハンドラを登録することができるのです。

name := r.PathValue("name")の部分では、パスパラメータを取得しています。
これも1.22から登場したメソッドです。

func (r *Request) PathValue(name string) string

マルチプレクサの節でも触れますが、/greet/hello/{name...}としていた時にパラメータnameの部分を取得することができます。

customHandlerについて

このハンドラは、POSTメソッドとして登録しています。

// ハンドラ
type customGreetHandler struct {
	Greet string `json:"greet"`
	Name  string `json:"name"`
}

func (cgh *customGreetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // jsonから構造体にデコード
	if err := json.NewDecoder(r.Body).Decode(&cgh); err != nil {
		fmt.Fprintf(w, "decode err : %v", err)
	}
	fmt.Fprintf(w, "%v!,%q\n", cgh.Greet, cgh.Name)
}

POSTメソッドなので、json形式で送られてきたパラメータを取得するため、encoding/jsonパッケージを使用し、デコードして構造体にマッピングしています。

addUserについて

少し触れましたが、addUserはPOSTメソッドとして登録されたハンドラ関数です。

func addUser(w http.ResponseWriter, r *http.Request) {
	type userData struct {
		ID   int    `json:"id"`
		Name string `json:"name"`
	}
	var user userData
    //jsonを構造体にデコード
	if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
		fmt.Fprintf(w, "decode err : %v", err)
	}
	fmt.Fprintf(w, "user was added , id : %d , name : %q", user.ID, user.Name)
}

先ほどは、マルチプレクサにハンドラを登録するためにはhttp.Handler型を満たす必要があることを述べました。
しかし、この関数はその型を満たせていません。

それでは、どのようにして登録できているのでしょうか??

マルチプレクサでこのハンドラ関数を登録しているコードは以下です。

userMux.Handle("POST /user/{id}", loggingMiddleware(http.HandlerFunc(addUser)))

先のハンドラ(helloHandlerなど)と異なるのは、以下の部分です。

http.HandlerFunc(addUser))

実は、これによって、addUser関数がhttp.Handlerを満たすようになっています。
http.HandlerFuncとは、以下のような関数型です。

// 関数型
type HandlerFunc func(ResponseWriter, *Request)

// この関数型は、ServeHTTPメソッドを持つ
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

先のコードでは、func(ResponseWriter, *Request)のシグネチャをとる関数をhttp.HandlerFuncでキャスト(型変換)していました。そして、それによって、ServeHTTPメソッドを持つようになり、http.Handlerを満たすことを実現しています。
自身が実装したfunc(ResponseWriter, *Request)の処理は、ServeHTTPメソッドにラップされたような形となっており、サーバーからきちんと実行されるようになっています[1]

ハンドラとハンドラ関数の使い分け(私見)

ハンドラとして持たせたい処理を用意するには、次の2パターンがあります。
(1). 構造体にServeHTTPメソッドを用意してhttp.Handler型を満たす
(2). func(ResponseWriter, *Request)のシグネチャをとるように実装しておき、http.HandlerFunc関数型でキャストする形でhttp.Handler型を満たす

(1)がhelloHandlerにあたり、(2)がaddUserにあたります。今回は、どちらのパターンも載せておこうという意図で無理やり両方の方法を使って実装し分けています。

結論、使い分けるポイントは

  • 構造体を使った状態管理や複雑なロジックを実装したいなら、(1)の方法
  • シンプルな処理なら(2)の方法
    と考えています。

(1)の方法ならばクリーンアーキテクチャなどに則り、責務わけされた別のモジュールをハンドラにDIさせ、ハンドラ内で使用することができます。
簡単な例を載せておきます。

// ハンドラ
type PostCartHandler struct {
	addCartUsecase cartUsecase.AddCartUsecase
}
// ユースケースモジュールをDI
func NewHandler(addCartUsecase cartUsecase.AddCartUsecase) *PostCartHandle {
	return &PostCartHandle{
		addCartUsecase: addCartUsecase,
	}
}

func(pch *PostCartHandler)ServeHTTP(w http.ResponseWriter, r *http.Request){
 //処理
}

これを踏まえて、、、addUser関数はハンドラ関数として実装しましたが、自分が実装するなら、以下のようにハンドラとして実装します。IDやNameといったuserオブジェクトの状態を持ち、処理を行うものと判断できるからです。

// ユーザーオブジェクト
type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}
// ハンドラ
type addUserHandler struct {
	user *User
}

func (auh *addUserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if err := json.NewDecoder(r.Body).Decode(auh.user); err != nil {
		fmt.Fprintf(w, "decode err : %v", err)
	}
	fmt.Fprintf(w, "user was added , id : %d , name : %q", auh.user.ID, auh.user.Name)
}
}

あくまで、私見ですが、自身がモヤモヤし続けていた2つの方法の使い分けを考えたらこうかなといったことを書きました。

(余談)よく見るhttp.HandleFunc関数とは

色々とハンドラの実装例を見ていると、http.HandleFunc関数を使用している例が散見されます。

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

この関数は、デフォルトマルチプレクサをマルチプレクサとして使用する際に用いられる関数です。
デフォルトマルチプレクサは、http.ListenAndServe(addr,handler)の第二引数にnilが入る時に内部で使用されるマルチプレクサのことです。マルチプレクサを今回のように作成しなくても用意してくれているのですね。
http.HandleFunc関数内部では、以下のhandleFuncメソッドが呼び出されています。

// HandleFunc関数の内部で呼び出しているhandleFuncメソッド
func (mux *serveMux121) handleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	if handler == nil {
		panic("http: nil handler")
	}
    // HandlerFunc関数型でキャストしている
	mux.handle(pattern, HandlerFunc(handler))
}

結局は同じように、mux.handle(pattern, HandlerFunc(handler))というふうに、func(ResponseWriter, *Request)のシグネチャをとるハンドラ関数をhttp.HandlerFunc関数型でキャストして、http.Handlerを満たすようにしてから、マルチプレクサにハンドラとして登録しているのがわかります。

以下に、http.HandleFunc関数で実装した時のコードを載せておきます。

コードを見る👀
package main

import (
	"fmt"
	"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "Hello, World!")
}

func main() {
	// ルートハンドラを登録
	http.HandleFunc("/", helloHandler)

	// サーバーを開始 (DefaultServeMuxを使用)
	http.ListenAndServe(":8080", nil)
}

ミドルウェア

ミドルウェアは、一般に以下のシグネチャをとることが多いようです。

func(h http.Handler)http.Handler

このようなシグネチャをとることで、ミドルウェア実装を入れ子にでき、複数のミドルウェアを適用させることが可能です。

func loggingMiddleware(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 引数にとったハンドラを実行
		h.ServeHTTP(w, r)
		method := r.Method
		path := r.URL.Path
        // ハンドラ実行の後にログを出力する
		log.Printf("%v => %v", method, path)
	})
}

ハンドラの実装については以下の記事が詳しいです。

https://tutuz-tech.hatenablog.com/entry/2020/03/23/220326

https://zenn.dev/gege/articles/0c5a5f755820fe

マルチプレクサ

構成図

func initRoutes() *http.ServeMux {
	// 親マルチプレクサを用意
	mux := http.NewServeMux()
	{
		// 子マルチプレクサ
		greetMux := http.NewServeMux()
		greetMux.Handle("GET /greet/hello/{name...}", loggingMiddleware(&helloHandler{}))
		greetMux.Handle("GET /greet/bye/{name...}", loggingMiddleware(&byeHandler{}))
		greetMux.Handle("POST /greet/custom", loggingMiddleware(&customGreetHandler{}))
		// 子マルチプレクサを親マルチプレクサに登録
		mux.Handle("/greet/", greetMux)
	}

	{
		// 子マルチプレクサ
		userMux := http.NewServeMux()
		userMux.Handle("GET /user/{id}", loggingMiddleware(http.HandlerFunc(fetchUser)))
		userMux.Handle("POST /user/{id}", loggingMiddleware(http.HandlerFunc(addUser)))
		// 子マルチプレクサを親マルチプレクサに登録
		mux.Handle("/user/", userMux)
	}

	return mux
}

グループ化して「リクエストURL-ハンドラ」の組み合わせを登録してある子マルチプレクサを、親元のマルチプレクサに登録しています。
{}は単なるブロックスコープで、わかりやすくグループ化するためです。
*http.ServeMux.Handleメソッドは、リクエストURLとhttp.Handlerをマルチプレクサに登録しいます。

そして注目すべきは、HTTPメソッドによる制御パスのワイルドカードです。

greetMux.Handle("GET /greet/bye/{name...}", loggingMiddleware(&byeHandler{}))

Handleメソッドの第一引数に、次のようなパターンを取ります。

[METHOD ][HOST]/[PATH]

そして、パスの中に/hoge/{param}とした場合に、パスのparam部分はワイルドカードとして働きます。しかし、このリクエストパスが登録されているときは/hoge/といったふうにparam部分を省略すると、マッチせずにエラーとなります。
/hoge/のように{param}部分がない場合のリクエストパスも同様にマッチさせたい場合は、{param...}とします。この書き方は末尾のみ有効です(...によってパスの残り全体にマッチします)。

注意点として、パターンの優先順位があります。
複数のパターンがマッチする場合、以下のルールで優先順位が決まります。

  • 具体性の高いパターンが優先される:
    • /images/thumbnails/ は /images/ よりも優先される。
  • メソッドやホストの指定:
    • ホスト指定があるパターンは、ないものよりも優先される。

3. 感想

今回は、net/httpでwebサーバーを実装するというテーマで記事を書いてみました。
誤りがあったらどうぞご指摘ください。

net/httpパッケージは、goにおけるwebサーバーを構成する基本的なパッケージです。
他のフレームワークの多くも、これをラップしています。

特にフレームワークにこだわりなし&標準で十分そうなら、標準パッケージに対する理解を深めることで、他のフレームワークへのキャッチアップをしやすくなると思います。

便利になったとはいえ、外部ライブラリやフレームワークのように組み込みのミドルウェアが用意されているわけではなく、実装が大変という部分も捨てきれません。(golangは軽量でシンプルであり、組み込みでミドルウェアまで用意してしまうと、その設計思想が崩れる気がするので、今後のアップデートでもそこは期待できないんじゃないのかなぁと思ったりします)

次、個人開発でwebアプリケーションを開発する時のwebサーバーには、標準のnet/httpを使おうかなと思います。

以下に、net/httpに関する理解が深められる記事を載せておきます。
非常に参考にさせていただきました。

https://zenn.dev/hsaki/books/golang-httpserver-internal

https://future-architect.github.io/articles/20210714a/

脚注
  1. http.HandlerがもつServeHTTPメソッドはいつ実行されるの?という話ですが、これはサーバーを起動しているhttp.ListenAndServe関数の内部を辿っていけば見つかります。詳しくは、右のZennBooksをご覧ください。 https://zenn.dev/hsaki/books/golang-httpserver-internal ↩︎

Discussion