はじめに
ちょうど3日前に Google App Engine (GAE) と Go言語を初めて触ってみました。
Go言語がバージョン 11 以降、少し勝手が違ったことで、構築時に若干迷ったので記事にしてみました。
いろんなドキュメントを見て判断はしていますが、なんせ経験が浅いので間違い等あれば指摘ください。
はじめる前に…
Go言語と GAE について私が学んだサイト等まとめておきます。
- 下記サイトで Go言語の学習
- Quickstart
- Building a Go App on App Engine
環境
OS
- Windows 10
言語
- go version go1.13.6 windows/amd64
どういう構成にしようか。
まずは GAE の複数サービス構築についてのドキュメントを読んでみる。
まず、私が見たのは下記サイト。
英語は自身無いのですが、要約すると、下記のような感じしょうか。
-
app.yaml
でアプリケーションを定義します -
service1.yaml
,service2.yaml
みたいにサービス分けることができる - ディレクトリ構造はサービス毎にディスプレイ切ってもいいし、yaml をルートに置いてソースコードだけディレクトリ切ってもいい
-
dispatch.yaml
,index.yaml
,cron.yaml
などの定義ファイルはルートに置いてね - default サービス (
app.yaml
) は最初に必ず deploy してね
どうやら Go言語にはディレクトリ構造の流儀みたいなのがあるらしい
詳しくは下記を参照ください。
目標のディレクトリ構成
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
$ cd gae-sample
$ git init
Go言語の初期化(モジュール化)
$ go mod init gae-sample
パッと書いていますが、ここも迷った点で、
Go言語の 11 以降はモジュール化すれば $GOPATH 配下にいなくても OK ?
GAE の yaml ファイル作成
$ type NUL > app.yaml
$ type NUL > dispatch.yaml
$ type NUL > service1.yaml
# デフォルト app.yaml です。
service: default
runtime: go113
main: ./src/cmd/default
# service1 の app.yaml です。
service: service1
runtime: go113
main: ./src/cmd/service1
dispatch:
- url: "*/service1/*"
service: service1
Go言語の必要ディレクトリを作成
※必要なければスキップしてください。
$ mkdir src
$ cd src
$ mkdir cmd
$ mkdir pkg
$ mkdir lib
main package を作成
$ 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 さんを使います。(service1 で若干改修)
ほんと、コピメでごめんなさい。
// 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]
// 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,
)
}
}
// 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]
// 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
$ 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 が曖昧な感じもあるので、むしろ助言いただけると助かります。
ご参考までに…。