はじめてのGolang Webアプリケーション  ~ テスト, Dockerコンテナ化まで

# 

# 概要

こんにちは。kwashiです。最近、コンテナ関連の技術にハマリ気味で、Golangの勉強を始めました。いままで、Pythonばかり使っていたのですが、すっかり、Golangにハマってしまいました。そこで、復習も兼ねて、GolangのインストールからWebアプリケーションの作成、テスト、Dockerコンテナ化までを対象に記事にしてみました。Golangの構文はわかったけど、次どうしようと思っている方にはちょうどいいかもしれません。

Golangのメリットは、以下のようなことが挙げられます。

+ Dockerイメージが軽量
  例えば、本記事で作成したアプリケーションは、以下のように22.6MBとかなり軽量です。

  ```bash
  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](https://qiita.com/kwashi/items/533bbb7d09e723c8b56f)にて記事にしたので参考にしてみてください。
  
+ 並列処理が書きやすい
  "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](https://github.com/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

```yaml
- path: /health/
  - GET: 
    res: {"health": 200}
```

# Goのインストールと設定

### 1. インストール

[Golang](https://golang.org/dl/)からダウンロードして展開してください。

以下のコマンドでインストールされたか確認してください。

```bash
go version

#go version go1.12.7 darwin/amd64
```

### 2. 環境設定

環境変数として、以下の変数を設定してください。
GOPATHは、goのプロジェクトを構築する環境です。また、GO111MODULEは、Go1.11から導入され始めたGoの新しいバージョン管理ツールGo Modules(vgo)を使用を管理するためのものです。適宜、個人の環境に合うように設定してください。

```bash
export GOPATH=$(go env GOPATH)
export PATH=$PATH:$GOPATH/bin
export GOPATH=/Users/xxxxxxxx/Documents/workspaces/golang
export GO111MODULE=on
```

この環境変数を毎回設定するのが面倒なので、.bash_profileに書き込みます。

```bash
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)などを作成します。

```bash
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を使用するので、以下のコマンドでダウンロードします。

```bash
go get -u github.com/gin-gonic/gin
```

まず、mainの処理を実装します。
versionはビルド時に使用します。


```golang:src/main.go
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ポートでサーバーを実行しています。

```golang:src/app/app.go
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)
	}

}
```

```golang:src/app/url_mappings.go
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まとめ](https://qiita.com/tomoyukilabs/items/81698edd5812ff6acb34)が参考になりました。

```golang:src/middleware/options_middleware.go
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()を実行して、エラーがなければ、結果を返します。

```golang:src/controllers/health/health_contoroller.go

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で返答するデータを以下のような構造体としています。

```golang:src/domain/health/create_health.go
package health

type CreateHealthResponse struct {
	Status      int    `json:"status"`
	Description string `json:"description"`
}
```

※エラーに関しては、独自に作成したものを使用しています。git: [example-golang-rest-api/src/utils/errors/](https://github.com/k-washi/example-golang-rest-api/tree/master/src/utils/errors)を参考にしてください。

※SOLIDパターンに関しては、[Hands-On Software Architecture with Golang](https://www.amazon.co.jp/Hands-Software-Architecture-Golang-applications-ebook/dp/B079X2RGKQ)を参考にしました。



```golang:src/services/health_service.go

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
}
```

### 実行

```bash
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を作成します。そして、

```golang
func init() {
	HealthService = &healthService{}
} 
```

としていたHealthServiceにテストのために新しく作成したhealthServiceMockを挿入します。
実際にテストを行うTestGetHealthStatusOKというテスト関数の最初でhealthService.GetHealth()が返すデータを作成し、MockのGetHealth関数が返すgetHealthFunction関数の返り値として設定しています。この結果、healthService.GetHealthは、作成した返り値を返すように処理されます。

そして、/health/へのリクエストを作成し、GetHealthStatusOKを実行します。
テストは、実行後のresponseをtestifyの機能を用いて評価しています。

```golang:src/contorollers/health/health_contoroller_test.go
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)

}
```

### テストの実行

以下のコマンドでテストを実行する。

```bash
go test ./...
```

# Docker image & コンテナ化

本章では、Makefile、Dockerfile、Docker imageの作成を行い、コンテナ化して実際にヘルスチェックを実行します。

まず、以下のようなMakefileを作成します。
```make help```で実行できるコマンドが表示されます。
また、```make build```or```make bin/app```でコンパイルでき、```make devel-deps```でその他開発に必要なライブラリをインストールできます。

```bash:Makefile
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ポートで公開するように設定しています。

```Dockerfile
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化できます。

```bash
docker build -t kwashizaki/example-golang-rest-api:v1.0.0 .
```

そして、以下のコマンドでコンテナ化できます。

```bash
docker run -it -p 8080:8080 --rm --name example-golang-rest-api kwashizaki/example-golang-rest-api:v1.0.0
```

以下のコマンドで実際にJSONが返ってくるか確認できます。

```bash
curl http://localhost:8080/health
#{"status":200,"description":"Health check OK"}
```

お疲れさまです。以上で、簡単ですが、GolangでWebアプリケーションを作成できました。

# 参考文献

+ [Hands-On Software Architecture with Golang](https://www.amazon.co.jp/Hands-Software-Architecture-Golang-applications-ebook/dp/B079X2RGKQ)
+ [Mastering Go](https://www.amazon.co.jp/gp/product/B07WC24RTQ/ref=ppx_yo_dt_b_d_asin_title_o04?ie=UTF8&psc=1)