Web開発
Ginの構築:EasyよりもSimpleを優先する
Building Gin: Simple over Easy (manualmeida.dev)
要約
この記事は、Go言語のWebフレームワークであるGinの設計哲学「EasyよりもSimple」について解説しています。作成者は、多くのマジックに頼るMartiniのようなフレームワークとは異なり、開発者が明確な制御フローを持ち、ボイラープレートコードを減らすためのシンプルなツールを提供することを目指しました。特に、高速なルーティングとゼロの破壊的変更に焦点を当てた設計原則が強調されています。
全文翻訳
2014年、私はサンフランシスコから何の計画もなく、一つの有用な傷跡を抱えて帰国しました。小さなソフトウェアチームがツールに何を必要としているかを見てきたのです。初めてのゲームを出荷した後、JoypadとTinySparkでSDKを構築するのに1年を費やしました。その後スペインに戻り、電気通信工学を始める寸前で、次に何を構築するかを決めていました。その答えは、人々の興味を中心に構築されたソーシャルネットワークであるFyveでした。バックエンドにはGoを選びました。この言語は適切な意味でプレーンだと感じたからです。Ginはその製品のWebフレームワークとして始まり、Goを学び、どのような種類のエンジニアになりたいかをまだ決めている最中に書かれました。コードはgin-gonic/ginにあります。Fyveは廃れましたが、Ginは生き残りました。
EasyよりもSimple
当時、GoのWebフレームワークとして人々が私に勧めたのはMartiniでした。その理由をすぐに理解しました。READMEは小さく、ミドルウェアモデルはエレガントで、数分でルートを応答させることができました。Martiniはリフレクションベースの依存性注入を使ってハンドラを結合していました。そのため、最初のデモはスムーズに感じられました。しかし、重要な動作を見えないところに移動させました。サービスは魔法のようにハンドラに現れ、制御フローは追跡が難しくなり、リフレクションはリクエストパス上で実行されました。
その頃、私はRob Pikeの「Simplicity is Complicated」という視点を通してGoコミュニティを読んでいました。私に響いたのは、ミニマリズムのスローガンではありませんでした。それはコストモデルでした。シンプルなソフトウェアは、それを使う人にとっての作業を減らすために、それを作る人にとってより多くの作業を必要とすることが多い、というものです。これがGinの設計指針となりました。Easyとは、最初の例でよく見えるものです。行数が少なく、構文がよりきれいで、ページ上の定型句が少ない。Simpleとは、カウントが低いことです。可動部品が少なく、学習する概念が少なく、覚える例外が少ない。Martiniは最初のバージョンをEasyにしました。私は、コードベースが古くなって私を驚かせた後も、GinがSimpleであり続けることを望みました。
中間点を見つける
アリストテレスの美徳のバージョンは中間点でした。多すぎず、少なすぎない。それはフレームワークの問題の形でもありました。Martiniはあまりにも多くの魔法を与えました。Goの標準net/httpは、Webプログラミングの正直なバージョンを与えます。魔法なし、明示的な制御フロー、そして頭の中で保持できるハンドラの形。しかし、それはあまりにも少ない助けしか与えません。ルートパラメーター、リクエスト解析、検証、レスポンスのための同じ配管を、ハンドラごとに繰り返し書きます。どれも難しいことではありません。十分な量がノイズになります。
Ginは、それらの中間点を見つける私の試みでした。リクエストパスを明示的に保ち、そこでリフレクションを実行せず、退屈な作業を検査できる一つのオブジェクト、Contextの背後に置くことです。
```go
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id") // path params, no reflection
c.JSON(200, gin.H{"id": id}) // response rendering, one call
})
r.Run(":8080")
```
この*gin.Contextは、リクエスト、レスポンスライター、パスパラメーター、検証ヘルパー、レンダリングを伝達します。あなたは一つのものを渡します。明白な操作は一つのメソッド呼び出しのままであり、機械はプロダクションがおかしくなったときにデバッグできるように十分に見えるままです。面白いことに、gin.Contextは2014年に出荷されました。Goの標準ライブラリにcontext.Contextが登場する2年前です。それが登場したとき、私たちは自分たちの名前を変更しませんでした。gin.Contextが標準インターフェースを満たすようにし、一つの破壊的変更もなく完全な互換性を追加しました。これは、SDKの仕事から得た製品感覚がWebフレームワークに現れたものです。開発者体験は、高速パスを自然に感じさせる機能です。
ラディックスツリーを中心としたルーター
ルーターは、シンプルさの境界線が具体的になった場所です。Martiniは正規表現のリストを巡回し、それぞれにマッチするかどうかを尋ねることができました。これは柔軟です。数字のみにマッチするルートを作成したり、パターン内に追加のルールを隠したりできます。それはまた、フレームワーク内の別の言語でもあります。Ginはより小さなルート言語を選択しました。静的セグメント、名前付きパラメーター、キャッチオールです。この選択により、ルーターはラディックスツリーを使用できるため、より高速になりました。これはhttprouterがGoで人気にしたアプローチと同じです。さらに重要なことに、ルートの推論が容易になりました。フレームワークは、巧妙なマッチャーではなく、通常のパスに向かうように促しました。
ルートルックアップ、ルートのスキャンなし
```
routes
0 nodes
1 matched
```
図:ライブで構築され、マッチングされる圧縮ラディックスツリー。共有プレフィックスは一つのパスに集約され、/blog/42/commentsはツリーを辿り、42を:slugとしてバインドします。
通常のルートから始める
ルーターは、/search、/support、/blog/:slug/commentsのようなルート文字列を認識します。素朴なルーターはそれらを一つずつテストできます。ラディックスツリーは共有テキストを構造に変換します。
共有プレフィックスは集約されます
/searchと/supportは/sを共有します。2つのブログルートは/blog/を共有し、次に:slugを共有します。各共有プレフィックスは一度だけ保存されます。
マッチングは一つのパスを辿ります
/blog/42/commentsの場合、ルックアップは/blog/を辿り、:slugで42をバインドし、次に/commentsに進みます。すべてのルートをスキャンすることはありません。
ホットパスは小さいままです
作業はURLの長さに従います。パラメーターは小さなスライスに収まり、マッチングされたハンドラはすでに最終ノードにあり、リクエストパスはリフレクションを回避します。/blog/42/commentsのマッチングは、/blog/を:slugノードまで辿り、42をバインドし、/commentsに進みます。作業はURLの長さに従い、アプリに登録されているルートの数には依存しません。ファイル内で別々に見えるルートは、メモリ内のコンパクトな一つのパスに集約されます。
nn個のルートとkkkの長さのリクエストパスを持つルーターの場合、ラディックスツリーのルックアップはTmatch(k)=O(k)を実行します。これはnに依存せず、mmm長のパターンに対して線形正規表現マッチングはO(n⋅m)のコストがかかります。ツリーは、このリクエストごとのスキャンを、共有プレフィックスを下る一度の走査と交換します。
同じアイデアは、小さな割り当ての選択にも現れました。パラメーターは事前割り当てされたスライスに格納されます。Contextオブジェクトはsync.Poolから取得され、リクエスト間でリセットされます。ガベージコレクターがクリーンアップするゴミが少なくなるため、レイテンシーが不安定になる理由が少なくなります。これが私が信頼する種類のパフォーマンス作業です。ホットパスでの操作が少なく、プログラマーの頭の中にある概念が少ないのです。
ゼロ破壊的変更のための設計
静かな目標は、ゼロの破壊的変更でした。GinがGoのエコシステムに属していると感じてほしかったので、Go自身の互換性約束から学びました。この制約は設計方法を変えます。メソッドを削除する前にメソッドを追加します。5文字節約できる賢い名前変更を拒否します。すべての公開関数を、見知らぬ人が会社を築くかもしれないものとして扱います。
それは守られました。Ginに対して書かれた最初のプログラムのいくつかは、10年以上経った今でもコンパイルされ、実行されます。ベンチマークは重要です。その互換性はもっと重要です。
Hacker News、そして成長
私は適切なタイミングでHacker NewsでGinをリリースしました。Goはそこで注目を集めており、一つのREADMEに収まり、ホットパスでリフレクションを避け、ベンチマークで好成績を収めたフレームワークは試しやすいものでした。その後の成長は劇的というよりも着実なものでした。人々はそれを使用し、課題を提出し、パッチを送信し、実際のサービスに組み込みました。今日、Ginは約88,000のスターを獲得し、290,000以上のプロジェクトがそれに依存しています。オープンソースの数字は虚栄心の指標になることもありますが、この場合、依存関係の数はスターよりも私にとって重要です。それは、元のスタートアップが消滅した後もAPIの決定がずっと複合的に影響し続けていることを意味します。
卒業させる
数年後、私はGinから一歩退き、私なしで改善を続けたメンテナーに引き継ぎました。私はそれをプロジェクトの卒業と考えています。特に、Bo-Yi WuとJavier Provechoに感謝します。彼らはプロジェクトを引き継ぎ、高い水準を維持しました。オープンソースで私が尊敬するのは、そのものが作者を必要としなくなることです。それは、他者のユースケース、他者の優先順位、他者の趣味との接触に耐え、生き残るのです。ライブラリを構築しているのであれば、それが目指すべき目標だと思います。10年間維持できると想像できるAPIを設計してください。たとえそれがEasyにするよりもコストがかかっても、内部はSimpleにしてください。あなたを超えるように構築してください。数年後、私はコンパイラでも同じことを試みました。