在GAE第二代上使用Go v1.12构建一个echo服务器(2019年版)

总结

以前我已经发布过关于建立环境的帖子,但它们都是旧的,所以这次我决定再次尝试建立适用于最新的GAE第二代的环境。

另外,此文章的源代码已在GitHub上公开。
https://github.com/brbranch/GaeGo112EchoSample/tree/gae_echo

2019年9月7日

我写了续集。
尝试使用Redis Cloud来替代GAE上的Mercari/datastore的Memcache连接【GAE/Go1.12】
https://qiita.com/br_branch/items/1b63b2c1dd9b4ff3931e

过去的文章 de

使用Golang1.8+echo+GoLand搭建环境,可以在Google App Engine上运行本地服务器。

Go语言初学者可以使用GAE+Echo(v2)+goon搭建服务器。

这次的目标

    • GAE/Go1.12 + echoサーバーを構築し、Datastoreにアクセスする。

 

    • ローカルでの開発環境を構築する

 

    ローカルでDatastore含めたテストを行う

行为或活动的背景条件

    • Mac OS 10.14

 

    • go1.12.9

 

    • Google Cloud SDK 253.0.0

 

    • echo v3

 

    mercari/datastore v1.6.1

从GAE第二代开始,将无法使用AppEngine API。

以前我使用的是goon,但是从这一次开始我打算试试用mercari/datastore的boom。

因为GAE/go1.12(GAE第二代)开始,无法使用AppEngine API。更多详情请参考https://cloud.google.com/appengine/docs/standard/go112/go-differences?authuser=0&hl=ja#migrating-appengine-sdk。

App Engine 不会修改 Go 工具链以包含 appengine 包。如果您正在使用 appengine 包或 google.golang.org/appengine 包,请迁移到 Google Cloud 客户端库。
(省略)
要在 App Engine 中使用 Memcache 服务,请使用 Redis Labs Memcached Cloud 替代 App Engine Memcache。

しれっとひどいことやってきますね。
goonはAppengine APIを前提としてるっぽいので、どうも使えないようなんです。

或许并不只有坏事。听说从go1.12开始要使用App Engine的第二代,虽然不太明白有什么改进,但既然是第二代,可能会有一些变好的地方。

詳細:Google App Engine 标准环境的运行时
https://cloud.google.com/appengine/docs/standard/runtimes

虽然不太清楚什么会改善,但感觉好像有些东西会变好,这是好事。
太好了,太好了。

我认为自由度肯定会增加。顺便说一下,看起来也不能再使用AppEngine Memcache了。

首先尝试以最简配置进行运行。

首先,我們將引入 echo,並在本地運行 Hello World,直到完成。

最初的文件夹组织

我做了以下的改变。

backend (GOPATH)
├-- src (作業フォルダ)
├-- .envrc
└-- .go-version

第一步,首先要确保能够以最低限的要求运行。

首先,为了确认最低限度的工作并能够部署,请将GAE的快速入门示例(helloWorld)放入src文件夹中。

把名字只保留在main.go / main_test.go等文件中。

backend (GOPATH)
├-- src (作業フォルダ)
│   ├ app.yaml
│   ├ main.go
│   └ main_test.go
├-- .envrc
└-- .go-version

然后,我将尝试正常部署。

$ cd ./backend/src
$ $ gcloud app deploy
Services to deploy:

