📣

Echoで始める憧れのOSS活動への第0.1歩

2022/09/19に公開

はじめに

3年目のバックエンドエンジニアです
OSS活動についていつかはやってみたいなと思っていたものの、
なかなかハードルが高そうで躊躇していました
今回ソフトウェアデザイン 2022年9月号を読んで大変刺激を受けたので、
小さい一歩から始めてみようと思い実践してみました

この記事でやること

  • ソフトウェアデザイン 2022年9月号OSSソースコードリーディングのススメの内容の実践
    • 適宜開発を進めながら、都度必要な箇所のソースコードを読んで見る
  • 使用言語はGo(Echo)

動作環境

  • macOS: BigSur(11.6.1)
  • Go: 1.18.4 darwin/amd64
  • Echo: 4.9.0
  • VSCode: 1.71.2

OSSを読む目的を決める

まずOSSを読む目的を明確にします
自分の場合、

  • Go言語及びフレームワークの理解を深めたい
  • 将来的にOSS活動を通じて海外就職してみたい
    という2点が目的になります

読みたいOSSのコードを決める

次に読みたいOSSのコードを決めます
ソースコードを選ぶ基準として、下記のような項目が挙げられていました

  • 使い慣れているもの
  • 仕様を把握しているもの(例えばHTTPの仕様など)
  • なるべく小さくてシンプルなもの
  • よく使われているプログラミング言語で書かれているもの

自分の場合、

  • Go言語で簡単なアプリケーションを作成したことがある
  • HTTPの仕様についてある程度は理解している
  • Webフレームワークを使用した経験がある
    といった理由から、
    GoのWebフレームワークであるEchoを選択してみました

まずはドキュメントを読む

開発をはじめるにあたり、まずはドキュメントを読んでみます
Echoの公式ドキュメントはこちらになります
https://echo.labstack.com/

クイックスタート

まずは下記のコマンドを実行し、Echoをインストールしていきます

$ mkdir myapp && cd myapp
$ go mod init myapp
$ go get github.com/labstack/echo/v4

インストールが完了したら、mainとなるファイルを作成してきます

touch server.go

作成したファイルに、下記を追記します

server.go
package main

import (
	"net/http"
	
	"github.com/labstack/echo/v4"
)

func main() {
	e := echo.New()
	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})
	e.Logger.Fatal(e.Start(":1323"))
}

Echoを起動し、動作を確認してみます

go run server.go
// 実行結果

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.9.0
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:1323

ブラウザを起動し、 http://localhost:1323/ にアクセスしてみます

Hello, World!が表示されました
シンプルな記載でWebサーバを立ち上げることができました

ソースコードを読んで見る

ここからが本題になります
まず、main関数の内容について確認してきます

server.go
func main() {
	e := echo.New()
	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})
	e.Logger.Fatal(e.Start(":1323"))
}

内容としては、下記の3つになります

  1. echo.New() でEchoのインスタンス作成
  2. e.GET(パス, ハンドラ) でルーティング
  3. e.Logger.Fatal(e.Start(":1323")) でログ出力

今までの自分の場合、
「Echoシンプルだし便利だ!別のハンドラーを追加してみよう!」
と開発を進めてしまっていましたが、一旦立ち止まってソースコードを読んでみたいと思います
※思ったより記事が長くなってしまったので、上記1.の部分のみ記事にします

Echoインスタンスの作成

まずはEchoのインスタンスを作成している部分のソースを見てみたいと思います
echo.New()を選択した状態でF12、または ⌘ + クリックしてみます
echo.New()の中身は下記のようなコードになっていました

echo.go
// New creates an instance of Echo.
func New() (e *Echo) {
	e = &Echo{
		filesystem: createFilesystem(),
		Server:     new(http.Server),
		TLSServer:  new(http.Server),
		AutoTLSManager: autocert.Manager{
			Prompt: autocert.AcceptTOS,
		},
		Logger:          log.New("echo"),
		colorer:         color.New(),
		maxParam:        new(int),
		ListenerNetwork: "tcp",
	}
	e.Server.Handler = e
	e.TLSServer.Handler = e
	e.HTTPErrorHandler = e.DefaultHTTPErrorHandler
	e.Binder = &DefaultBinder{}
	e.JSONSerializer = &DefaultJSONSerializer{}
	e.Logger.SetLevel(log.ERROR)
	e.StdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0)
	e.pool.New = func() interface{} {
		return e.NewContext(nil, nil)
	}
	e.router = NewRouter(e)
	e.routers = map[string]*Router{}
	return
}

echo.New()の中身は、ざっくりと下記の内容が行われてました

  1. ポイント型の Echo の構造体を初期化
  2. Echoの構造体の項目に値を設定
  3. ポイント型のEchoを戻り値として返却

上記の内、1と3はなにが行われているか理解できましたが、
2の部分がいまいちイメージできていません
ということで、
続いてEchoの構造体をソースコードを確認していきます
すべてのソースコードを読むとかなり時間がかかってしまうため、
今回は

