Tech Do | メディアドゥの技術ブログ 

株式会社メディアドゥのエンジニアによるブログです。

Go + Echoでウェブページをフェッチするツールを作る

メディアドゥでは、エンジニア有志によって執筆された【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リクエストが送信された場合の処理を定義しています。ここでは、ステータスコードは200http.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.BodyReadCloser型なので、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にバインドしています。エラーが発生した場合は、ステータスコード400http.StatusBadRequest)を返しています。
req := new(fetchRequest)
if err := c.Bind(req); err != nil {
    return c.NoContent(http.StatusBadRequest)
}
  • 24〜27行
    logicパッケージのFetch関数でウェブページをフェッチしています。エラーが発生した場合は、ステータスコード400http.StatusBadRequest)を返しています。
resp, err := logic.Fetch(req.URL)
if err != nil {
    return c.NoContent(http.StatusBadRequest)
}
  • 29行
    ウェブページのフェッチに成功した場合、ステータスコードは200http.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