VSCode Remote DevelopmentでGolangの開発環境を爆速で構築する

この記事は[株式会社クロノス](https://www.kronos.jp/)の「~2020年春~勝手にやりますアドベントカレンダー」の１０日目の記事です。

「~2020年春~勝手にやりますアドベントカレンダー」インデックスは[こちら](https://qiita.com/beeeyan/items/342d51fa301bcf81e9c0)（随時更新）

[前回の記事](https://qiita.com/maroKanatani/items/7ca85205430cead76817)で作成したカレンダーは[こちら](https://ad-calendar-992c5.firebaseapp.com/view-calendar/RJjcAaxRFnD5IPDw2bDq)

# はじめに
開発環境、どうやって構築していますか？
ローカルに色々インストールしちゃう人も多いと思いますが、言語のバージョン違いや環境変数などで、**「自分の環境では動くんやけど、他人の環境では動けへん。なんでや・・・」**みたいな光景を開発者であれば一度は目にしてきたのではないでしょうか。
解決方法はいくつかありますが、今回はVSCodeのRemote DevelopmentとDockerを使った方法を紹介したいと思います。
**DockerはもはやWebエンジニアにとっての必須教養**と言っても過言ではないと思いますので、これを期にDockerでの環境構築を始めてみても良いのではないでしょうか。
今回は僕の中で話題のGolangの環境を構築してみます。**（GoもDockerもマジで社内で流行ってほしい）**

最終的に出来上がったものは[こちら](https://github.com/Kanatani28/remote-go-sample)

# Remote Developmentとは
VSCodeの拡張機能で、その名の通りリモートでの開発をサポートする拡張機能です。
Remote SSH、Remote Containers、Remote WSLの３つの拡張機能がセットになっています。
今回はRemote Containersを使って、Dockerで立てたコンテナの中で開発していきます。

# 動作環境
- Mac
- VSCode
- Remote Development（VSCode拡張機能）
- Docker for Mac  
(Win HomeのDocker ToolBoxだとたぶん上手くいかないので注意。[ここ](https://code.visualstudio.com/docs/remote/containers)にもDocker Desktop for Windows/Macって書かれてる)

# ちょっと試してみる

Microsoftさんが[いい感じのプロジェクト](https://github.com/microsoft/vscode-remote-try-go)を用意してくれているのでcloneします。（[他の対応言語](https://github.com/microsoft?utf8=%E2%9C%93&q=vscode-remote-try&type=&language=)もいくつかあって便利そう）

```
git clone https://github.com/microsoft/vscode-remote-try-go.git
```

cloneできたらVSCodeで開きましょう

```
code vscode-remote-try-go
```

開くと以下のようなダイアログが出てくるので「Reopen in Container」をクリック
![スクリーンショット 2020-03-01 14.19.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239351/ffcb8504-6f30-1dfb-3346-0fa34b364631.png)

最初だけDockerfileからイメージの作成と、コンテナの作成を行うので結構時間かかります。

で、VSCodeのターミナルが以下のようになったらオッケーです。

![スクリーンショット 2020-03-01 14.28.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239351/fb32eb2f-1c74-3aff-a570-021400e3f024.png)

以上でGolangで開発できる環境が整いました。
開発用のコンテナが作成され、その中で開発を進めることができます。
めっちゃ簡単ですよね。

# 動かしてみる

F5かデバッグのメニューから三角のアイコンをクリックすることででアプリケーションの起動、デバッグができます。（デフォルトだと9000番ポートで起動します）
アプリの標準出力等はデバッグコンソールに出てきます。
補完機能も快適です。

![terminal.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239351/9afa2685-f5c5-c7ab-20f6-aa551aeb4f97.gif)

リポジトリ一つ落としてくるだけで環境作れるなんて素敵じゃないですか？

この辺りは[公式](https://github.com/microsoft/vscode-remote-try-go)のREADME読めば大体書かれています。

# 実運用で使えそうなことあれこれ

## リポジトリの作成
ここはGitの使い方のような感じになります。
先程の手順でリポジトリをcloneする際、そのままcloneしてきましたが
実運用で使用する場合、以下のように別名を指定してcloneするのが良いでしょう。

```
git clone https://github.com/microsoft/vscode-remote-try-go.git [別名]
```

で、このままだとリモートがMicrosoftのリポジトリのままなので

```
git remote -v
// 結果
origin	https://github.com/microsoft/vscode-remote-try-go.git (fetch)
origin	https://github.com/microsoft/vscode-remote-try-go.git (push)
```

リモートに自分で作成したリポジトリのURLを設定し、pushしましょう

```
git remote set-url origin [自分で作成したリポジトリのURL]
git push
```

## 開発用コンテナの作成をDocker Composeで行う
Docker Composeで環境を立ち上げたいことも多いと思います。
今回はMySQLのコンテナを立ち上げ、Golangで作ったアプリケーションからアクセスする想定で設定を修正してみます。

まずはdocker-compose.yml、DB周りのファイルを追加しましょう。

```:ディレクトリ構成（追加修正以外の部分は省略）
.
├── .devcontainer
│   ├── Dockerfile
│   ├── devcontainer.json   // 修正
│   └── docker-compose.yml  // 追加
├── db                      // 追加
│   ├── conf
│   │   └── my.conf        // 追加（MySQL設定）
│   └── init
│       └── 00_init.sql    // 追加（テストデータ）
└── src
```

```json-doc:devcontainer.json（元と差分のあるところのみコメントを付与している）
{
	"name": "Go Sample",
	// dockerFile -> dockerComposeFile
	"dockerComposeFile": [
		"docker-compose.yml"
	],
	// docker-compose.servicesのどのコンテナで作業するのかを設定する
	"service": "app",
	// workspaceを設定する
	"workspaceFolder": "/app",
	"runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],

	// docker-compose.yml内で設定できるので不要
	//"appPort": [9000],

	"settings": { 
		"terminal.integrated.shell.linux": "/bin/bash",
		"go.gopath": "/go",
		"go.inferGopath": true,
		"go.useLanguageServer": true
	},

	"remoteUser": "vscode",

	"extensions": [
		"ms-vscode.go"
	]
}
```

```yml:docker-compose.yml
version: '3'
services:
  db:
    image: mysql
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    ports: 
      - '33306:3306'
    expose: 
      - '3306'
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: sample
      MYSQL_USER: test
      MYSQL_PASSWORD: test
      TZ: "Asia/Tokyo"
    volumes:
      - ../db/conf:/etc/mysql/conf.d
      - ../db/init:/docker-entrypoint-initdb.d

  app:
    build: ./
    working_dir: /app
    ports: 
      - '11323:1323'
    volumes:
      - ../:/app
    expose:
      - '1323'
    command: sleep infinity

```

```conf:my.conf
[mysql]
default-character-set=utf8

[mysqld]
character-set-server=utf8
```

```sql:00_init.sql
USE sample;
SET CHARACTER_SET_CLIENT = utf8;
SET CHARACTER_SET_CONNECTION = utf8;

CREATE TABLE users(
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL
);

INSERT INTO users(name) VALUES ('Alex');
```



ファイルが追加できたら再度コンテナ内で開き直しましょう。

次にコンテナ内でMySQLのドライバを取得します。

```:コンテナ内で実行
go get github.com/go-sql-driver/mysql
```

ドライバが取得できたらMySQLにアクセスしデータを取得するようにserver.goを修正します。

```go:server.go
package main

import (
	"database/sql"
	"fmt"
	"io"
	"net/http"

	_ "github.com/go-sql-driver/mysql"
)

// 修正部分
func handle(w http.ResponseWriter, r *http.Request) {
	// DB接続部分。接続文字列は(MySQLユーザー名:パスワード@tcp(ホスト名:ポート)/DB名)
	db, err := sql.Open("mysql", "test:test@tcp(db:3306)/sample")

	if err != nil {
		panic(err)
	}
	defer db.Close()

	stmt, err := db.Prepare("select * from users where id < ?")
	if err != nil {
		panic(err)
	}
	defer stmt.Close()

	var (
		id   int
		name string
	)
	rows, err := stmt.Query(2)

	for rows.Next() {
		err := rows.Scan(&id, &name)
		if err != nil {
			panic(err)
		}
		fmt.Println(id, name)
	}
	defer rows.Close()

	if err = rows.Err(); err != nil {
		panic(err)
	}
	io.WriteString(w, "Hello, "+name)
}

func main() {
    // docker-composeで設定したポートを設定する
	portNumber := "1323"
	http.HandleFunc("/", handle)
	fmt.Println("Server listening on port ", portNumber)
	http.ListenAndServe(":"+portNumber, nil)
}
```

設定できたらブラウザからlocalhost:11323にアクセスしてみましょう。
Hello, Alexと表示されたら成功です。

また、以下のようなエラーが出たら、goplsをUpdateしてあげると直るかもしれません。

```
The gopls server crashed 5 times in the last 3 minutes. The server will not be restarted.
```

## Go Modulesで依存関係を解決する

ライブラリの管理をGo Modulesで行うようにしてみましょう。
まずはworkspace直下でgo mod initを実行します。（initの後のモジュール名は任意です）

```
go mod init example.com/sample
```

次にgo buildを実行します。

```
go build src/main/server.go
```

実行できたらgo.sumファイルが作成され、
go.modにmysql driverが追加されるはずです。

次にsettings.jsonをGo Modules用に修正します。

```json-doc:settings.json
{
    "go.useLanguageServer": true,
    // true -> false
    "go.inferGopath": false,
    // 追加   
    "go.toolsEnvVars": {
        "GO111MODULE": "on", 
     },
}
```

これでGo Modulesでライブラリを管理できるようになりました。
試しにGolangのWebフレームワーク、Echoをインストールしてみましょう

```
go get -u github.com/labstack/echo/...
```

go.modが更新されます。

```
module example.com/sample

go 1.14

require (
	github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
	github.com/go-sql-driver/mysql v1.5.0
	github.com/labstack/echo v3.3.10+incompatible // indirect
	github.com/labstack/gommon v0.3.0 // indirect
	github.com/mattn/go-colorable v0.1.6 // indirect
	github.com/valyala/fasttemplate v1.1.0 // indirect
	golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 // indirect
	golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect
	golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect
	golang.org/x/text v0.3.2 // indirect
)
```

ついでにserver.goを以下のように修正してみましょう。

```go:server.go
package main

import (
	"database/sql"
	"fmt"
	"net/http"

	_ "github.com/go-sql-driver/mysql"
	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
)

func handle(c echo.Context) error {
	// DB接続部分。接続文字列は(MySQLユーザー名:パスワード@tcp(ホスト名:ポート)/DB名)
	db, err := sql.Open("mysql", "test:test@tcp(db:3306)/sample")

	if err != nil {
		return c.String(http.StatusInternalServerError, "Error")
	}
	defer db.Close()

	stmt, err := db.Prepare("select * from users where id < ?")
	if err != nil {
		return c.String(http.StatusInternalServerError, "Error")
	}
	defer stmt.Close()

	var (
		id   int
		name string
	)
	rows, err := stmt.Query(2)

	for rows.Next() {
		err := rows.Scan(&id, &name)
		if err != nil {
			return c.String(http.StatusInternalServerError, "Error")
		}
		fmt.Println(id, name)
	}
	defer rows.Close()

	if err = rows.Err(); err != nil {
		return c.String(http.StatusInternalServerError, "Error")
	}
	return c.String(http.StatusOK, "Hello, "+name)
}

func main() {
	// Echo instance
	e := echo.New()

	// Middleware
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	// Routes
	e.GET("/", handle)

	// Start server
	e.Logger.Fatal(e.Start(":1323"))
}

```

FWも導入して実運用っぽい雰囲気が出てきたのではないでしょうか？


# 結局何が嬉しいのか

- GOPATHなど、Golang動かすのに必要な設定とかもろもろ面倒くさいのが楽になる
- ローカル環境汚さずに動かせるのがめっちゃいい
- コマンド一発で環境ができるので楽ちん
- 全く同じ環境を他人に共有できる（環境構築の手順を間違えることほとんどがなくなる）

などなど。
settings.jsonやdevcontainer.jsonなどの設定項目がたくさんあるので最初は大変かもしれませんが、上記のようなメリットもかなり大きいと思います。


# 参考
[VS Code Remote Development（公式）](https://code.visualstudio.com/docs/remote/remote-overview)
[Go言語の依存モジュール管理ツール Modules の使い方](https://blog.mmmcorp.co.jp/blog/2019/10/10/go-mod/)
[Go の echo ってWebサーバーでサクッと REST しちゃう](https://qiita.com/ezaki/items/62e806ae42828bb3567a)
[VS Code Remote Development で Docker 開発環境を利用する](https://qiita.com/yito/items/1edd82414dfc2ab2a7bc)