LoginSignup
16
13

More than 3 years have passed since last update.

Go言語初心者がGAE+Echo(v2)+goonでサーバーを構築する

Last updated at Posted at 2017-07-23

この記事を三行でいうと

  • ゼロからGo開発環境を構築
  • Echo(v2.0)+goonでWebサーバーを構築してGAEローカルサーバーで動かしてみた
  • 何かテストでいいノウハウがあったら教えてください

追記(2019/09/08)

最新版も記載しました。
GAE第二世代+Go v1.12でechoサーバーを構築する(2019年度版)
https://qiita.com/br_branch/items/a26480a05ecb97ac20b3

記事の内容

Go言語をいじりだして1ヶ月目の初心者です。掲題の環境で開発をする仕事がまわってきたのですが、途中から参画したこともあってよくわからないままこれまでやってきました。ただ、それだとちょっと気持ち悪かったので、学習も兼ねてゼロから構築してみた話をつらつら書いていきます。
WebアプリケーションのフレームワークはEchoのv2を使います。また、Google Cloud Datastoreを簡単に操作できるgoonを使ってデータ操作をします。

※自分の備忘録も兼ねてるので無駄な記述もありますがご了承ください。

やったこと

  • (Mac OSXでの)Go開発環境の構築
    • Go 1.8.3
    • Gogland
    • App Engine SDK for Go
    • direnv
    • glide
  • Echo+goonでのHello world
  • テストしやすい設計の検討(答え出てない)

Go開発環境の構築

この節のゴール

まずは(後々Echo + goon で作成することは考慮にいれつつ) Google App Engine Go Standard Environment ドキュメント にあるサンプルソースがローカル環境上で動くようになるまでを目指します。
※ ただし環境はMac OS v10.12 Sierraです

プロジェクトのディレクトリ構成

以下のディレクトリ構成の前提で話を進めていきます。

  • /ProjectDir
    • .envrc
    • app.yaml
    • main.go

Go言語のインストール

これは簡単ですね。 Go言語の公式サイトからインストールすれば完了です。とりあえず1.8.3の現時点での最新版にしました。
GAEはGo言語の1.6で動いているらしい(と構築後に気づいた)ので、合わせたほうがいいかなと思ったのですが、今のところは特に問題なく動いてるのでこのままにしてます。
ちなみに、Go言語のバージョン管理で goenv というのもあるらしい。まあ必要になったら入れようと思います。

Goglandのインストールと設定

GoglandはJetBrains社が2016年に出したGo言語用のIDEですね。今のところ無料で使えるので使ってます。
インストール後は、GOPATHを設定するだけでもう開発できちゃいます。

image.png
GOPATH/ProjectDir の絶対パスを選択

App Engine SDK For Go

Go言語で作成したプロジェクトをApp EngineにデプロイするためのSDKです。その他、開発用に、ローカル環境に擬似的にAppEngineの環境(MemcacheやDatastore)を立ち上げてくれるツールなども存在します。
以前は appcfg.pyというSDK内のスクリプトでデプロイをしていたそうですが、最近は gcloud 内のコマンドに統合されたそうです。なので、おそらくappcfg.pyは非推奨になるのだと思うのですが、色々試してみたもののgcloudではEcho+goon+glideで作成したプロジェクトのデプロイに失敗するので以前のを使って構築します(だれかgloudでデプロイする方法知ってる人いたら教えて><)

これも簡単ですね。 Quickstart for Go App Engine Standard Environmentの"Download The SDK"に従ってとってくるだけです。
ただ、上にも書いたように今回はappcfg.pyを利用するため、 Google Cloud SDKだけじゃなく、Optional: Download and install the original App Engine SDK for Goの方もダウンロード&インストールします(両方必要なのかどうかは不明)。

direnvのインストール

direnvとは、対象のディレクトリをカレントディレクトリにした際に環境変数を自動で書き換えてくれる君です。Go言語の場合、GOPATHの環境変数を設定しないと動かなかったりするので、この子を使うととても便利っぽい。

インストールは、Macの場合以下だけでできます(Homebrewを入れてるの前提)。

$ brew install direnv

インストール後、.bash_profile に以下の設定をします。

# エディタはお好きなものでOK
export EDITOR=/usr/bin/vim
eval "$(direnv hook bash)"

また、/ProjectDirで以下のコマンドを打ち、.envrcファイルを作成します。

