Dokcer ComposeでGoとPostgreSQLの開発環境構築

# 構成
## Go アプリ
<dl>
<dt>アプリ名／コンテナ名</dt>
	<dd>app／app</dd>

<dt>機能</dt>
	<dd>IDを指定してテーブルを検索・結果表示</dd>
<dt>フォルダ共有</dt>
	<dd>コンテナ中の go ファイルをホストOS から編集できるようにしたいので、<br>
             ホストOS の go ファイルがあるフォルダを golang コンテナにマウント</dd>
</dl>

## PostgreSQL
<dl>
<dt>コンテナ名</dt>
	<dd>postgres</dd>
<dt>ユーザ名／パスワード</dt>
	<dd>app_user／password</dd>
<dt>データベース名／テーブル名</dt>
	<dd>app_db／TEST_USER</dd>
<dt>テストデータ</dt>
	<dd>コンテナ起動時にテーブル作成とデータ挿入を行う</dd>
</dl>

## ファイル配置
``$GOPATH/src/app`` 中に、docker と go ソースコードを次のような形で配置

```:$GOPATH/src/app
│
├─docker-compose.yml
├─docker
│  ├─golang
│  │   └─Dockerfile
│  └─postgres
│      ├─Dockerfile
│      └─init
│          ├─1_create_table.sql
│          └─2_insert_testdata.sql
└─go
    └─main.go
```
# Docker 設定
## Docker Compose
「今日から始めるDocker【docker-composeを使ってGo & Mysqlをよしなに起動しよう編】」[^1] を参考にしました。

```docker-compose.yml
# docker-compose のバージョン
version: '3'

# 各コンテナの情報
services:

  # postgres サービス
  postgres:
    # コンテナの名前
    container_name: postgres
    # Dockerfile のディレクトリパス
    build:
      context: .
      dockerfile: ./docker/postgres/Dockerfile
    # postgres 設定
    environment:
      - POSTGRES_USER=app_user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=app_db

  # golang アプリケーション
  app:
    container_name: app
    # postgres をビルドした後に app をビルド
    depends_on:
      - postgres
    # Dockerfile を指定
    build:
      context: .
      dockerfile: ./docker/golang/Dockerfile
    # GOPATHを指定
    environment:
      - GOPATH=/go
    # フォルダ共有（ホストOS:コンテナ）
    volumes:
      - ./go:/go/src/app/go/
    # docker-compose run 実行時に実行されるコマンド
    command: go run main.go
```

## Dockerfile
### app
```:Dockerfile
FROM golang:1.9

# コンテナ作業ディレクトリの変更
WORKDIR /go/src/denki/go
# ホストOSの ./go の中身を作業ディレクトリに追加
ADD ./go .

# パッケージのインストール
RUN go get github.com/lib/pq
```
### postgres
```:Dockerfile
FROM postgres:latest
RUN localedef -i ja_JP -c -f UTF-8 -A /usr/share/locale/locale.alias ja_JP.UTF-8
ENV LANG ja_JP.UTF-

# 初期化用 sql ファイルを、コンテナ内の所定のディレクトリにコピー
COPY ./docker/postgres/init/*.sql /docker-entrypoint-initdb.d/
```
/docker-entrypoint-initdb.d/ 内に sql ファイルを配置すると、それらを自動で実行してくれるらしい[^2]

# その他ファイル
## SQLファイル
テーブル作成

```1_create_table.sql
CREATE TABLE TEST_USER (
	user_id BIGINT PRIMARY KEY,
	user_password VARCHAR(20) NOT NULL
);
```
テストデータ挿入

```2_insert_testdata.sql
DELETE FROM TEST_USER;
INSERT INTO TEST_USER VALUES(1, 'password1');
INSERT INTO TEST_USER VALUES(2, 'password2');
```
## Go
書籍「Goプログラミング実践入門」[^3]を参考にしました

```main.go
package main

import (
	"database/sql"
	"fmt"
	"log"

	// postgres ドライバ
	_ "github.com/lib/pq"
)

// TestUser : テーブルデータ
type TestUser struct {
	UserID   int
	Password string
}

// メイン関数
func main() {

	// Db: データベースに接続するためのハンドラ
	var Db *sql.DB
	// Dbの初期化
	Db, err := sql.Open("postgres", "host=postgres user=app_user password=password dbname=app_db sslmode=disable")
	if err != nil {
		log.Fatal(err)
	}

	// SQL文の構築
	sql := "SELECT user_id, user_password FROM TEST_USER WHERE user_id=$1;"

	// preparedstatement の生成
	pstatement, err := Db.Prepare(sql)
	if err != nil {
		log.Fatal(err)
	}

	// 検索パラメータ（ユーザID）
	queryID := 1
	// 検索結果格納用の TestUser
	var testUser TestUser

	// queryID を埋め込み SQL の実行、検索結果1件の取得
	err = pstatement.QueryRow(queryID).Scan(&testUser.UserID, &testUser.Password)
	if err != nil {
		log.Fatal(err)
	}

	// 検索結果の表示
	fmt.Println(testUser.UserID, testUser.Password)
}
```
# Docker Compose の実行
シェルで以下のコマンドを実行

```
$ docker-compose build  # イメージの作成
$ docker-compose up     # コンテナの作成、起動
```

コンテナの起動順序を指定していないため、postgres より前に app コンテナが先に起動する場合がある。
そうなると DB にテーブルが存在しないので、以下のエラーが吐かれる

```
app     | 2018/12/15 13:50:02 dial tcp 172.29.0.2:5432: getsockopt: connection refused
```

一回 Ctrl+C でコンテナを停止させ、もう一度 ``docker-compose up`` でコンテナを実行すると、app の実行結果（ ``app | 1 password1`` ）が表示される

※起動順序を指定したいときは、docker-compose の entrypoint とスクリプトによって、DBコンテナが起動するまでアプリ起動を待機させる方法がある [^4] 

コンテナの停止・破棄

```
docker-compose down
```

イメージの破棄

```
docker image rm app_app app_postgres
```

# 詰まったところ
## go get に失敗する
イメージのビルドの際、go get において 

```
cannot find package "パッケージ名" ... (from $GOROOT) ... (from $GOPATH)
```

のエラーが出てビルドに失敗することがある。原因として、

* パッケージ自体が存在しないか、パスが間違っている
* golang イメージに git がインストールされていない[^5]

があるらしいので、URIを確認するなり、イメージのバージョンを ``golang:1.9`` に変更する等する

## postgres コンテナに接続できない
main.go の DB 接続設定で

```
var Db *sql.DB
// Dbの初期化
Db, err := sql.Open("postgres", "host=localhost user=...
```

のように ``host=localhost`` とすると 

```
app  | ... dial tcp 127.0.0.1:5432: connect: connection refused
```

データベース接続エラーが吐かれる。``host`` の値は、接続先のコンテナ名にする必要がある[^6] ので、``host=postgres`` に修正して解決


[^1]: https://qiita.com/yu-croco/items/0c0d5083298f7db9e1bb
[^2]: https://qiita.com/hirosemi/items/199a3c753130c12d52f1
[^3]: https://book.impress.co.jp/books/1115101145
[^4]: http://docs.docker.jp/compose/startup-order.html
[^5]: https://stackoverflow.com/questions/44210502/docker-compose-cannot-find-package-golang
[^6]: https://qiita.com/unvavo/items/7b3d1ef9403f6b4b8456