descriptor:      [/Users/xxxxx/git/my/xxxxxx/backend/src/app.yaml]
source:          [/Users/xxxxx/git/my/xxxxxx/backend/src]
target project:  [xxxxxx]
target service:  [default]
target version:  [20190906t203700]
target url:      [https://xxxxxx.appspot.com]


Do you want to continue (Y/n)?  Y

Beginning deployment of service [default]...
Created .gcloudignore file. See `gcloud topic gcloudignore` for details.
╔════════════════════════════════════════════════════════════╗
╠═ Uploading 3 files to Google Cloud Storage                ═╣
╚════════════════════════════════════════════════════════════╝
File upload done.
Updating service [default]...done.
Setting traffic split for service [default]...done.
Deployed service [default] to [https://xxxxx.appspot.com]

You can stream logs from the command line by running:
  $ gcloud app logs tail -s default

To view your application in the web browser run:
  $ gcloud app browse
image.png

真好啊!

第二步,尝试在本地运行。

从 `go1.12` 开始,可以简单地在本地使用 `go run` 命令来运行程序。
也就是说,不再需要使用 GAE 本地服务器了。

$ go run main.go
2019/09/06 21:18:30 Defaulting to port 8080
2019/09/06 21:18:30 Listening on port 8080
image.png

哦,原来如此。

第三步:安装Go Module

接下来,我们将实际安装Go模块并进行部署的尝试。

$ go mod init gaego112echosample
go: creating new go.mod: module gaego112echosample
$ ls
app.yaml     go.mod       main.go      main_test.go

暫時先不使用,但是我會加入echo。

image.png

由于没有包含库文件,将会导致赤字。
我们来尝试构建一下。

$ go build
go: finding github.com/labstack/echo v3.3.10+incompatible
go: downloading github.com/labstack/echo v3.3.10+incompatible
go: extracting github.com/labstack/echo v3.3.10+incompatible
go: finding github.com/labstack/gommon/log latest
go: finding github.com/labstack/gommon/color latest
︙

然后,情况就会变成这样。

image.png

如果您在环境变量中设置了 GO111MODULE=on,在 $GOPATH 中设置的位置将不会下载软件包,而是下载到 go env GOPATH 输出的位置。

在这个状态下,我尝试了相同的部署。以前在使用dep或glide时可能会遇到依赖问题或发生错误,但即使现在也能够无任何问题地部署并运行。

image.png

第四步。创建为Echo服务器。

好的,下一次我們將開始集成Echo。
首先,最基本的就可以了。

package main

import (
    "fmt"
    "github.com/labstack/echo"
    "log"
    "net/http"
    "os"
)

func main() {
    e := echo.New()
    http.Handle("/", e)

    e.GET("/", func (e echo.Context) error {
        return e.String(http.StatusOK, "Hello Echo!")
    })

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
        log.Printf("Defaulting to port %s", port)
    }

    log.Printf("Listening on port %s", port)
    log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}

在本地或部署后都能正常运行。

image.png
image.png

很开心。

第五步:生成日志

如下所述,google.golang.org/appengine 现在已经无法使用。
可以使用标准的log包作为替代。

让我们试试看,这意味着它可以从任何地方轻松地被召唤出来。

    e.GET("/", func (e echo.Context) error {
        log.Printf("Logger test") /// 追加
        return e.String(http.StatusOK, "Hello Echo!")
    })

当您部署并访问时,通常会在日志中显示。

image.png

嗯,这个挺轻松的呢。

第六步。处理DataStore。

现在,我们终于要开始处理DataStore了。顺便一提,我们还要尝试将处理过程拆分为包。
正如之前提到的,由于第二代AppEngine无法使用AppEngine API,所以我们以前使用的google.golang.org/appengine/datastore已经无法使用了。
相反,我们将使用cloud.google.com/go/datastore,并借用由Mercari开发的boom。

package handler

import (
    "cloud.google.com/go/datastore"
    "github.com/labstack/echo"
    "go.mercari.io/datastore/boom"
    "go.mercari.io/datastore/clouddatastore"
    "log"
    "net/http"
    "os"
)

type Post struct {
    Kind    string `datastore:"-" boom:"kind,post" json:"-"`
    ID      int64 `datastore:"-" boom:"id" json:"id"`
    Content string `datastore:"content" json:"content"`
}

func HelloWorld(e echo.Context) error {
    // app.yaml などにPROJECT_IDを設定しておく
    projectId := os.Getenv("PROJECT_ID")
    log.Printf("Project ID: %s", projectId)
    // DataStore Clientの作成
    ctx := e.Request().Context()
    dataClient, err := datastore.NewClient(ctx, projectId)
    if err != nil {
        log.Fatalf("failed to get client (reason: %v)", err)
        return e.String(http.StatusInternalServerError, "error")
    }
    // mercari.datastoreでラップする
    client, err := clouddatastore.FromClient(ctx, dataClient)
    if err != nil {
        log.Fatalf("failed to get datastoreclient (reason: %v)", err)
        return e.String(http.StatusInternalServerError, "error")
    }
    defer client.Close()
    // boomを利用
    b := boom.FromClient(ctx, client)
    post := &Post{ID: 12345, Content:"test"}
    // 保存
    if _, err := b.Put(post); err != nil {
        log.Fatalf("failed to put datastore (reason: %v)", err)
        return e.String(http.StatusInternalServerError, "error")
    }
    // 取得
    getPost := &Post{ID: 12345}
    if err := b.Get(getPost); err != nil {
        log.Fatalf("failed to get datastore (reason: %v)", err)
        return e.String(http.StatusInternalServerError, "error")
    }

    return e.JSON(http.StatusOK, getPost)
}
// 抜粋
e.GET("/", handler.HelloWorld)

これでローカル環境でも、GAEの環境でもGCP/DataStoreに保存 / 取得することができました。

image.png

如果gloud上登录的用户具有访问权限,似乎可以自动连接到本地环境。

第七步骤:访问本地的DataStore模拟器。

虽然也可以在这里进行开发,但为了方便起见,我们当然希望能连接到本地的模拟器。
因此,我将尝试进行相应的设置。

ローカルのエミュレーターを起動

以下のコマンドでできます。
–project の部分は [a-z][a-z0-9\-]+ の命名規則であれば任意でも問題ないようです。指定しなければ gcloud で設定している current project id が入ります。

$ gcloud beta emulators datastore start --host-port localhost:8059 --project test-project
WARNING: Reusing existing data in [/Users/xxxx/.config/gcloud/emulators/datastore].
Executing: /Users/xxxx/tools/google-cloud-sdk/platform/cloud-datastore-emulator/cloud_datastore_emulator start --host=localhost --port=8059 --store_on_disk=True --consistency=0.9 --allow_remote_shutdown /Users/xxxx/.config/gcloud/emulators/datastore
[datastore] 9 07, 2019 2:00:18 午後 com.google.cloud.datastore.emulator.CloudDatastore$FakeDatastoreAction$9 apply
[datastore] 情報: Provided --allow_remote_shutdown to start command which is no longer necessary.
[datastore] 9 07, 2019 2:00:18 午後 com.google.cloud.datastore.emulator.impl.LocalDatastoreFileStub <init>
[datastore] 情報: Local Datastore initialized:
[datastore]     Type: High Replication
[datastore]     Storage: /Users/xxxx/.config/gcloud/emulators/datastore/WEB-INF/appengine-generated/local_db.bin
[datastore] 9 07, 2019 2:00:18 午後 com.google.cloud.datastore.emulator.impl.LocalDatastoreFileStub load
[datastore] 情報: The backing store, /Users/xxxx/.config/gcloud/emulators/datastore/WEB-INF/appengine-generated/local_db.bin, does not exist. It will be created.
[datastore] API endpoint: http://localhost:8059
[datastore] If you are using a library that supports the DATASTORE_EMULATOR_HOST environment variable, run:
[datastore]
[datastore]   export DATASTORE_EMULATOR_HOST=localhost:8059
[datastore]
[datastore] Dev App Server is now running.
[datastore]
[datastore] The previous line was printed for backwards compatibility only.
[datastore] If your tests rely on it to confirm emulator startup,
[datastore] please migrate to the emulator health check endpoint (/). Thank you!

让我们亲身实际操作一下

在启动上述模拟器的情况下,我将在本地进行协作。
请使用以下命令运行。

$ env DATASTORE_EMULATOR_HOST=localhost:8059 DATASTORE_PROJECT_ID=test-project go run main.go

当访问 localhost:8080 时,页面会正常显示,并在启动模拟器的终端上输出日志。

[datastore] The health check endpoint for this emulator instance is http://localhost:8059/9 07, 2019 2:06:34 午後 io.gapi.emulators.grpc.GrpcServer$3 operationComplete
[datastore] 情報: Adding handler(s) to newly registered Channel.
[datastore] 9 07, 2019 2:06:34 午後 io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
[datastore] 情報: Detected HTTP/2 connection.
[datastore]
[datastore]
[datastore] 9 07, 2019 2:06:48 午後 com.google.cloud.datastore.emulator.impl.LocalDatastoreFileStub lambda$persist$7
[datastore] 情報: Time to persist datastore: 27 ms

这样一来,在本地环境下的开发也会变得更加顺利了(`・ω・´)

在本地仿真器内查看(Google Cloud图形用户界面)

ただ、エミュレーターの場合今までのように Appengine Local Serverの管理画面からDataStoreの中を見るということができません。ちょっと気になったりすることもあるんで、それも見れるようにします。

すごい人が作ってたので、それを利用します。

谷歌云的图形用户界面(Google Cloud GUI)

$ npm i -g google-cloud-gui
$ google-cloud-gui

执行以上操作,浏览器会自动启动。

image.png

只需要一种选择:在“Projects”左侧按下+按钮,就可以确认添加项目。

image.png

很棒。当我查看终端时,有很多错误,但它可以使用,所以没问题。

第八步:撰写测试用例。

终于快要到达目标了。
这次我们先使用DataStore模拟器,试图以实际连接DataStore的方式来描述测试。

func TestHelloWorld(t *testing.T) {
    // 環境変数をセット
    os.Setenv("DATASTORE_PROJECT_ID", "test-project")
    os.Setenv("DATASTORE_EMULATOR_HOST", "localhost:8059")

    e := echo.New()
    req, err := http.NewRequest("GET", "/", nil)
    if err != nil {
        t.Fatal(err)
    }

    rr := httptest.NewRecorder()

    c := e.NewContext(req, rr)
    err = HelloWorld(c)
    if err != nil {
        t.Errorf("unexpeced handler reponse (err: %v)", err)
        return
    }

    if status := rr.Code; status != http.StatusOK {
        t.Errorf(
            "unexpected status: got (%v) want (%v)",
            status,
            http.StatusOK,
        )
    }

    expected := `{"id":12345,"content":"test"}`
    if strings.TrimSpace(rr.Body.String()) != expected {
        t.Errorf(
            "unexpected body: got (%v) want (%v)",
            rr.Body.String(),
            expected,
        )
    }
}

在启动DataStore模拟器的状态下运行,测试通过了。

=== RUN   TestHelloWorld
2019/09/07 14:51:33 Project ID: 
--- PASS: TestHelloWorld (0.02s)
PASS

尝试后的感受

总的来说,我们成功地完成了搭建,没有遇到太多问题。
感谢Go Module使我们不再被vendoring束缚,这真是太好了。
不过,正如我之前提到的,从GAE/go1.12开始,无法使用AppEngine API,这真是相当令人沮丧。
这对现有项目造成的影响可能相当大…

还没做好的事情

因为这种情况,现在应该无法实现DataStore和memcache的协作。
(我在某处读到mercari/datastore非常出色并提供本地缓存的文章,但我还没有确认其功能)
“未来不要使用Appengine Memcache,而是使用Redis Cloud”,GCP也说了,下一次我们可以尝试实现这种协作。
据说Redis Cloud在30MB之前是免费的。

30MB啊… (´・_・`)
就算是个人使用,实际应用起来似乎还是需要付费。

故障排除

在构建过程中出现的错误将被整理记录。

在Go环境中无法安装Go 1.12。

我在使用 goenv 进行 Go 版本管理,但在我的环境中,即使运行 install –list 命令也没有显示任何内容,一开始无法成功安装。

$ goenv install 1.12.9
go-build: definition not found: 1.12.9

See all available versions with `goenv install --list'.

If the version you need is missing, try upgrading goenv:

  brew update && brew upgrade goenv

解决的途径

我通过以下方法进行了更新。

$ brew update # brew自体をアップデートする
$ $ brew install --HEAD goenv
Error: Xcode alone is not sufficient on Mojave.
Install the Command Line Tools:
  xcode-select --install
# 怒られたんで、言われるままにやります(というか入ってなかったのか…)
$ xcode-select --install
xcode-select: note: install requested for command line developer tools
$ brew install --HEAD goenv
Error: goenv 1.23.3 is already installed
To install HEAD, first run `brew unlink goenv`.
# また怒られた。言われた通りにunlinkする
$ $ brew unlink goenv
Unlinking /usr/local/Cellar/goenv/1.23.3... 4 symlinks removed
$ brew install --HEAD goenv
# 成功!

只有版本号为Mac OS 10.14的情况下,无法使用go1.12进行测试。

为什么每次尝试运行go test都会出现这样的错误呢?

$ go test
go: creating new go.mod: module github.com/brbranch/JankenShogiOnline
# runtime/cgo
In file included from gcc_libinit.c:8:
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/pthread.h:232:66: error: unknown type name 'size_t'
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/pthread.h:249:43: error: unknown type name 'size_t'
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/pthread.h:256:66: error: unknown type name 'size_t'
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/pthread.h:524:1: error: unknown type name 'size_t'
gcc_libinit.c:97:18: error: variable has incomplete type 'struct timespec'
gcc_libinit.c:97:9: note: forward declaration of 'struct timespec'
gcc_libinit.c:110:3: error: implicit declaration of function 'nanosleep' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
FAIL    github.com/xxx/xxxx/backend/src [build failed]

解决方案 (jiě jué àn)

这个问题在GitHub上的地址是https://github.com/golang/go/issues/30072。

image.png

虽然我不是很明白,但是我按照以下的方法进行后,能够顺利进行测试。

$ env CGO_ENABLED=0 go test
PASS
ok      github.com/xxxx/xxxx/backend/src    0.013s

虽然我不太理解,但是因为它在动,所以我认为是正义的(´・ω・`)。

请提供更具体的上下文,以便我为您进行更准确的翻译。

App Engine 标准环境的运行时环境
https://cloud.google.com/appengine/docs/standard/runtimes

将您的App Engine应用迁移到Go 1.12

谷歌云图形用户界面
https://github.com/GabiAxel/google-cloud-gui

分享一下关于Mercari/Datastore的经验和见解

使用GAE+GO+Echo进行了对go1.11的兼容。

bannerAds