はじめに
こんにちは、kenです。お仕事ではGoをよく書いています。
最近毎日GitHubのトレンドを見るようにしているのですが、先日いつものようにトレンドをチェックしていたら何やら面白そうなリポジトリを見つけたので今日はそれについて紹介しようと思います。
その名も「go-blueprint」です。
go-blueprintとは
Go Blueprint is a CLI tool that allows users to spin up a Go project with the corresponding structure seamlessly. It also gives the option to integrate with one of the more popular Go frameworks (and the list is growing with new features)!
go-blueprintはリポジトリのREADMEにもあるとおり、Goのプロジェクトをシームレスに立ち上げることができるCLIツール です。この後実際に触ってみる様子をお見せしますが、使用したいwebフレームワークやDBMSを選択するだけであっという間にGoのプロジェクトの雛形が生成されます。イメージとしてはReactのCreate React App
が近いのかなと思います。
docker-compose.yml
やMakefile
もその雛形のなかに入っているので最初の面倒な環境構築をスキップできるのが良いところです。
早速使ってみる
では早速go-blueprintを使ってGoプロジェクトを作ってみます。
まずはREADMEに書いている通り、go install
でgo-blueprintをインストールします。
$ go install github.com/melkeydev/go-blueprint@latest
その後
$ go-blueprint create
を実行すると
対話式のCLIが起動しました!
ここではプロジェクト名と使いたいwebフレームワーク、そしてDBMSを選択します。
今回はwebフレームワークとしてchi
、DBMSとしてPostgres
を選択してみました。
すると、指定したプロジェクト名でカレントディレクトリ配下にGoプロジェクトが生成されます。
今回はsample-project
という名前のプロジェクトにしたので最後に
Next steps:
* cd into the newly created project with: `cd sample-project`
という案内が出ていますね。優しい!
素直に従ってディレクトリを移動し、生成されたディレクトリ群を確認してみます。
go-blueprint % cd sample-project
sample-project % tree -a -I '.git'
.
├── .air.toml
├── .env
├── .gitignore
├── Makefile
├── README.md
├── cmd
│ └── api
│ └── main.go
├── docker-compose.yml
├── go.mod
├── go.sum
├── internal
│ ├── database
│ │ └── database.go
│ └── server
│ ├── routes.go
│ └── server.go
└── tests
└── handler_test.go
7 directories, 13 files
先ほども書きましたがMakefile
やdocker-compose.yml
も生成されていますね。1
Makefileには開発を進めるのに便利なコマンドがいくつか登録されています。
たとえばmake run
を実行するとアプリケーションが起動し、localhost:8080
にアクセスするとレスポンスを受け取ることができます。生成したばかりですがすでにサーバーとして機能しています。
またmake docker-run
とするとDockerコンテナが立ち上がり、Postgresが使えるようになります。
試しにtasks
テーブルを作ってレコードを直接INSERTし、アプリケーションのコードにGET /tasks
が来たらそのtasksテーブルの全レコードを返す実装を加えてみると普通に動いてくれました。
root@4edb061ace5b:/# psql -U melkey -d blueprint
psql (16.3 (Debian 16.3-1.pgdg120+1))
Type "help" for help.
blueprint=# CREATE TABLE tasks (
task_id SERIAL PRIMARY KEY,
task_name TEXT NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_completed BOOLEAN DEFAULT FALSE
);
CREATE TABLE
blueprint=# INSERT INTO tasks (task_name, description, is_completed) VALUES
('Buy groceries', 'Milk, Eggs, Bread, Butter', FALSE),
('Write report', 'Finish the quarterly report by Friday', FALSE),
('Call plumber', 'Fix the leaking sink in the kitchen', TRUE),
('Schedule meeting', 'Set up a meeting with the project team', FALSE),
('Pay bills', 'Pay electricity and water bills', FALSE);
INSERT 0 5
データベースとの接続を確立する部分がすでに生成された雛形に含まれており、またdocker-compose.yml
にPostgresのコンテナの設定がすでに含まれているのでアプリケーションのコードには全レコードを取得するためのSELECT
文を書くだけで済みました。楽すぎる...!
さらにmake run
の代わりにmake watch
を実行するとGoのホットリロードツールであるairが起動するので、快適に開発を行うことができます。このairの設定ファイルであるair.toml
も先程の雛形に含まれているので追加で設定する必要はありません。至れり尽くせりですね!
advancedオプションも使ってみる
これでgo-blueprint create
で生成された雛形については大方説明できたかなと思いますが、このgo-blueprintにはadvanced
オプションがあるのでそちらも触っていきたいと思います。
先ほどの雛形を生成するコマンドに--advanced
を付け加えて実行すると、前と同じ3つの質問にプラスして1つ質問が追加されます。
$ go-blueprint create --advanced
ここで聞かれているのは
- Templを使ったHTMXをサポートするか
- GitHub Actionsを使ったCI/CDワークフローの設定も追加するか
- Websocketをサポートするか
です。今回は試しに「Templを使ったHTMX」のみにチェックを入れてみます。
ちなみにtemplとはGoでHTMLを書くためのツールです。
.templ
という拡張子のファイルにGoとHTMLをミックスしたような独自の記法に従ってコーディングをし、templ generate
を実行すると.templ
のファイルからGo言語のソースコードファイルが生成されます。その中にはHTMLをレンダリングするための関数が含まれており、それを使えばサーバーから動的に生成されたHTMLを返すことが可能になります。
生成されたディレクトリに移動し、初回なのでtemplをインストールします。またtemplのファイルを生成するためにtempl generate
も実行します。
go-blueprint % cd sample-project-advanced
sample-project-advanced % go install github.com/a-h/templ/cmd/templ@latest
go: downloading github.com/natefinch/atomic v1.0.1
go: downloading github.com/a-h/protocol v0.0.0-20230224160810-b4eec67c1c22
go: downloading github.com/cenkalti/backoff/v4 v4.3.0
go: downloading github.com/cli/browser v1.3.0
go: downloading go.lsp.dev/jsonrpc2 v0.10.0
go: downloading github.com/a-h/parse v0.0.0-20240121214402-3caf7543159a
go: downloading go.uber.org/zap v1.27.0
go: downloading golang.org/x/mod v0.17.0
go: downloading github.com/andybalholm/brotli v1.1.0
go: downloading go.lsp.dev/uri v0.3.0
go: downloading golang.org/x/sys v0.19.0
go: downloading go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2
go: downloading github.com/segmentio/encoding v0.4.0
go: downloading github.com/segmentio/asm v1.2.0
sample-project-advanced % templ generate
(✓) Complete [ updates=2 duration=2.385834ms ]
ここまでを実行した状態でlocalhost:8080/web
にアクセスすると、下のGIF画像のようなサイトが表示されます。
Helloに続いてInputに入力した文字列が続けて表示されるだけの単純なものですが、これを参考にして先ほどつくったtasksテーブルにWEB上からタスクをINSERTする実装を行っていきます。(TemplやHTMXを触るのは初めてなので、これから先のコードで拙い部分があったらごめんなさい)
まずはモデルを表すTask
とタスク追加のリクエストを表すCreateTaskRequest
をtasks.goに定義します。
package types
import "github.com/google/uuid"
type Task struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt string `json:"created_at"`
IsCompleted bool `json:"is_completed"`
}
type CreateTaskRequest struct {
TaskName string `json:"name"`
Description string `json:"description"`
IsCompleted bool `json:"is_completed"`
}
次に動的なHTMLを生成するために必要な.templ
のコードを書きます。
ここではタスクを追加するためのフォーム欄と、上で定義したTask
のスライスを受取りそれを表形式で表示するためのコンポーネントを記述しています。
package web
import "sample-project-advanced/types"
templ CreateTaskForm(tasks []types.Task) {
@Base() {
<h1>Create Task Form</h1>
<form action="/tasks" method="POST">
<input id="name" name="name" type="text"/>
<input id="description" name="description" type="text"/>
<input id="is_completed" name="is_completed" type="checkbox"/>
<button type="submit">Create!</button>
</form>
@TaskList(tasks)
}
}
templ TaskList(tasks []types.Task) {
<style>
table {
margin: 16px 0px;
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid black;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
</style>
<table>
<thead>
<tr>
<th>Completed</th>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
for _, task := range tasks {
<tr>
<td>
if task.IsCompleted {
<input type="checkbox" checked disabled/>
} else {
<input type="checkbox" disabled/>
}
</td>
<td>{ task.Name }</td>
<td>{ task.Description }</td>
</tr>
}
</tbody>
</table>
}
最後にアプリケーションサーバー側の実装をしていきます。
ハンドラーにHandleTasks
とCreateTaskHandler
を追加し、HandleTasks
では先程のtasks.templ
からHTMLを生成しクライアントに返す実装を、CreateTaskHandler
ではリクエストに受け取った内容でTaskをINSERTする実装をしていきます。
またGetTasks
ではTasksテーブルのすべてのレコードを取得する処理を書いており、これを使って取得したTasksのスライスをHandleTasks
内でCreateTaskForm
の引数として渡しています。
package server
import (
"encoding/json"
"log"
"net/http"
"sample-project-advanced/cmd/web"
"sample-project-advanced/types"
"github.com/a-h/templ"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/google/uuid"
)
func (s *Server) RegisterRoutes() http.Handler {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Get("/", s.HelloWorldHandler)
r.Get("/health", s.healthHandler)
fileServer := http.FileServer(http.FS(web.Files))
r.Handle("/assets/*", fileServer)
r.Get("/web", templ.Handler(web.HelloForm()).ServeHTTP)
r.Post("/hello", web.HelloWebHandler)
// my original Handler
r.Get("/web/tasks", s.HandleTasks) // ←追加
r.Post("/tasks", s.CreateTaskHandler) // ←追加
return r
}
func (s *Server) HelloWorldHandler(w http.ResponseWriter, r *http.Request) {
// 生成された雛形に存在するものなので省略
}
func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
// 生成された雛形に存在するものなので省略
}
func (s *Server) CreateTaskHandler(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var req = types.CreateTaskRequest{
TaskName: r.FormValue("name"),
Description: r.FormValue("description"),
IsCompleted: r.FormValue("is_completed") == "on",
}
id := uuid.New()
query := "INSERT INTO tasks (id, name, description, is_completed) VALUES ($1, $2, $3, $4)"
_, err = s.db.Exec(query, id, req.TaskName, req.Description, req.IsCompleted)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
s.HandleTasks(w, r)
}
func (s *Server) HandleTasks(w http.ResponseWriter, r *http.Request) {
tasks, err := s.GetTasks()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
web.CreateTaskForm(tasks).Render(r.Context(), w)
}
func (s *Server) GetTasks() ([]types.Task, error) {
rows, err := s.db.Query("SELECT id, name, description, created_at, is_completed FROM tasks")
if err != nil {
return nil, err
}
defer rows.Close()
var tasks []types.Task
for rows.Next() {
var task types.Task
err := rows.Scan(&task.ID, &task.Name, &task.Description, &task.CreatedAt, &task.IsCompleted)
if err != nil {
return nil, err
}
tasks = append(tasks, task)
}
if err = rows.Err(); err != nil {
return nil, err
}
return tasks, nil
}
ここまで書けたらTask
を追加するフォームが完成しているはずです。
templの扱いに習熟する必要はありますが、サクッと動的なHTMLを返す実装もできました。ヤッター!
さいごに
今回記事内で作成したプロジェクトは下のGitHubから確認していただけます。
実際に使ってみた感想ですが、ちょっとした実装をしたいときには面倒な環境設定が不要なため便利かなと思いました。ただ良くも悪くも最初に生成されたプロジェクトの構造に縛られてしまうところはあるなとも感じたので、自分の理想のプロジェクトの形がある人にとっては使いづらいかもしれません。興味がある方はぜひお手元で動かしてみてください。
ここまで読んでいただきありがとうございました、間違いなどありましたらコメントにてご指摘ください。
-
ちなみにgo-blueprint.devというサイトで、質問の回答によってどう生成されるファイルが変わるかをWEB上でシミュレートすることもできます。 ↩