e.Server.Handler = e

の部分のみに着目して確認していきます

Echo構造体の中身の確認

Echoの構造体の中身は下記の通りです

echo.go
type (
	// Echo is the top-level framework instance.
	Echo struct {
		filesystem
		common
		// startupMutex is mutex to lock Echo instance access during server configuration and startup. Useful for to get
		// listener address info (on which interface/port was listener binded) without having data races.
		startupMutex     sync.RWMutex
		StdLogger        *stdLog.Logger
		colorer          *color.Color
		premiddleware    []MiddlewareFunc
		middleware       []MiddlewareFunc
		maxParam         *int
		router           *Router
		routers          map[string]*Router
		pool             sync.Pool
		Server           *http.Server
		TLSServer        *http.Server
		Listener         net.Listener
		TLSListener      net.Listener
		AutoTLSManager   autocert.Manager
		DisableHTTP2     bool
		Debug            bool
		HideBanner       bool
		HidePort         bool
		HTTPErrorHandler HTTPErrorHandler
		Binder           Binder
		JSONSerializer   JSONSerializer
		Validator        Validator
		Renderer         Renderer
		Logger           Logger
		IPExtractor      IPExtractor
		ListenerNetwork  string
	}

	// 以下省略
)

上記より

e.Server.Handler = e

はGoの標準パッケージである net/http のServerのHandlerということがわかりました

net/httpのServer構造体の確認

Server.Handlerの内容は下記のとおりです

net/http/server.go
// A Server defines parameters for running an HTTP server.
// The zero value for Server is a valid configuration.
type Server struct {
	// 省略
	Handler Handler // handler to invoke, http.DefaultServeMux if nil
  // 省略
}

さらに、Handlerは

net/http/server.go
// A Handler responds to an HTTP request.
//
// ServeHTTP should write reply headers and data to the ResponseWriter
// and then return. Returning signals that the request is finished; it
// is not valid to use the ResponseWriter or read from the
// Request.Body after or concurrently with the completion of the
// ServeHTTP call.
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

ServeHTTP()をメソッドに持つインターフェースであることがわかりました

Handlerインターフェースを満たす実装部分の確認

echo.goに戻り、ソースコードを確認すると下記の通りServeHTTP()を実装しているため、
Handlerインターフェースを満たしています

echo.go
// ServeHTTP implements `http.Handler` interface, which serves HTTP requests.
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Acquire context
	c := e.pool.Get().(*context)
	c.Reset(r, w)
	var h func(Context) error

	if e.premiddleware == nil {
		e.findRouter(r.Host).Find(r.Method, GetPath(r), c)
		h = c.Handler()
		h = applyMiddleware(h, e.middleware...)
	} else {
		h = func(c Context) error {
			e.findRouter(r.Host).Find(r.Method, GetPath(r), c)
			h := c.Handler()
			h = applyMiddleware(h, e.middleware...)
			return h(c)
		}
		h = applyMiddleware(h, e.premiddleware...)
	}

	// Execute chain
	if err := h(c); err != nil {
		e.HTTPErrorHandler(err, c)
	}

	// Release context
	e.pool.Put(c)
}

まとめ

少し長くなってしまいましたが、

e.Server.Handler = e

の部分でなにをしているのか、改めて整理してみたいと思います

  • 左辺の e.Server.Handler はServeHTTP()をメソッドに持つインターフェース(を宣言した変数)
  • 右辺の e は ポインタ型のEchoであり、ServeHTTP()を実装している
    → ポインタ型のEchoはHandlerインターフェースに含まれる(満たしている)

つまり、e.Server.Handler = e とすることで
Handlerインターフェースを満たすことになり、
Handlerインターフェースを引数に持っている関数も実行することができるようになります
その結果、Echo経由でGoのnet/httpなどの機能が使えるようになると理解しました

おわりに

憧れのOSS活動への第0.1歩として、GoのフレームワークであるEchoのソースコードを読んでみました
ソースコードを読む中で、Goのインターフェース周りの理解が浅いことに気がつくことができました
また、時間はかかったものの、
単に「こう書けば動く」という認識だったものを
「なぜそう書くことで動くのか?」という納得感につなげることができました
OSS 活動への0.2歩、0.3歩へとつなげていけるよう、
理解が浅い部分について復習していきたいと思います

参考

参考文献

武内 覚(2022) コードリーディングの始め方 Software Design編集部 『ソフトウェアデザイン 2022年9月号』 技術評論社 pp.176-184
早川 大貴(2022) コードリーディングの具体的手法 Software Design編集部 『ソフトウェアデザイン 2022年9月号』 技術評論社 pp.186-199

参考サイト

Echo公式ドキュメント
net/http/server.go
Interfaces in GoLang
Goのinterfaceがわからない人へ

Discussion