$ cd ${/ProjectDir}
$ direnv edit .
# 上記設定の場合Vimが立ち上がるので、以下入力
export GOPATH=$(pwd)

glideのインストール

glideはGo言語用の各プロジェクトごとのパッケージ管理らしいです。
プロジェクトごとの、とあえていうのは、Go言語には言語仕様でパッケージ管理っぽいことができるらしい(go getのことなのかな? まだよくわかってない。。。)のですが、その場合globalとなるため、色々アレだよねっていうのでできたらしいです。

インストールは以下できます。そう、Macならね。

$ brew install glide

まだインストールするだけで、特に何も設定はしません。

サンプルプロジェクトのダウンロードと動作確認

本当はglideとかdirenvとかは入れなくてもサンプルプロジェクトは動くのですが。
サンプルプロジェクトと動かし方は Googleさんのクイックスタートに書いてあるので、ここでは割愛します。
(Download the Hello World appTest the applicationをやるだけで動きますね)

Echo v2.0とGoonでHello Worldを作成する

開発するための環境ができあがったら、Echoとgoonを使って簡単なWebアプリケーションを作ってみます。

ここからのディレクトリ構成

以下のディレクトリ構成の前提で話を進めていきます。

  • /ProjectDir
    • .envrc
    • app.yaml
    • /src
      • glide.yaml
      • glide.lock (glideで作成される)
      • /backend (ソースコード配置)
      • /maintest (テストコード配置)
      • /vendor (glideで作成される)

別にcommonファイルは今回扱いません(いちおうsubmoduleで分けるならsrc直下の方がやりやすそうだよっていう自分の備忘のために描いてるだけです)。

glide.yamlの作成

今回はEchoとgoonを利用するので、以下だけは作りはじめる前にいれておきます。
また、appengineのログなども出したいので、同じように入れておきます。
testeratorは、テストを高速にするライブラリらしいです。入れですね。

package: .
import:
- package: github.com/labstack/echo
  version: v2
  subpackages:
      - engine/standard
- package: github.com/mjibson/goon
- package: golang.org/x/net
  subpackages:
      - context
- package: google.golang.org/appengine
  subpackages:
      - log
- package: github.com/favclip/testerator

今回、ぼくは直接vimでglide.yamlを作成したのですが、glideには自動で作成するコマンドもあります。ただ自動生成の場合、既存のソースから必要とされてるパッケージを記述するって形らしいので、goファイルがないとエラーになるっぽいです。

依存関係のダウンロードは以下のコマンドを打つだけです。

$ cd ${/ProjectDir/src} # glide.yamlのあるディレクトリへ行く
$ glide up

すると、vendorのディレクトリが同一階層上に作成されて依存ファイルがぬるぬる入ってきます。
別の記事では「glide.yamlは実際のソースファイルを配置する場所(ここだと/main)に入れないとAppEngineで動かないよ!」って書き込みがあったのですが、今のところ普通に動いてるのでこのディレクトリ構成でも問題ないんじゃないかな。たぶん。
(このディレクトリ構成ならテスト時にも利用できるし)

app.yamlの編集

実際作り出す前に、 AppEngineに配置する際の設定を行います。

runtime: go
api_version: go1

handlers:
    - url: /.*
      script: _go_app
      secure: optional

automatic_scaling:
    max_concurrent_requests: 40
    min_idle_instances: 0
    max_idle_instances: 1

skip_files:
    - \.gitignore
    - \.DS_Store
    - \.envrc
    - README.md
    - ^.*\.yaml
    - \.git/.*
    - ^\.idea/.*
    - ^.*\.iml
    - src/vendor
    - src/test

たぶん重要なのが src/vendor をスキップにすることかなと思います。Echoの場合、AppEngineが許容していないライブラリを内部で利用しているため、それをスキップしないと怒られるっぽいです。
(どうしてスキップしても動いてるのかはよくわかんない。。。)

実際に作成する

長い設定も終わりです。というわけで、実際に簡単なのを作成してみます。
実際はクラスを分けてそれぞれに役割を分担させたほうがいいのだろうけど、面倒なので、/src/main内にmain.goだけを作成し、そこに全部突っ込みます。

package backend

import (
    "github.com/labstack/echo"
    "github.com/labstack/echo/middleware"
    "github.com/labstack/echo/engine/standard"
    "google.golang.org/appengine"
    "google.golang.org/appengine/log"
    "github.com/mjibson/goon"
    "net/http"
    "fmt"
    "strconv"
    "encoding/json"
)

