メディアドゥでは、エンジニア有志によって執筆された【Tech Do Book】という合同誌を発行しています。
本日はその中から、Tech Do Book vol.1 【4章 Go + Echoでウェブページをフェッチするツールを作る】を紹介します。
はじめに
GoのEchoというフレームワークを使うと、いともたやすくウェブアプリケーションを構築できます。本章では、Echoの魅力をお伝えするために、ブラウザ上で指定したウェブページをフェッチするツールを作ってみます。
Echoとは
Echoは、Go製のウェブアプリケーションフレームワークです。*1 公式サイトで「High performance, extensible, minimalist Go web framework」とうたっているように、拡張性が高くミニマルな設計になっています。他にも、次のような特徴があります。*2
- 最適化されたHTTPルーター(★)
- 堅牢でスケーラブルなRESTful APIの構築
- APIのグルーピング(★)
- 拡張可能なミドルウェアフレームワーク
- アプリケーション、グループ、ルートの各レベルでミドルウェアを定義
- JSON、XML、フォームのためのデータバインディング(★)
- さまざまなHTTPレスポンスを送信するための便利な関数(★)
- 一元的なエラーハンドリング
- 任意のテンプレートエンジンによるレンダリング
- ロガーのフォーマットを定義
- 高度なカスタマイズ性
- Let’s Encryptによる自動TLS
- HTTP/2のサポート
今回は、★印のものを活かしたアプリケーションを作っていきます。
Hello Worldプログラムを作る
Echoがどのようなものなのかを理解するために、まずは定番のHello Worldプログラムを作ってみましょう。*3
Goをインストール
Goをインストールしていない場合は、インストールしてください。*4インストールされているGoのバージョンは、次のコマンドで確認できます。
$ go version
この記事で使用しているGoのバージョンは、1.11.5
(執筆した2019年2月13日時点の最新版)です。
Echoをインストール
次のコマンドで、Echoとその依存パッケージをインストールします。
$ go get -u github.com/labstack/echo/...
この記事で使用しているEchoのバージョンは、4.0.0
(執筆した2019年2月13日時点の最新版)です。
プログラムを作成
server.go
ファイルを作成します。
package main import ( "net/http" "github.com/labstack/echo" ) 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")) }
- 9〜15行
main
関数を定義しています。プログラムの実行時には、この関数が呼び出されます。
func main() {
(省略)
}
- 10行
Echoのインスタンスを作成しています。
e := echo.New()
- 11〜13行
/
というパスに対してGETリクエストが送信された場合の処理を定義しています。ここでは、ステータスコードは200
(http.StatusOK
)で、Hello World
という文字列を返すようにしています。
e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello World") })
- 14行
1323
ポートでサーバーを起動しています。
e.Logger.Fatal(e.Start(":1323"))
プログラムを実行
それでは、作成したプログラムを実行してみましょう。次のコマンドで、server.go
ファイルを実行します。
$ go run server.go
すると、ターミナル上に次のようなメッセージが表示されます。
____ __ / __/___/ / ___ / _// __/ _ \/ _ \ /___/\__/_//_/\___/ v4.0.0 High performance, minimalist Go web framework https://echo.labstack.com ____________________________________O/_______ O\ ⇨ http server started on [::]:1323
ブラウザでhttp://localhost:1323/にアクセスすると、Hello World
という文字列が表示されるはずです。
このように、Echoを使うといともたやすくウェブアプリケーションを構築することができるのです。
ウェブページをフェッチするツールを作る
さて、ここからが本題です。より実践的な例として、ブラウザ上で指定したウェブページをフェッチするツールを作ってみましょう。必要なものは、次の2つです。
- ウェブページをフェッチして返すAPI(Go)
- 入力フォームを持つウェブページ(HTML)
Hello Worldプログラムでは1つのファイルにすべての処理を記述していましたが、今回は複数のファイルに分けて記述します。最終的なディレクトリ構造は、次のようになります。
. ├── handler │ └── api │ └── fetch.go ├── logic │ └── fetch.go ├── public │ └── index.html ├── router │ └── router.go └── server.go
APIを作成
GoとEchoで、ウェブページをフェッチして返すAPIを作ります。
logic/fetch.go
logic
ディレクトリに、fetch.go
ファイルを作成します。このファイルでは、ウェブページをフェッチする処理を定義します。
package logic import ( "io/ioutil" "net/http" ) type FetchResponse struct { Body string } func Fetch(url string) (*FetchResponse, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } client := new(http.Client) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } return &FetchResponse{ Body: string(body), }, nil }
- 8〜10行
FetchResponse
という型を定義しています。FetchResponse
はレスポンス情報を保持するための構造体で、string
型のBody
をメンバーに持ちます。
type FetchResponse struct { Body string }
- 12〜34行
Fetch
関数を定義しています。この関数では、引数で受け取ったURLを元にウェブページをフェッチします。
func Fetch(url string) (*FetchResponse, error) { (省略) }
- 13〜16行
リクエスト情報を持つ変数req
を作成しています。エラーが発生した場合は、そのエラーをそのまま返しています。
req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err }
- 18行
HTTPクライアントを作成しています。
client := new(http.Client)
- 19〜22行
HTTPクライアントでリクエストを送信しています。エラーが発生した場合は、そのエラーをそのまま返しています。
resp, err := client.Do(req) if err != nil { return nil, err }
- 24行
resp.Body
はReadCloser
型なので、defer文でClose
関数を実行してリソースを解放するようにしています。
defer resp.Body.Close()
- 26〜29行
ioutil
パッケージのReadAll
関数でresp.Body
を読み込んで、[]byte
型の変数body
に格納しています。エラーが発生した場合は、そのエラーをそのまま返しています。
body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err }
- 31〜33行
FetchResponse
型の値を作成してそのポインターを返しています。
return &FetchResponse{ Body: string(body), }, nil
handler/api/fetch.go
handler/api
ディレクトリに、fetch.go
ファイルを作成します。このファイルでは、リクエストを受け取ってレスポンスを返す処理を定義します。
package api import ( "net/http" "github.com/labstack/echo" "github.com/mediadotech/go-fetch/logic" ) type fetchRequest struct { URL string } func BindFetchHandler(g *echo.Group) { g.POST("/fetch", fetch) } func fetch(c echo.Context) error { req := new(fetchRequest) if err := c.Bind(req); err != nil { return c.NoContent(http.StatusBadRequest) } resp, err := logic.Fetch(req.URL) if err != nil { return c.NoContent(http.StatusBadRequest) } return c.String(http.StatusOK, resp.Body) }
- 7行
logic
パッケージをインポートしています。github.com/mediadotech/go-fetch
の部分は、自身のリポジトリのURLに置き換えてください。
"github.com/mediadotech/go-fetch/logic"
- 10〜12行
fetchRequest
という型を定義しています。fetchRequest
はリクエスト情報を保持するための構造体で、string
型のURL
をメンバーに持ちます。
type fetchRequest struct { URL string }
- 14〜16行
BindFetchHandler
関数を定義しています。この関数では、URLに対応するハンドラーをバインドします。
func BindFetchHandler(g *echo.Group) {
(省略)
}
- 15行
/fetch
というパスに対してPOSTリクエストが送信された場合の処理を定義しています。ここでは、fetch
関数を実行するようにしています。
g.POST("/fetch", fetch)
- 18〜30行
fetch
関数を定義しています。この関数では、リクエストを元にウェブページをフェッチし、レスポンスを生成します。
func fetch(c echo.Context) error { (省略) }
- 19〜22行
リクエスト情報をfetchRequest
型の変数req
にバインドしています。エラーが発生した場合は、ステータスコード400
(http.StatusBadRequest
)を返しています。
req := new(fetchRequest) if err := c.Bind(req); err != nil { return c.NoContent(http.StatusBadRequest) }
- 24〜27行
logic
パッケージのFetch
関数でウェブページをフェッチしています。エラーが発生した場合は、ステータスコード400
(http.StatusBadRequest
)を返しています。
resp, err := logic.Fetch(req.URL) if err != nil { return c.NoContent(http.StatusBadRequest) }
- 29行
ウェブページのフェッチに成功した場合、ステータスコードは200
(http.StatusOK
)で、取得したレスポンスボディをそのまま返しています。
return c.String(http.StatusOK, resp.Body)
router/router.go
router
ディレクトリに、router.go
ファイルを作成します。このファイルでは、URLと対応するハンドラーのバインディング処理を定義します。
package router import ( "github.com/labstack/echo" "github.com/mediadotech/go-fetch/handler/api" ) func Bind(e *echo.Echo) { apiGroup := e.Group("api") api.BindFetchHandler(apiGroup) }
- 5行
handler/api
パッケージをインポートしています。github.com/mediadotech/go-fetch
の部分は、自身のリポジトリのURLに置き換えてください。
"github.com/mediadotech/go-fetch/handler/api"
- 8〜11行
Bind
関数を定義しています。この関数では、URLに対応するハンドラーをバインドします。
func Bind(e *echo.Echo) {
(省略)
}
- 9行
apiGroup
というグループを作成しています。グループはURLで言うところのパスに相当するもので、ここではapi
というパスを定義しています。
apiGroup := e.Group("api")
- 10行
handler/api
パッケージのBindFetchHandler
関数でハンドラーの割り当てを行なっています。
api.BindFetchHandler(apiGroup)
server.go
server.go
ファイルを作成します。このファイルでは、サーバー本体の処理を定義します。
package main import ( "github.com/labstack/echo" "github.com/mediadotech/go-fetch/router" ) func main() { e := echo.New() router.Bind(e) e.Static("/", "public") e.Logger.Fatal(e.Start(":1323")) }
- 5行
router
パッケージをインポートしています。github.com/mediadotech/go-fetch
の部分は、自身のリポジトリのURLに置き換えてください。
"github.com/mediadotech/go-fetch/router"
- 8〜13行
main
関数を定義しています。プログラムの実行時には、この関数が呼び出されます。
func main() {
(省略)
}
- 9行
Echoのインスタンスを作成しています。
e := echo.New()
- 10行
router
パッケージのBind
関数でルーティングの設定を行なっています。
router.Bind(e)
- 11行
public
ディレクトリ内のファイルを静的リソースとして配置しています。
e.Static("/", "public")
- 12行
1323
ポートでサーバーを起動しています。
e.Logger.Fatal(e.Start(":1323"))
ウェブページを作成
今回はGoとEchoがメインですので、とくにライブラリやフレームワークは使わずに素のHTMLとCSSで書いていきます。
public/index.html
public
ディレクトリに、index.html
ファイルを作成します。このファイルでは、入力フォームを持つページを定義します。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>go-fetch</title> <style> body { margin: 8px; } #url { width: calc(100% - 8px); } </style> </head> <body> <form method="post" action="/api/fetch"> <label> <div>URL</div> <input type="url" id="url" name="url" required autofocus> </label> <div> <input type="submit" value="Fetch"> </div> </form> </body> </html>
- 19〜27行
/api/fetch
に対してPOSTリクエストを送信するための入力フォームを設置しています。
<form method="post" action="/api/fetch"> <label> <div>URL</div> <input type="url" id="url" name="url" required autofocus> </label> <div> <input type="submit" value="Fetch"> </div> </form>
- 8〜14行
入力フォームの横幅を広くするために、申し訳程度のCSSを書いています。
body { margin: 8px; } #url { width: calc(100% - 8px); }
プログラムを実行
それでは、作成したプログラムを実行してみましょう。次のコマンドで、server.go
ファイルを実行します。
$ go run server.go
ブラウザでhttp://localhost:1323/にアクセスすると、次のような入力フォームが表示されるはずです。
試しに、http://example.com/
と入力して実行してみましょう。すると、次のような文字列が表示されるはずです。
example.com
はIANAが管理している例示用のドメインで、実際にアクセスすると簡素なウェブページが表示されます。*5上記の画像は、サーバー上でフェッチしたhttp://example.com/
のHTMLが表示されているというわけです。
おわりに
今回は使用しませんでしたが、Echoには他にも便利な機能がたくさんあります(筆者が使ったことのないものも含めて)。たとえば、ミドルウェアという仕組みを使えば、BASIC認証やCORSなどの機能も容易に実装できます。アプリケーション独自の処理であっても、ミドルウェアを自作することで実現可能です。数多あるウェブアプリケーションフレームワークの選択肢の一つとして、Echoを使ってみてはいかがでしょうか。
今回はTech Do Book の紹介でした。現在vol.5まで発行されている合同誌には、エンジニア組織が選択している技術、Go言語に関する内容、エンジニア採用の話など様々なコンテンツが入っています。興味のある方は、こちらのリンクから無料ダウンロードいただけます!
booth.pm