dockerでgoの開発環境を構築する

# dockerでgoの開発環境を構築する

本記事は以下の記事からのリンクとして作成されました。
以下の記事も是非ご覧ください！！

- [第三回開発合宿](https://qiita.com/fumiya-uehara/private/d5e10380299a22995c64)

## 本記事について

本記事は、Go言語の開発環境をdockerで構築するための方法を記した記事です。
ホストOSからコンテナ上で動作する実行ファイルをデバッグできることが目標です。

完成したリポジトリは以下です。
https://github.com/fumiya-uehara/go_dev

## 環境

- macOS Catalina ver 10.15
- go 1.13
- Docker version 18.09.2, build 6247962
- docker-compose version 1.24.0, build 0aa5906

## 事前準備

### Goのインストール

ホストOSにgoがインストールされていることを確認してください。

```shell

❯ go version
go version go1.13 darwin/amd64
```

もしもインストールされていない場合は `Homebrew` などのパッケージマネージャを利用してインストールしてください。

```shell

❯ brew install go
```

### GOPATHを設定する

Go言語ではGOPATHという環境変数を設定する必要があります。
インストールされたライブラリをソースコード内でimportする際に利用されたり、 `go get` で外部ライブラリをインストールした際に保存先として指定されたりと様々なケースで使われます。

GOPATHを設定していないとimportで指定したライブラリを読み込めないので注意が必要です。
また、Goの実行環境がDocker上にある場合、Docker上ではインストールされているライブラリがローカルには存在しないため、IDEによる定義ジャンプができないなど不便な面もあるのでDocker上でインストールされたライブラリに関してはローカルにもインストールするなどの考慮が必要です。（なんかいい方法ないのだろうか・・・）

以下が設定方法です。（筆者はshellにzshを利用しているのでzshの設定方法です）

```shell

❯ cat << EOL >> ~/.zprofile
then cmdand> export GOPATH="$HOME/work/go/dev"
then cmdand> export PATH="$HOME/work/go/dev/bin:$PATH"
then cmdand> EOL
❯ echo $GOPATH
/Users/sumio/work/go/dev
```

GOPATHに指定するディレクトリは自由ですので悩まずに適当なところに置けばよいです。
もちろん後から変更もできますが、変更するとこれまでにインストールしたライブラリへのパスが通らないのでimportで呼び出せないことを頭の片隅に入れといてください。（環境がdocker上にある場合はあまり関係ないですが）

### realizeのインストール

今回realizeというタスクランナーツールを利用し、ホットリロードを行います。
このツールのみローカル環境へのインストールが必要となるため事前にインストールしておきます。

```shell

❯ go get -u github.com/oxequa/realize
❯ which realize
/Users/sumio/work/go/dev/bin/realize
```

インストール後、realizeへのパスが通っていればOKです。

## 構築手順

**勉強不足で誤った理解で記述している箇所があるかもしれません。その際はご指摘ください。**

### GoModulesの導入

GoModulesはバージョン管理システムです。
Projectのディレクトリを作成する際にも少し触れましたが、これまでGo言語でProjectを作成するとなると `${GOPATH}/src` 配下に置くことが暗黙的なルールとしてあり、このルールに従って開発を進めるとプロジェクト毎にライブラリのバージョンを変えることができないなどの問題がありました。

これまではこの問題に対して各Projectディレクトリ配下にvendorというディレクトリを切り、そこにProjectで利用する固有バージョンのライブラリを置くことで対処してきました。（GoはvendorというディレクトリがProjectディレクトリ配下にある時優先的にそこにあるライブラリを読み込みます。）

しかし、GoModulesの導入によってvendorは廃止となり、容易にライブラリを管理することができるようになりました。仕組みとしては、`go.mod`というファイルでProjectで利用するライブラリのバージョンを管理し、実際のライブラリは`${GOPATH}/pkg/mod`配下に置かれます。
もしも、必要としているライブラリがインストールされていなければ`${GOPATH}/pkg/mod`配下に追加しますし、前述したPATHにライブラリが存在しているのであれば、それを利用するといった感じで動作します。（すみません。検証していないので確証はないですがドキュメントなど読むとそうなのかなって思ってます。）

前置きが非常に長くなってしまいました。
以下を実行し、`module-aware mode`で動作させ、`go.mod`ファイルを作りましょう。

※`go mod init github.com/fumiya-uehara/go_dev` の `github.com/fumiya-uehara/go_dev` は対象プロジェクトのリモートリポジトリを指定することをお勧めします。

```shell

❯ cd ${PROJECT_ROO}
❯ export GO111MODULE=on
❯ go mod init github.com/fumiya-uehara/go_dev 
```

`export GO111MODULE=on` にて `module-aware mode` に変更しています。デフォルトは `auto` です。
Go1.13からは常に `module-aware mode` で動作すると言われていましたが、最終的には `auto` で落ち着いたようです。
ただ、 `auto` の場合でも `go.mod` ファイルがカレントディレクトリもしくは親ディレクトリに存在する場合は、 `on` になります。
さらに詳しく知りたい方は以下のリリースノートをチェックしてください。

https://golang.org/cmd/go/#hdr-Module_support

### realizeの設定

タスクランナーツールとしてrealizeを導入しています。
開発時にコードを変更する度に `go run` や `go build` を行うのはとても不便です。

そのためにコードの変更を検知し自動でbuildしてくれるホットリロードの仕組みが必要となります。
本プロジェクトではホットリロードを実現するためにrealizeを導入しています。

```shell
cd ${PROJECT_ROOT}
realize start
```

上記コマンド実行後、`.realize.yaml`が作成されればOKです。
realizeがフォアグラウンドで動作し続けるので `Ctrl + c` を押下してKillしましょう。

```yaml
settings:
  legacy:
    force: false
    interval: 0s
schema:
  - name: go_dev
    path: .
    commands:
      install:
        status: true
        method: go build -o /app/go_dev
      run:
        status: true
        method: /go/bin/dlv
    args:
      - exec
      - /app/go_dev
      - --headless=true
      - --listen=:3000
      - --api-version=2
      - --accept-multiclient
    watcher:
      extensions:
        - go
      paths:
        - /
      ignore:
        paths:
          - .git
          - .realize
          - vendor
```

大事な部分のみピックアップして説明します。

```yaml
commands:
  install:
    status: true
    method: go build -o /app/go_dev
  run:
    status: true
    method: /go/bin/dlv
args:
  - exec
  - /app/go_dev
  - --headless=true
  - --listen=:3000
  - --api-version=2
  - --accept-multiclient
```

変更を検知すると対象のファイルをbuildし、最新の状態になります。

ただ、リモートデバッグを行っている最中にコードを変更するとbuildは走りますが、一度コンテナとのコネクションを切断し、新たに繋ぎ直さないと新しいbinaryファイルを読み込みません。
当然の動作のように見えますし、そこまで手間ではないですが、良い方法があれば知りたいものです。

### Dockerfile(Go実行コンテナ)

`multi-stage build`を採用していますが、`# Final Stage`は利用されていません。
なので、実際にこちらを見ながら構築する方は消しちゃって問題ありません。

```dockerfile
# Variable
ARG GO_VERSION=1.13
ARG ALPINE_VERSION=3.10

# Build Stage
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-stage
ENV CGO_ENABLED 0

WORKDIR /go/go_dev
COPY . .
RUN set -ex && \
    apk update && \
    apk add --no-cache git && \
    go get gopkg.in/urfave/cli.v2@master && \
    go get github.com/oxequa/realize && \
    go get github.com/go-delve/delve/cmd/dlv && \
    go build -o /go/bin/dlv github.com/go-delve/delve/cmd/dlv && \
    go build -o /app/go_dev

# Final Stage
FROM alpine:${ALPINE_VERSION}
WORKDIR /app
COPY --from=build-stage /app/go_dev .
EXPOSE 9000
ENTRYPOINT ["/app/go_dev"]
```

一つだけ説明します。

```shell
go get gopkg.in/urfave/cli.v2@master && \
go get github.com/oxequa/realize && \
```

realizeのインストールに失敗する問題があり、対策で `gopkg.in/urfave/cli.v2@master` を前段でインストールしています。

realizeの依存関係の問題のようです。以下のissueです。
https://github.com/oxequa/realize/issues/253

### Dockerfile(mysql実行コンテナ)

以下を実行し、ディレクトリとファイルを作成します。
PATHについては適宜変更してください。

```shell
$ cd ~/work/go/go_dev && \
mkdir -p ./docker/db && \
cd ./docker/db && touch {Dockerfile,my.cnf}
```

FilePath: `${PROJECT_ROOT}/docker/db/Dockerfile`

```dockerfile
FROM mysql:8.0.17
COPY my.cnf /etc/my.cnf
```

FilePath: `${PROJECT_ROOT}/docker/db/my.cnf`

```config
[mysqld]
port=3306
socket=/tmp/mysql.sock
key_buffer_size=16M
max_allowed_packet=8M
default_authentication_plugin=mysql_native_password

[mysqld_safe]
pid-file=/var/run/mysqld/mysqld.pid
```

ログの設定を行いたい場合はご自由にmy.cnfへ追記してください。
コンテナ内のログ出力先とホストOSの任意のディレクトリをマッピングするとコンテナに入らずともログを確認できると思うので、docker-composeを書くときに意識するといいかもしれません。

### docker-compose

```yaml
version: '3.7'
services:
  gin:
    build:
      context: .
      target: build-stage
    depends_on:
      - db
    volumes:
      - ./:/go/go_dev
    command: realize start --run
    ports:
      - "19001:9000"
      - "3000:3000"
    security_opt:
      - seccomp:unconfined
    cap_add:
      - SYS_PTRACE
  db:
    build: ./docker/db
    ports:
      - "13306:3306"
    volumes:
      - ./docker/db/mysql:/docker-entrypoint-initdb.d
    environment:
      MYSQL_ROOT_PASSWORD: "example"
      MYSQL_DATABASE: "test"
```

全てを説明していては時間が足りないので、大事なところだけ抜粋します。

```
build:
  context: .
  target: build-stage
```

Dockerfile内ではmulti-stage build構成を採用していますが、ローカルの開発環境ではFinal-Stageの利用はありません。
そのため、targetにはbuild-stageが指定されています。

```
command: realize start --run
```

上記コマンドを実行することで変更を自動検知しbuildを行い、buildされたbinaryファイルを対象にデバッガを起動します。

```
security_opt:
  - seccomp:unconfined
cap_add:
  - SYS_PTRACE
```

Docker上で動作するプロセスをホストOSにアタッチするための設定です。
リモートデバッグを行うのに利用します。

### main.go

```go
package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	r := gin.Default()

	r.GET("/structJSON_test", func(context *gin.Context) {
		var msg struct {
			Name    string `json:"user"`
			Message string
			Number  int
		}
		msg.Name = "foo"
		msg.Message = "これは構造体をJSONで返すためのテストです。"
		msg.Number = 1111
		context.JSON(http.StatusOK, msg)
	})
	_ = r.Run(":9000")
}
```

ginを利用しており、エンドポイントとして`/structJSON_test`を用意しました。
これは構造体をJSON形式で返すAPIです。
用途としてはテスト用です。

### 動作確認

以下を実行します。

```shell

❯ cd ${PROJECT_ROOT}
❯ docker-compose build
❯ docker-compose up -d
```

もしも、起動に失敗している場合は最初に貼ったリポジトリと見比べて誤りがないか確認してください。
それでも解決することが難しい場合はコメントください。（筆者がこれまで記述した手順に誤りがある可能性も十分にあるので・・・）

問題なく起動に成功したらデバッグの設定を行います。
筆者はIDEにGoLandを利用していますのでそちらの設定を紹介します。

以下の Edit Configurations... をクリックします。
![スクリーンショット 2019-11-03 14.09.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329786/25c7594f-cfaa-65c8-e1b5-27cc7d3880fc.png)

左上の『+』マークをクリックし、「Go Remote」をクリックします。
![スクリーンショット 2019-11-03 14.10.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329786/15083389-50d0-b457-21a1-e2ac56acf947.png)

Portを「3000」に設定し、右下のAPPLYをクリックします。
![スクリーンショット 2019-11-03 14.11.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329786/f87be391-c4bf-47a6-c3a6-7f57db83f0d9.png)

虫のマークをクリックします。
![スクリーンショット 2019-11-03 14.12.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329786/32d56084-b963-176b-4a18-29c74fec025a.png)

下図のように緑の点が出てくれば接続できています。
![スクリーンショット 2019-11-03 14.12.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329786/be2a58a3-1ba5-11fd-dbc0-178e94800d6a.png)

ブレークポイントをおきましょう。
![スクリーンショット 2019-11-03 14.13.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329786/3cffd62f-514d-52e3-32e0-54ed2ad2a669.png)

以下のコマンドを実行してください

```shell
❯ curl localhost:19001/structJSON_test
```

すると、以下のように止まると思います。
![スクリーンショット 2019-11-03 14.14.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329786/8e31834a-7836-34c2-bbc0-fbfbb333fd23.png)


最後に以下のような結果が返ってくれば完了です。

```json
{"user":"foo","Message":"これは構造体をJSONで返すためのテストです。","Number":1111}
```

## 最後に

拙い文章で読み辛い箇所がたくさんあったと思いますが、ここまで読んでいただきありがとうございます。

Goの環境を構築する上で大変だったことは、`$GOPATH`や`GoModules`などの独自概念を理解することでした。色々な記事を読み、実際に動かして検証してやっと今の理解まで至りました。（まだまだ初歩ですが）
こればっかやってた2ヶ月でコードはほとんど書いてなかったりします。笑
やっと「書ける！」って思っていたのですが、次はディレクトリ構成で悩んでおり、コードを書ける日はまだ遠そうです。

## 参考

**realize & delve**

- https://qiita.com/keitakn/items/f46347f871083356149b

**multi stage build**

- https://qiita.com/minamijoyo/items/711704e85b45ff5d6405
- https://medium.com/travis-on-docker/multi-stage-docker-builds-for-creating-tiny-go-images-e0e1867efe5a

**GoModules**

- https://qiita.com/propella/items/e49bccc88f3cc2407745
- https://syfm.hatenablog.com/entry/2019/08/10/170730
- https://budougumi0617.github.io/2019/02/15/go-modules-on-go112/