👨‍💻

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

2023/10/09に公開

こんにちは!
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をインストールしてください。
Remote Developmentを1行で解説すると

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

です。詳細は公式ドキュメントをご覧ください。

コンテナを新規作成する

VSCodeの左下の青い部分をクリック
スクリーンショット 2023-10-08 14.42.31.png

「新しい開発コンテナ」を選択
スクリーンショット 2023-10-08 14.42.48.png

「Go&PostgreSQL」を選択
スクリーンショット 2023-10-08 14.43.12.png

「Dev Containerの作成」を選択。コンテナを作成するのに5分 ~ 10分ぐらいかかると思います
スクリーンショット 2023-10-08 14.43.21.png

Goファイルを作成する

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

スクリーンショット 2023-10-08 14.51.15.png

Goファイル作成

vscode ➜ /workspaces/go-postgres $ touch main.go

Hello, world!を表示させるだけのGoファイルを記述

package main

import (
	"fmt"
)

func main() {
	fmt.Printf("Hello, world!")
}

「Go 初期化」でググるとgo mod initが沢山ヒットしたので実行してみる

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によると解説がありましたので日本語に訳した上で引用します

go mod init引数を指定しないと、VCS メタデータなどのさまざまなヒントに基づいて適切なモジュール パスを推測しようとします。go mod initただし、常に適切なモジュール パスを推測できるとは限りません。
このエラーが発生した場合はgo mod init、ヒューリスティックでは推測できなかったため、モジュール パス ( などgo mod init github.com/you/hello) を自分で指定する必要があります。

モジュールパスって何?と思って調べてみたら公式ドキュメントに書かれていたので日本語に訳した上で引用します。

モジュールのパスは、モジュール内のパッケージ パスのプレフィックスです。
モジュール パスでは、モジュールが何を行うか、およびモジュールがどこにあるかを説明する必要があります。通常、モジュール パスは、リポジトリのルート パス、リポジトリ内のディレクトリ (通常は空)、およびメジャー バージョンのサフィックス (メジャー バージョン 2 以降のみ) で構成されます。

他の言語やフレームワークでimport './xxx'みたいなことをする場合、相対パスではなく自分で決めたモジュール名からimportする必要があるっぽいですね。今回はメッセージ通りexample.com/mを採用します(ここは自由に決めて大丈夫です)

vscode ➜ /workspaces/go-postgres $ go mod init example.com/m

以下のメッセージが表示されると思います

go: creating new go.mod: module example.com/m
go: to add module requirements and sums:
        go mod tidy

以下のようなgo.modファイルが作成されました。

module example.com/m

go 1.21.2

公式ドキュメントの説明を日本語に訳した上で引用します。

各Goモジュールはgo.modファイルで定義され、他のモジュールやGoのバージョンへの依存関係など、モジュールのプロパティを記述します。

この状態でgoファイルを起動させてみましょう

vscode ➜ /workspaces/go-postgres $ go run main.go
Hello, world!

リポジトリにファイルをpushする

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

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を以下のようにします。

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ファイル実行・・・と思ったらエラー出ましたね。

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でインストール。(詳細はこちら)

vscode ➜ /workspaces/go-postgres (master) $ go mod tidy
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ファイル実行

vscode ➜ /workspaces/go-postgres (master) $ go run main.go

サーバーが立ち上がることが確認できればOKです!

Server is running on port 8080...

go.modファイルとgo.sumファイルが以下のようになるはずです

module example.com/m

go 1.21.2

require github.com/lib/pq v1.10.9
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ファイルを作成

vscode ➜ /workspaces/go-postgres (master) $ mkdir migrations
vscode ➜ /workspaces/go-postgres (master) $ cd migrations
vscode ➜ /workspaces/go-postgres/migrations (master) $ touch 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ファイルを作成

vscode ➜ /workspaces/go-postgres (master) $ mkdir api
vscode ➜ /workspaces/go-postgres (master) $ cd api
vscode ➜ /workspaces/go-postgres/migrations (master) $ touch 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の修正

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ファイルを実行

vscode ➜ /workspaces/go-postgres (master) $ go run main.go

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

getの確認

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

postの確認

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

再度getの確認

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

終わりに

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

Discussion