Google App Engine に Go言語 1.13 の複数サービスを deploy する

# はじめに
ちょうど３日前に Google App Engine (GAE) と Go言語を初めて触ってみました。
Go言語がバージョン 11 以降、少し勝手が違ったことで、構築時に若干迷ったので記事にしてみました。

いろんなドキュメントを見て判断はしていますが、なんせ経験が浅いので間違い等あれば指摘ください。

# はじめる前に…
Go言語と GAE について私が学んだサイト等まとめておきます。

- 下記サイトで Go言語の学習
    - [はじめてのGo言語](http://cuto.unirita.co.jp/gostudy/)
- [Quickstart](https://cloud.google.com/appengine/docs/standard/go/quickstart?hl=ja)
- [Building a Go App on App Engine](https://cloud.google.com/appengine/docs/standard/go/building-app/?hl=ja)

# 環境
## OS
- Windows 10

## 言語
- go version go1.13.6 windows/amd64

# どういう構成にしようか。
## まずは GAE の複数サービス構築についてのドキュメントを読んでみる。
まず、私が見たのは下記サイト。

- [Structuring Web Services in App Engine](https://cloud.google.com/appengine/docs/standard/go/configuration-files?hl=ja)

英語は自身無いのですが、要約すると、下記のような感じしょうか。
- ```app.yaml``` でアプリケーションを定義します
- ```service1.yaml```, ```service2.yaml``` みたいにサービス分けることができる
- ディレクトリ構造はサービス毎にディスプレイ切ってもいいし、yaml をルートに置いてソースコードだけディレクトリ切ってもいい
- ```dispatch.yaml```, ```index.yaml```, ```cron.yaml``` などの定義ファイルはルートに置いてね
- default サービス (```app.yaml```) は最初に必ず deploy してね


## どうやら Go言語にはディレクトリ構造の流儀みたいなのがあるらしい
詳しくは下記を参照ください。
- [Goにはディレクトリ構成のスタンダードがあるらしい。](https://qiita.com/sueken/items/87093e5941bfbc09bea8)
- [Golangプロジェクトのディレクトリ構成について考えてみた](https://yyh-gl.github.io/tech-blog/blog/go_project_template/)
- [Golangパッケージの配置ルールとディレクトリ構成](https://hodalog.com/golang-standard-project-layout/)

## 目標のディレクトリ構成

GAE と Go言語の流儀から、私はこんな感じがいいんでないかな？と思いました。

```
gae-sample (application root directory)
+-- dispatch.yaml
+-- app.yaml
+-- service1.yaml
+-- src
    +-- cmd
    |   +-- default
    |   |   +-- main.go
    |   |   +-- main_test.go
    |   +-- service1
    |       +-- main.go
    |       +-- main_test.go
    +-- api
    +-- pkg
    +-- lib
```


※ api, pkg, lib はこの記事では使用しません。

# 環境構築
## アプリケーションのルートディレクトリに移動して git init

``` shell
$ cd gae-sample
$ git init
```

## Go言語の初期化（モジュール化）

``` shell
$ go mod init gae-sample
```

パッと書いていますが、ここも迷った点で、
Go言語の 11 以降はモジュール化すれば $GOPATH 配下にいなくても OK ?


## GAE の yaml ファイル作成

``` shell
$ type NUL > app.yaml
$ type NUL > dispatch.yaml
$ type NUL > service1.yaml
```

``` app.yaml
# デフォルト app.yaml です。

service: default
runtime: go113
main: ./src/cmd/default
```

``` service1.yaml
# service1 の app.yaml です。

service: service1
runtime: go113
main: ./src/cmd/service1
```

``` dispatch.yaml
dispatch:
  - url: "*/service1/*"
    service: service1
```

## Go言語の必要ディレクトリを作成

※必要なければスキップしてください。

``` shell
$ mkdir src
$ cd src
$ mkdir cmd
$ mkdir pkg
$ mkdir lib
```

## main package を作成

``` shell
$ cd cmd
$ mkdir default
$ mkdir service1
$ cd default
$ echo package main > main.go
$ echo package main > main_test.go
$ cd ..\service1
$ echo package main > main.go
$ echo package main > main_test.go
```

main.go は GAE の Quickstart でも動かした [helloworld さん](https://github.com/GoogleCloudPlatform/golang-samples/tree/master/appengine/go11x/helloworld)を使います。（service1 で若干改修）
ほんと、コピメでごめんなさい。

``` cmd/default/main.go
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// [START gae_go111_app]

// Sample helloworld is an App Engine app.
package main

// [START import]
import (
	"fmt"
	"log"
	"net/http"
	"os"
)

// [END import]
// [START main_func]

func main() {
	http.HandleFunc("/", indexHandler)

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

	log.Printf("Listening on port %s", port)
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatal(err)
	}
	// [END setting_port]
}

// [END main_func]

// [START indexHandler]

// indexHandler responds to requests with our greeting.
func indexHandler(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	fmt.Fprint(w, "Hello, World!")
}

// [END indexHandler]
// [END gae_go111_app]
```

``` cmd/default/main_test.go
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestIndexHandler(t *testing.T) {
	req, err := http.NewRequest("GET", "/", nil)
	if err != nil {
		t.Fatal(err)
	}

	rr := httptest.NewRecorder()
	handler := http.HandlerFunc(indexHandler)
	handler.ServeHTTP(rr, req)

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

	expected := "Hello, World!"
	if rr.Body.String() != expected {
		t.Errorf(
			"unexpected body: got (%v) want (%v)",
			rr.Body.String(),
			"Hello, World!",
		)
	}
}

func TestIndexHandlerNotFound(t *testing.T) {
	req, err := http.NewRequest("GET", "/404", nil)
	if err != nil {
		t.Fatal(err)
	}

	rr := httptest.NewRecorder()
	handler := http.HandlerFunc(indexHandler)
	handler.ServeHTTP(rr, req)

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

``` cmd/service1/main.go
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// [START gae_go111_app]

// Sample helloworld is an App Engine app.
package main

// [START import]
import (
	"fmt"
	"log"
	"net/http"
	"os"
)

// [END import]
// [START main_func]

func main() {
	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/service1/", service1Handler)

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

	log.Printf("Listening on port %s", port)
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatal(err)
	}
	// [END setting_port]
}

// [END main_func]

// [START indexHandler]

// indexHandler responds to requests with our greeting.
func indexHandler(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	fmt.Fprint(w, "Hello, service1 World!")
}

// [END indexHandler]
// [START service1Handler]

// indexHandler responds to requests with our greeting.
func service1Handler(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/service1/" {
		http.NotFound(w, r)
		return
	}
	fmt.Fprint(w, "This is the service1 page!")
}

// [END service1Handler]
// [END gae_go111_app]
```

``` cmd/service1/main_test.go
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestService1Handler(t *testing.T) {
	req, err := http.NewRequest("GET", "/service1/", nil)
	if err != nil {
		t.Fatal(err)
	}

	rr := httptest.NewRecorder()
	handler := http.HandlerFunc(service1Handler)
	handler.ServeHTTP(rr, req)

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

	expected := "This is the service1 page!"
	if rr.Body.String() != expected {
		t.Errorf(
			"unexpected body: got (%v) want (%v)",
			rr.Body.String(),
			"This is the service1 page!",
		)
	}
}

func TestService1HandlerNotFound(t *testing.T) {
	req, err := http.NewRequest("GET", "/service1/404", nil)
	if err != nil {
		t.Fatal(err)
	}

	rr := httptest.NewRecorder()
	handler := http.HandlerFunc(service1Handler)
	handler.ServeHTTP(rr, req)

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

## deploy apps

``` shell
$ cd <application root directory>
$ gcloud app deploy app.yaml service1.yaml dispatch.yaml
$ gcloud app browse
$ gcloud app browse -s service1
```

- ```gcloud app browse``` でブラウザが開き、「Hello, World」が表示されたら OK です。
- ```gcloud app browse -s service1``` でブラウザが開き、「Hello, service1 World」が表示されたら OK です。
- 最後に default サービスの URL 末尾に ```/service1/``` を付け、その URL にアクセスすると、「This is the service1 page!」と表示されれば、```dispatch.yaml``` も正常に機能しているはずです。

# さいごに

Go言語 ver 1.13 で複数サービスを作成し、GAE にアップロードしてみました。
まだ Go言語の GOPATH と Module が曖昧な感じもあるので、むしろ助言いただけると助かります。
ご参考までに…。