概要
こんにちは。kwashiです。最近、コンテナ関連の技術にハマリ気味で、Golangの勉強を始めました。いままで、Pythonばかり使っていたのですが、すっかり、Golangにハマってしまいました。そこで、復習も兼ねて、GolangのインストールからWebアプリケーションの作成、テスト、Dockerコンテナ化までを対象に記事にしてみました。Golangの構文はわかったけど、次どうしようと思っている方にはちょうどいいかもしれません。
Golangのメリットは、以下のようなことが挙げられます。
-
Dockerイメージが軽量
例えば、本記事で作成したアプリケーションは、以下のように22.6MBとかなり軽量です。REPOSITORY TAG IMAGE ID CREATED SIZE kwashizaki/example-golang-rest-api v1.0.0 8d92d819d8ad 8 days ago 22.6MB
-
パフォーマンス
JavaやC++に匹敵するぐらい性能(処理速度)が良いです。 -
可読性
Gofmtにより強制的にコードフォーマットされ、一貫したフォーマットになります。 -
コンパイル時間が早い
C++やJavaと比較してもかなり早いです。 -
gRPCのサポート
昨今、マイクロサービスのはやりとともに、RPC経由の通信が増えています。
GolangによるgRPCに関しては、Golangで始めるgRPCにて記事にしたので参考にしてみてください。 -
並列処理が書きやすい
"go"というキーワードを関数呼び出し時に付け加えることで、goroutineというGoの軽量なスレッドを簡単に実行することができます。また、channelとうgoroutine間で通信する方法も用意されています。
個人的には、機械学習やデータ解析など複雑なことをする場合は、Pythonを選択します。一方で、データのCRUDや外部APIとの連携などのサービス(機能)を作成する場合は、Golangを選択します。
※ 注意
本記事では、Webアプリケーションのフレームワークとして、ginを用います。また、Mac OS上でのみ動作を確認しています。
また、git, Docker for mac, VSCodeも既にインストールされているものとします。
git: k-washi/example-golang-rest-apiの/health関連部分が本記事に対応しています。
Docker image: kwashizaki/example-golang-rest-api
作成するWeb アプリケーションについて
アプリケーションは、ベースのURL + pathで表現されるURLへの問い合わせに対応したAPIです。
/health/は単純にGETに対してstatus 200を返します。
Base URL = http://localhost:8080
- path: /health/
- GET:
res: {"health": 200}
Goのインストールと設定
1. インストール
Golangからダウンロードして展開してください。
以下のコマンドでインストールされたか確認してください。
go version
#go version go1.12.7 darwin/amd64
2. 環境設定
環境変数として、以下の変数を設定してください。
GOPATHは、goのプロジェクトを構築する環境です。また、GO111MODULEは、Go1.11から導入され始めたGoの新しいバージョン管理ツールGo Modules(vgo)を使用を管理するためのものです。適宜、個人の環境に合うように設定してください。
export GOPATH=$(go env GOPATH)
export PATH=$PATH:$GOPATH/bin
export GOPATH=/Users/xxxxxxxx/Documents/workspaces/golang
export GO111MODULE=on
この環境変数を毎回設定するのが面倒なので、.bash_profileに書き込みます。
vi ~/.bash_profile
cat ~/.bash_profile
#...
#export GOPATH=$(go env GOPATH)
#export PATH=$PATH:$GOPATH/bin
#export GOPATH=/Users/wxxxxxx/Documents/workspaces/golang
#export GO111MODULE=on
また、GOPATH内にソースフォルダー(src)などを作成します。
tree -L 2
.
├── bin
│ ├── gobump
│ ├── ...
│ └── protoc-gen-go
├── pkg
│ └── mod
└── src
├── github.com
└── golang.org
今回は、github上でソースを管理することを想定します。例えば、gitbubにk-washi/example-golang-rest-apiというリポジトリを作成したとします。$GOPATH/src/github.com/k-washi/で、git clone https://github.com/k-washi/example-golang-rest-api.git
とすることでgithubに対応したプロジェクトの開始ができます。 こちらも個人の設定に合わせてください。
3. VSCodeの設定
ExtensionのGoをインストールする。
実装
本章では、 http://localhost/health/
でstatus 200が返ってくるWeb アプリケーションを作成します。
Webフレームワークとして、軽量かつシンプルなGinを使用するので、以下のコマンドでダウンロードします。
go get -u github.com/gin-gonic/gin
まず、mainの処理を実装します。
versionはビルド時に使用します。
package main
import (
"github.com/k-washi/example-golang-rest-api/src/app"
)
const version = "0.1.0"
func main() {
app.StartApp()
}
StartApp()はsrc/appに以下のように実装しています。package appになっていることに注目してください。このappのパッケージがmain.goで呼び出されています。
- router.Use() - ミドルウェア処理を追加(ルーティングされる前に処理されます)
- mapUrls() - app.goの下にプログラムを記載しています。同じディレクトリで同じパッケージ名にてプログラムを始めているので、routerという変数が共有して使用できていることが見て取れます。ここでは、routerにGETなどのメソッド、ルーティングするパス、実行する処理(health pkgのGetHealthStatusOK関数)を登録しています。
- router.Run(:8080)で8080ポートでサーバーを実行しています。
package app
import (
"github.com/gin-gonic/gin"
"github.com/k-washi/example-golang-rest-api/src/middleware"
)
var (
router *gin.Engine
)
func init() {
router = gin.Default()
}
//StartApp call by main for starting app.
func StartApp() {
router.Use(middleware.OptionsMethodResponse())
mapUrls()
if err := router.Run(":8080"); err != nil {
panic(err)
}
}
package app
import (
"github.com/k-washi/example-golang-rest-api/src/controllers/health"
)
func mapUrls() {
router.GET("/health", health.GetHealthStatusOK)
}
次に、ミドルウェアについて説明します。ミドルウェアでは、CORSの対策のため、レスポンスのHeader設定とOPTIONメソッドへの対応を行っています。
設定する内容は、以下のコードに記載しています。アプリケーションに合わせて適宜設定が必要となります。
メソッドがOPTIONのとき、Abord()を用いることで、URLに合わせたルーティングの実行を行わずにstatus 200を返答しています。
OPTIONではないとき、Next()を用いて、ルーティングの実行に移ります。
CORSに関しては, CORSまとめが参考になりました。
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
)
//OptionsMethodResponse CROS options response
func OptionsMethodResponse() gin.HandlerFunc {
return func(c *gin.Context) {
//アクセスを許可するドメインを設定
c.Writer.Header().Set("Access-Control-Allow-Origin", "http://localhost")
//リクエストに使用可能なメソッド
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
//Access-Control-Allow-Methods 及び Access-Control-Allow-Headers ヘッダーに含まれる情報をキャッシュすることができる時間の長さ(seconds)
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
//リクエストに使用可能なHeaders
c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Origin, X-Requested-With, Content-Type, Accept")
//認証を使用するかの可否
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
//レスポンスのContent-Typeを設定する
c.Writer.Header().Set("Content-Type", "application/json")
if c.Request.Method != "OPTIONS" {
c.Next()
} else {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
c.Abort()
}
return
}
}
次に、/health/へのルーティング時に呼ばれる関数GetHealthStatusOK()を実装します。
サービスHealthService.GetHealth()を実行して、エラーがなければ、結果を返します。
package health
import (
"net/http"
"github.com/k-washi/example-golang-rest-api/src/services"
"github.com/gin-gonic/gin"
)
//GetHealthStatusOK status 200 response.
func GetHealthStatusOK(c *gin.Context) {
result, err := services.HealthService.GetHealth()
if err != nil {
c.JSON(err.GetStatus(), err)
return
}
c.JSON(http.StatusOK, result)
}
MVCモデルを考えた場合、先程実装したhealth/controller.goはContorollerにあたります。次にMVCモデルのModelにあたる機能(Service)を実装します。
SOLIDパターンにならって、HealthServiceを公開し、関数呼び出し時の初期化で構造体healthServiceを挿入します。また、HealthServiceの型として、healthServiceInterfaceを実装しています(この実装によりテスト時にMock作成が簡単になります)。このInterfaceにもhealthServiceに埋め込んだ関数を記載しておきます。
また、JSONで返答するデータを以下のような構造体としています。
package health
type CreateHealthResponse struct {
Status int `json:"status"`
Description string `json:"description"`
}
※エラーに関しては、独自に作成したものを使用しています。git: example-golang-rest-api/src/utils/errors/を参考にしてください。
※SOLIDパターンに関しては、Hands-On Software Architecture with Golangを参考にしました。
package services
import (
"net/http"
"github.com/k-washi/example-golang-rest-api/src/domain/health"
"github.com/k-washi/example-golang-rest-api/src/utils/errors"
)
type healthServiceInterface interface {
GetHealth() (*health.CreateHealthResponse, errors.APIError)
}
var (
//HealthService health check service
HealthService healthServiceInterface
)
type healthService struct{}
func init() {
HealthService = &healthService{}
}
func (s *healthService) GetHealth() (*health.CreateHealthResponse, errors.APIError) {
result := health.CreateHealthResponse{
Status: http.StatusOK,
Description: "Health check OK",
}
return &result, nil
}
実行
go run src/main.go
#もしappという名前でコンパイルするなら
go build -o app src/main.go
テスト
ここでは、上記で実装したhealth/contoroller.goのGetHealthStatusOK関数のテストを行います。テストにはtestifyというライブラリを使用します。
テストのため、ファイル名の最後に_test.goを追加したファイルに実装します。また、テストを実行する関数にはTestという接頭語をつける必要があります。
まず、テスト対象がhealthService.GetHealth()と依存関係があるため、healthServiceのMockを作成します。そして、
func init() {
HealthService = &healthService{}
}
としていたHealthServiceにテストのために新しく作成したhealthServiceMockを挿入します。
実際にテストを行うTestGetHealthStatusOKというテスト関数の最初でhealthService.GetHealth()が返すデータを作成し、MockのGetHealth関数が返すgetHealthFunction関数の返り値として設定しています。この結果、healthService.GetHealthは、作成した返り値を返すように処理されます。
そして、/health/へのリクエストを作成し、GetHealthStatusOKを実行します。
テストは、実行後のresponseをtestifyの機能を用いて評価しています。
package health
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/gin-gonic/gin"
"github.com/k-washi/example-golang-rest-api/src/domain/health"
"github.com/k-washi/example-golang-rest-api/src/services"
"github.com/k-washi/example-golang-rest-api/src/utils/errors"
)
/*
#依存関係のあるhealthService.GetHealthのモックを作成
*/
type healthServiceMock struct {
}
var (
getHealthFunction func() (*health.CreateHealthResponse, errors.APIError)
)
func (m *healthServiceMock) GetHealth() (*health.CreateHealthResponse, errors.APIError) {
return getHealthFunction()
}
func init() {
services.HealthService = &healthServiceMock{}
}
//TestGetHealthStatusOK test of status 200 with service mock
func TestGetHealthStatusOK(t *testing.T) {
exsistHealthResponse := health.CreateHealthResponse{
Status: http.StatusOK,
Description: "Health check OK",
}
getHealthFunction = func() (*health.CreateHealthResponse, errors.APIError) {
return &exsistHealthResponse, nil
}
response := httptest.NewRecorder()
c, _ := gin.CreateTestContext(response)
request, _ := http.NewRequest(http.MethodGet, "/health", nil)
c.Request = request
GetHealthStatusOK(c)
assert.EqualValues(t, http.StatusOK, response.Code)
//Conver the JSON response to a map
var healthJSONResponse health.CreateHealthResponse
err := json.Unmarshal([]byte(response.Body.String()), &healthJSONResponse)
//Grab the balue & whether or not it exists
statusVal := healthJSONResponse.Status
descriptionVal := healthJSONResponse.Description
assert.Nil(t, err)
assert.Equal(t, exsistHealthResponse.Status, statusVal)
assert.Equal(t, exsistHealthResponse.Description, descriptionVal)
}
テストの実行
以下のコマンドでテストを実行する。
go test ./...
Docker image & コンテナ化
本章では、Makefile、Dockerfile、Docker imageの作成を行い、コンテナ化して実際にヘルスチェックを実行します。
まず、以下のようなMakefileを作成します。
make help
で実行できるコマンドが表示されます。
また、make build
ormake bin/app
でコンパイルでき、make devel-deps
でその他開発に必要なライブラリをインストールできます。
NAME := example-golang-rest-api
VERSION := $(gobump show -r)
REVISION := $(shell git rev-parse --short HEAD)
LDFLAGS := "-X main.revision=$(REVISION)"
export GO111MODULE=on
## Install dependencies
.PHONY: deps
deps:
go get -v -d
# 開発に必用な依存をインストールする
## Setup
.PHONY: deps
devel-deps: deps
GO111MODULE=off go get \
golang.org/x/lint/golint \
github.com/motemen/gobump/cmd/gobump \
github.com/Songmu/make2help/cmd/make2help
# テストの実行
## Run tests
.PHONY: test
test: deps
go test ./...
## Lint
.PHONY: lint
lint: devel-deps
go vet ./...
golint -set_exit_status ./...
## build binaries ex. make bin/myproj
bin/%: ./src/main.go deps
go build -ldflags $(LDFLAGS) -o $@ $<
## build binary
.PHONY: build
build: bin/app
##Show heop
.PHONY: help
help:
@make2help $(MAKEFILE_LIST)
次に、Dockerfileを作成します。
アプリケーションを実行するイメージを軽量化するため、ビルドイメージと実行イメージに分けマルチステージビルドを行います(今回のアプリケーションのDocker imageは20MB程度になる)。
ビルドステージでは、おそらくプロジェクト配下にgo.modファイルが作成されているであろうgo.mod, go.sumをコピーしてgo mod download
で依存関係をインストールします。
そして、makeファイルでビルドしています。
実行ステージでは、コンパイル結果のappをビルドステージからコピーして、コンテナ化時の実行エントリーポイントと、8080ポートで公開するように設定しています。
FROM golang:1.12.7-alpine3.10 as build-step
RUN apk add --update --no-cache ca-certificates git make
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /go-app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN make devel-deps
RUN make bin/app
#runtime image
FROM alpine
COPY --from=build-step /go-app/bin/app /app
ENTRYPOINT ["./app"]
EXPOSE 8080
以下のコマンドで、Docker image化できます。
docker build -t kwashizaki/example-golang-rest-api:v1.0.0 .
そして、以下のコマンドでコンテナ化できます。
docker run -it -p 8080:8080 --rm --name example-golang-rest-api kwashizaki/example-golang-rest-api:v1.0.0
以下のコマンドで実際にJSONが返ってくるか確認できます。
curl http://localhost:8080/health
#{"status":200,"description":"Health check OK"}
お疲れさまです。以上で、簡単ですが、GolangでWebアプリケーションを作成できました。