type (
    TestEntity struct {
        _kind string `goon:"kind,TestEntity"`
        Id int64 `datastore:"-" goon:"id" json:"id"`
        Name string `datastore:"name,noindex" json:"name"`
    }
)

// 最初に呼ばれる箇所
func init() {
    e := echo.New()
        // ミドルウェアの設定
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
    e.Use(middleware.Gzip())
    e.Use(UseAppEngine)

    // Entityを作成または更新する
    e.PUT("/entity/:id/:name", func(e echo.Context) error {
        id , err := strconv.ParseInt(e.Param("id"), 10 , 64)
        if err != nil {
            log.Warningf(e.StdContext() , "failed to parseInt (id:%s).",e.Param("id"))
            return e.String(http.StatusNotFound , "")
        }
        name := e.Param("name")
        entity := &TestEntity{Id:id , Name:name}
        db := goon.FromContext(e.StdContext())
        if _, err := db.Put(entity); err != nil {
            log.Errorf(e.StdContext() , "failed to create entity.")
            return e.String(http.StatusInternalServerError, "failded to create entity.")
        }
        return e.String(http.StatusOK , fmt.Sprintf("create user (id:%d name:%s)",id, name))
    })

    // Entityを取得する
    e.GET("/entity/:id", func(e echo.Context) error {
        id , err := strconv.ParseInt(e.Param("id"), 10 , 64)
        if err != nil {
            log.Warningf(e.StdContext() , "failed to parseInt (id:%s).",e.Param("id"))
            return e.String(http.StatusNotFound , "")
        }
        db := goon.FromContext(e.StdContext())
        entity := &TestEntity{Id:int64(id)}
        if err := db.Get(entity); err != nil{
            log.Warningf(e.StdContext() , "entity not found (id : %d)", id)
            return e.String(http.StatusNotFound , fmt.Sprintf("entity not found (id : %d)", id))
        }
        result , err := json.Marshal(entity)
        if err != nil {
            log.Errorf(e.StdContext() , "failed to marshal entity.")
            return e.String(http.StatusInternalServerError, fmt.Sprintf("failed to marshal (err: %v)", err))
        }

        return e.String(http.StatusOK , string(result))

    })

    s := standard.New("")
    s.SetHandler(e)
    http.Handle("/", s)
}

// AppEngineを利用できるコンテキストを設定する
func UseAppEngine (next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        if r, ok := c.Request().(*standard.Request); ok {
            namespace := "development"
            ctx := appengine.WithContext(c.StdContext(), r.Request)
            ctx , err := appengine.Namespace(ctx, namespace)
            if err != nil {
                log.Errorf(ctx, "unresolve to set namespace (err %v)", err)
            }
            log.Infof(ctx , "namespace:%s", namespace)
            c.SetStdContext(ctx)
        }
        return next(c)
    }
}

ローカルサーバー上に起動する際にはAppEngine SDKを利用し、以下のようにします。

$ cd ${/ProjectDir}
$ dev_appserver.py app.yaml 

動作確認してみます。

$ curl -i -X PUT -s 'localhost:8080/entity/1/abc'
HTTP/1.1 200 OK
vary: Accept-Encoding
content-type: text/plain; charset=utf-8
Cache-Control: no-cache
Expires: Fri, 01 Jan 1990 00:00:00 GMT
Content-Length: 27
Server: Development/2.0
Date: Sun, 23 Jul 2017 14:14:08 GMT

create user (id:1 name:abc)

$ curl -i -X GET -s 'localhost:8080/entity/1'
HTTP/1.1 200 OK
vary: Accept-Encoding
content-type: text/plain; charset=utf-8
Cache-Control: no-cache
Expires: Fri, 01 Jan 1990 00:00:00 GMT
Content-Length: 21
Server: Development/2.0
Date: Sun, 23 Jul 2017 14:15:07 GMT

{"id":1,"name":"abc"}

雑感

結構簡単に環境構築も実装もできることは今回最初から構築してみてわかってきました。
ただ、GAEで動かす前提の設計だと、テストが少し面倒になっちゃいますね。。

少なくともビジネスロジック部分はサーバーから切り離してテストしたいのですが、その場合goonをインタフェースでラップし、Dependency Injectionパターンで作るような設計にするのがいいのかなぁ。Echoもgoonもテスト設計というのがないような気がするというか、そもそもGoのインタフェースって癖があると感じちゃうというか、なんというか。

進捗ダメです。

16
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
13