Remote Developmentを使って爆速でGo&PostgreSQLの環境構築を行う

こんにちは！
Remote Developmentを使ったGoの開発に関してまとめましたので参考になれば幸いです

## github リポジトリ
以下をcloneすることでこの記事に書かれてることが再現できます！

https://github.com/mikaijun/go-postgre-onboarding/

## こんな方にオススメ!

- これからGoを学習したい方
- Goを使ってデータベースとのやりとりを行いたい方
- ローカルマシンを汚したくない方(brewなど)
- Dockerは使いたいけど詳しい設定はよくわからない方
- Githubを使ってGoのコードをリモートにpushしたい方

### この記事で扱わないこと

- VSCodeのインストール方法
- Docker(Docker Desktop)のインストール方法
- Githubのアカウント登録
- SQLの解説や使用したGoのパッケージの解説
- SSHを使用したGitHubへの接続
  - 最速で開発できることをを目的としてますのでビジネスで開発する場合はSSHの使用を検討してください

## Remote Developmentのインストール
[Remote Development](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack)をインストールしてください。
Remote Developmentを1行で解説すると

**開発に必要なDocker(Docker Compose)を提供してくれるスターターセット**

です。詳細は[公式ドキュメント](https://code.visualstudio.com/docs/remote/remote-overview)をご覧ください。


## コンテナを新規作成する
VSCodeの左下の青い部分をクリック
![スクリーンショット 2023-10-08 14.42.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/840696/c21f9ffa-2168-73f8-5fde-53c3bfa3c901.png)

「新しい開発コンテナ」を選択
![スクリーンショット 2023-10-08 14.42.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/840696/ad035853-eeee-710d-a6ee-5a51ed252a41.png)

「Go&PostgreSQL」を選択
![スクリーンショット 2023-10-08 14.43.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/840696/87a6fd21-b8df-f4bb-73e4-b062dcab69ef.png)

「Dev Containerの作成」を選択。コンテナを作成するのに5分 ~ 10分ぐらいかかると思います
![スクリーンショット 2023-10-08 14.43.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/840696/bdbe2946-fc46-8caf-92c0-bd8dddf0eef9.png)


## Goファイルを作成する
コンテナが作成されると新しい新しいVSCodeのウインドが立ち上がると思います(下記画像)。
このVSCodeのウインド(下記画像)はコンテナの中に入ってますのでここでインストールした拡張機能はローカルマシンには反映されないのが特徴です

![スクリーンショット 2023-10-08 14.51.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/840696/52e25c5b-e1bf-587e-132a-5d584d6e478e.png)

Goファイル作成
```zsh
vscode ➜ /workspaces/go-postgres $ touch main.go
```
Hello, world!を表示させるだけのGoファイルを記述
```main.go
package main

import (
	"fmt"
)

func main() {
	fmt.Printf("Hello, world!")
}
```
「Go 初期化」でググると`go mod init`が沢山ヒットしたので実行してみる
```zsh
vscode ➜ /workspaces/go-postgres $ go mod init
go: cannot determine module path for source directory /workspaces/go-postgres (outside GOPATH, module path must be specified)

Example usage:
        'go mod init example.com/m' to initialize a v0 or v1 module
        'go mod init example.com/m/v2' to initialize a v2 module
```

[公式のwiki](https://github.com/golang/go/wiki/Modules#why-does-go-mod-init-give-the-error-cannot-determine-module-path-for-source-directory)によると解説がありましたので日本語に訳した上で引用します
> go mod init引数を指定しないと、VCS メタデータなどのさまざまなヒントに基づいて適切なモジュール パスを推測しようとします。go mod initただし、常に適切なモジュール パスを推測できるとは限りません。
このエラーが発生した場合はgo mod init、ヒューリスティックでは推測できなかったため、モジュール パス ( などgo mod init github.com/you/hello) を自分で指定する必要があります。

モジュールパスって何？と思って調べてみたら[公式ドキュメント](https://go.dev/ref/mod#module-path)に書かれていたので日本語に訳した上で引用します。
>モジュールのパスは、モジュール内のパッケージ パスのプレフィックスです。
モジュール パスでは、モジュールが何を行うか、およびモジュールがどこにあるかを説明する必要があります。通常、モジュール パスは、リポジトリのルート パス、リポジトリ内のディレクトリ (通常は空)、およびメジャー バージョンのサフィックス (メジャー バージョン 2 以降のみ) で構成されます。

他の言語やフレームワークで`import './xxx'`みたいなことをする場合、相対パスではなく自分で決めたモジュール名からimportする必要があるっぽいですね。今回はメッセージ通り`example.com/m`を採用します(ここは自由に決めて大丈夫です)
```zsh
vscode ➜ /workspaces/go-postgres $ go mod init example.com/m
```
以下のメッセージが表示されると思います
```zsh
go: creating new go.mod: module example.com/m
go: to add module requirements and sums:
        go mod tidy
```

以下のようなgo.modファイルが作成されました。
```go.mod
module example.com/m

go 1.21.2
```
[公式ドキュメント](https://go.dev/doc/modules/gomod-ref)の説明を日本語に訳した上で引用します。
> 各Goモジュールはgo.modファイルで定義され、他のモジュールやGoのバージョンへの依存関係など、モジュールのプロパティを記述します。

この状態でgoファイルを起動させてみましょう
```zsh
vscode ➜ /workspaces/go-postgres $ go run main.go
```
```zsh
Hello, world!
```

## リポジトリにファイルをpushする
コンテナの中でgitの設定を行います。
冒頭で説明した通り、sshの設定を行わないので今回はHTTPS接続でpushします。
今回はチュートリアルなので`.env`もpushしていますが、実際は.gitignoreで除外してください

```zsh
vscode ➜ /workspaces/go-postgres $ git init
vscode ➜ /workspaces/go-postgres (master) $ git add .
vscode ➜ /workspaces/go-postgres (master) $ git commit -m "first commit"
vscode ➜ /workspaces/go-postgres (master) $ git remote add origin https://github.com/${ユーザー名}/${リポジトリ名}.git
vscode ➜ /workspaces/go-postgres (master) $ git push origin HEAD
```

## PostgreSQLを導入する
マイグレーションとAPIのエンドポイントの準備としてmain.goを以下のようにします。

```main.go
package main

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

	_ "github.com/lib/pq"
)

func main() {
	host := os.Getenv("POSTGRES_HOSTNAME")
	database := os.Getenv("POSTGRES_DB")
	user := os.Getenv("POSTGRES_USER")
	password := os.Getenv("POSTGRES_PASSWORD")

	_, err := sql.Open("postgres", fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable", host, user, password, database))
	if err != nil {
		panic(err)
	}

    // TODO: マイグレーションとAPIのエンドポイント作成


	fmt.Println("Server is running on port 8080...")
	http.ListenAndServe(":8080", nil)
}
```

再びgoファイル実行・・・と思ったらエラー出ましたね。

```zsh
vscode ➜ /workspaces/go-postgres (master) $ go run main.go
main.go:9:2: no required module provides package github.com/lib/pq; to add it:
        go get github.com/lib/pq
```
go getは非推奨らしいのでgo mod tidyでインストール。(詳細は[こちら](https://zenn.dev/tmk616/articles/383fc3fbb0ec4b))
```zsh
vscode ➜ /workspaces/go-postgres (master) $ go mod tidy
```
```zsh
go: finding module for package github.com/lib/pq
go: downloading github.com/lib/pq v1.10.9
go: found github.com/lib/pq in github.com/lib/pq v1.10.9
```
再びgoファイル実行
```zsh
vscode ➜ /workspaces/go-postgres (master) $ go run main.go
```
サーバーが立ち上がることが確認できればOKです！
```zsh
Server is running on port 8080...
```
go.modファイルとgo.sumファイルが以下のようになるはずです
```go.mod
module example.com/m

go 1.21.2

require github.com/lib/pq v1.10.9
```
```go.sum
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
```

## マイグレーションファイル作成
migrationsディレクトリとusers.goファイルを作成
```zsh
vscode ➜ /workspaces/go-postgres (master) $ mkdir migrations
```
```zsh
vscode ➜ /workspaces/go-postgres (master) $ cd migrations
```
```zsh
vscode ➜ /workspaces/go-postgres/migrations (master) $ touch users.go
```

```migrations/users.go
package migrations

import (
	"database/sql"

	_ "github.com/lib/pq"
)

func UsersMigrate(db *sql.DB) {
	// users テーブルが存在するかどうかを調べる
	var count int
	err := db.QueryRow("SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'users'").Scan(&count)
	if err != nil {
		panic(err)
	}

	// users テーブルが存在する場合は処理終了
	if count >= 1 {
		return
	}

	if err != nil {
		panic(err)
	}

	_, err = db.Exec("CREATE TABLE users (user_id serial PRIMARY KEY, user_name VARCHAR(50));")

	if err != nil {
		panic(err)
	}

	_, err = db.Exec(`
		INSERT INTO
			users (user_name)
		VALUES
			('Smith'),
			('ohnson'),
			('Brown')
	`)

	if err != nil {
		panic(err)
	}
}
```


## APIファイルの作成
apiディレクトリとusers.goファイルを作成
```zsh
vscode ➜ /workspaces/go-postgres (master) $ mkdir api
```
```zsh
vscode ➜ /workspaces/go-postgres (master) $ cd api
```
```zsh
vscode ➜ /workspaces/go-postgres/migrations (master) $ touch users.go
```

```api/users.go
package api

import (
	"database/sql"
	"encoding/json"
	"io"
	"log"
	"net/http"
)

type User struct {
	Id   int    `db:"user_id"`
	Name string `db:"user_name"`
}

func Users(db *sql.DB) {
	http.HandleFunc("/users/get", func(w http.ResponseWriter, r *http.Request) {
		rows, err := db.Query("SELECT * FROM users")
		if err != nil {
			log.Fatal(err)
		}
		defer rows.Close()

		users := []User{}
		for rows.Next() {
			user := User{}
			err := rows.Scan(&user.Id, &user.Name)
			if err != nil {
				log.Fatal(err)
			}
			users = append(users, user)
		}

		json.NewEncoder(w).Encode(users)
	})

	http.HandleFunc("/users/create", func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(r.Body)
		if err != nil {
			log.Fatal(err)
		}

		user := User{}
		err = json.Unmarshal(body, &user)
		if err != nil {
			log.Fatal(err)
		}

		stmt, err := db.Prepare("INSERT INTO users (user_name) VALUES ($1) RETURNING user_id, user_name;")
		if err != nil {
			log.Fatal(err)
		}
		defer stmt.Close()

		err = stmt.QueryRow(user.Name).Scan(&user.Id, &user.Name)
		if err != nil {
			log.Fatal(err)
		}

		json.NewEncoder(w).Encode(user)
	})
}
```


## main.goの修正
```main.go
package main

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

    //　追加
	"example.com/m/api"
    //　追加
	"example.com/m/migrations"
	_ "github.com/lib/pq"
)

func main() {
	host := os.Getenv("POSTGRES_HOSTNAME")
	database := os.Getenv("POSTGRES_DB")
	user := os.Getenv("POSTGRES_USER")
	password := os.Getenv("POSTGRES_PASSWORD")

	db, err := sql.Open("postgres", fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable", host, user, password, database))
	if err != nil {
		panic(err)
	}

    //　追加
	migrations.UsersMigrate(db)
    //　追加
	api.Users(db)

	fmt.Println("Server is running on port 8080...")
	http.ListenAndServe(":8080", nil)
}

```

## 動作確認
(実行していない場合は)goファイルを実行
```zsh
vscode ➜ /workspaces/go-postgres (master) $ go run main.go
```

以降別タブのターミナルで実行

getの確認
```zsh
vscode ➜ /workspaces/go-postgres/api (master) $ curl http://localhost:8080/users/get
```
```zsh
[{"Id":1,"Name":"Smith"},{"Id":2,"Name":"Johnson"},{"Id":3,"Name":"Brown"}]
```

postの確認
```zsh
vscode ➜ /workspaces/go-postgres/api (master) $ curl -X POST   -d '{ "name": "Bob" }' http://localhost:8080/users/create
```
```zsh
{"Id":4,"Name":"Bob"}
```

再度getの確認
```zsh
vscode ➜ /workspaces/go-postgres/api (master) $ curl http://localhost:8080/users/get
```
```zsh
[{"Id":1,"Name":"Smith"},{"Id":2,"Name":"Johnson"},{"Id":3,"Name":"Brown"},{"Id":4,"Name":"Bob"}]
```


## 終わりに
これでGoでAPI開発ができるようになると思います。
Docker周りの環境構築のヒントになれば幸いです。
ご覧頂きありがとうございます！