はじめに
今回は、Go言語(Golang)を使用して、シンプルなワークアウト記録Webアプリケーションを作成してみました。この記事は、私がGo言語の基本を学ぶための備忘録として書いています。そのため間違っている部分や改善点など、あればコメントしていただけると幸いです。データベースにはPostgreSQLを使用し、Dockerを利用して環境を構築します。
プロジェクト構造
まずは、プロジェクトの構造を見てみましょう:
/my-go-workout-app
├── Dockerfile
├── docker-compose.yml
├── go.mod
├── go.sum
├── init.sql
├── main.go
├── handlers
│ └── records.go
│ └── download_handler.go
├── models
│ └── workout.go
└── templates
└── index.html
各ファイルの役割:
-
Dockerfile
: Goアプリケーションのコンテナ化のための指示書 -
docker-compose.yml
: アプリケーションとデータベースのコンテナを定義・管理 -
go.mod
,go.sum
: Go言語の依存関係管理ファイル -
init.sql
: データベースの初期化スクリプト -
main.go
: アプリケーションのエントリーポイント -
handlers/records.go
: HTTPリクエストを処理するハンドラ関数 -
models/workout.go
: データモデルの定義 -
templates/index.html
: Webページのテンプレート
コード解説
1. go.mod
module myapp
go 1.18
require (
github.com/lib/pq v1.10.9
)
-
module myapp
: このプロジェクトのモジュール名を定義します。 -
go 1.18
: 使用するGo言語のバージョンを指定します。 -
require
: プロジェクトの依存関係を列挙します。ここでは、PostgreSQLドライバgithub.com/lib/pq
を使用しています。
2. main.go
package main // パッケージ名を宣言。Goのプログラムはパッケージ単位で構成される。
import (
"database/sql" // SQLデータベースとのやり取りを行うパッケージ
"fmt" // フォーマット処理を行うためのパッケージ(文字列や出力整形)
"log" // ログ出力を行うパッケージ
"net/http" // HTTPサーバーを構築するためのパッケージ
"os" // 環境変数などのOSとのやり取りを行うパッケージ
"time" // 時間関連の処理を行うパッケージ
"myapp/handlers"
_ "github.com/lib/pq" // PostgreSQLドライバをインポート。明示的に使用しないため、名前を省略
)
func main() {
// DSN(データソース名)を作成。データベース接続情報をフォーマットして環境変数から取得
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable",
os.Getenv("DB_HOST"),
os.Getenv("DB_USER"),
os.Getenv("DB_PASSWORD"),
os.Getenv("DB_NAME"))
var err error // エラーハンドリング用の変数を宣言
for i := 0; i < 30; i++ { // データベース接続を最大30回試みるループ
handlers.DB, err = sql.Open("postgres", dsn) // PostgreSQLデータベースに接続
if err == nil { // 接続成功時の処理
err = handlers.DB.Ping()
if err == nil {
break
}
}
log.Printf("データベース接続を試行中... (%d/30)", i+1)
time.Sleep(2 * time.Second)
}
if err != nil { // 最終的にエラーが発生した場合
log.Fatalf("データベース接続に失敗しました: %v", err) // エラーメッセージを表示してプログラムを終了
}
defer handlers.DB.Close() // プログラム終了時にデータベース接続を閉じる
log.Println("サーバーがポート8080で起動しています...")
http.HandleFunc("/", handlers.ShowRecords)
http.HandleFunc("/add", handlers.AddRecord)
http.HandleFunc("/download", handlers.DownloadCSV)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
解説:
-
import
: 必要なパッケージをインポートします。_
で始まるインポートは、パッケージの初期化のみを行います(ここではPostgreSQLドライバの初期化)。 -
main()
: プログラムのエントリーポイントです。 - データベース接続:環境変数から接続情報を取得し、接続を確立します。接続に失敗した場合は30回まで再試行します。
- HTTPサーバーの設定:
http.HandleFunc()
で各パスに対するハンドラ関数を設定します。 -
http.ListenAndServe(":8080", nil)
: 8080ポートでHTTPサーバーを起動します。
3. handlers/records.go
package handlers
import (
"database/sql"
"fmt"
"log"
"net/http"
"text/template"
"time"
"myapp/models"
)
var DB *sql.DB
var tmpl = template.Must(template.ParseFiles("/app/templates/index.html")) // テンプレートファイルをパースして読み込み
func ShowRecords(w http.ResponseWriter, r *http.Request) {
// `:=`はGo特有の書き方で、型推論を用いて変数を宣言し、値を代入する際に使う。
rows, err := DB.Query("SELECT id, exercise, weight, reps, sets, date FROM workout_records")
if err != nil {
log.Printf("クエリ実行エラー: %v", err)
http.Error(w, "内部サーバーエラー", http.StatusInternalServerError)
return
}
defer rows.Close()
var records []models.WorkoutRecord
for rows.Next() {
var record models.WorkoutRecord
if err := rows.Scan(&record.ID, &record.Exercise, &record.Weight, &record.Reps, &record.Sets, &record.Date); err != nil {
log.Printf("行のスキャンエラー: %v", err)
continue // エラーがあってもスライスに追加せず、ループを継続する。
}
records = append(records, record) // 成功したらスライスに追加
}
if err := tmpl.Execute(w, records); err != nil {
log.Printf("テンプレート実行エラー: %v", err)
http.Error(w, "内部サーバーエラー", http.StatusInternalServerError)
}
}
func AddRecord(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
exercise := r.FormValue("exercise")
reps := r.FormValue("reps")
sets := r.FormValue("sets")
weight := r.FormValue("weight")
date := time.Now().Format("2006-01-02")
if exercise == "" || weight == "" || reps == "" || sets == "" || date == "" {
http.Error(w, "全ての項目を入力してください。", http.StatusBadRequest)
return // フォームの値が空の場合、処理を終了する
}
_, err := DB.Exec("INSERT INTO workout_records (exercise, weight, reps, sets, date) VALUES ($1, $2, $3, $4, $5)", exercise, weight, reps, sets, date)
if err != nil {
log.Printf("レコード挿入エラー: %v", err)
http.Error(w, "内部サーバーエラー", http.StatusInternalServerError)
return
}
fmt.Fprint(w, "トレーニング記録が追加されました!")
}
解説:
-
ShowRecords
: データベースからワークアウト記録を取得し、HTMLテンプレートにデータを渡して表示します。 -
AddRecord
: フォームから送信されたデータを受け取り、新しいワークアウト記録をデータベースに追加します。 - エラーハンドリング:各ステップでエラーをチェックし、適切なHTTPステータスコードとエラーメッセージを返します。
4. models/workout.go
package models
// WorkoutRecordは筋トレ記録の構造体です。
type WorkoutRecord struct {
ID int // レコードID
Exercise string // 種目(例: スクワット)
Weight int // 重量(kg)
Reps int // レップ数(回数)
Sets int // セット数
Date string // 記録の日付
}
解説:
-
WorkoutRecord
: ワークアウト記録の構造体を定義します。この構造体はデータベースのテーブル構造と対応しています。
5. handlers/download_handler.go(データのダウンロード)
package handlers
import (
"encoding/csv"
"net/http"
"strconv"
"myapp/models"
)
func DownloadCSV(w http.ResponseWriter, r *http.Request) {
rows, err := DB.Query("SELECT id, exercise, weight, reps, sets, date FROM workout_records")
if err != nil {
http.Error(w, "データの取得に失敗しました", http.StatusInternalServerError)
return
}
defer rows.Close()
w.Header().Set("Content-Type", "text/csv")
w.Header().Set("Content-Disposition", "attachment; filename=workout_records.csv")
writer := csv.NewWriter(w)
writer.Write([]string{"ID", "種目", "重量 (kg)", "回数", "セット数", "日付"})
for rows.Next() {
var record models.WorkoutRecord
err := rows.Scan(&record.ID, &record.Exercise, &record.Weight, &record.Reps, &record.Sets, &record.Date)
if err != nil {
http.Error(w, "データの読み取りに失敗しました", http.StatusInternalServerError)
return
}
writer.Write([]string{
strconv.Itoa(record.ID),
record.Exercise,
strconv.Itoa(record.Weight),
strconv.Itoa(record.Reps),
strconv.Itoa(record.Sets),
record.Date,
})
}
writer.Flush()
if err := writer.Error(); err != nil {
http.Error(w, "CSVの書き込みに失敗しました", http.StatusInternalServerError)
return
}
}
解説:
- データベースからワークアウト記録を全て取得します。
- HTTPレスポンスヘッダーを設定し、ブラウザにCSVファイルとしてダウンロードするよう指示します。
- CSVライターを作成し、ヘッダー行を書き込みます。
- データベースから取得した各レコードをCSVの行として書き込みます。
- 最後に、バッファに残っているデータを書き込み、エラーチェックを行います。
このハンドラーを使用することで、ユーザーはワークアウト記録を簡単にCSVファイルとしてダウンロードできるようになります。エラーハンドリングも適切に行われており、データベースやCSV書き込みの問題が発生した場合にも適切なエラーメッセージが返されるようになっています。
6. templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GoTrain-筋トレ記録</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f9;
margin: 0;
padding: 20px;
text-align: center;
}
h1 {
color: #333;
}
table {
width: 80%;
margin: 20px auto;
border-collapse: collapse;
background-color: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
th, td {
padding: 12px 20px;
border: 1px solid #ddd;
text-align: center;
}
th {
background-color: #007bff;
color: #fff;
}
tr:nth-child(even) {
background-color: #f2f2f2;
}
form {
margin: 20px auto;
padding: 20px;
background-color: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
width: 50%;
}
input[type="text"],
input[type="number"] {
width: 80%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
background-color: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
label {
display: block;
margin: 10px 0;
text-align: left;
}
.download-link {
display: inline-block;
margin: 20px 0;
padding: 10px 20px;
background-color: #28a745;
color: white;
text-decoration: none;
border-radius: 4px;
}
.download-link:hover {
background-color: #218838;
}
</style>
</head>
<body>
<h1>GoTrain-筋トレ記録</h1>
<table>
<tr>
<th>ID</th>
<th>種目</th>
<th>重量 (kg)</th>
<th>回数</th>
<th>セット数</th>
<th>日付</th>
</tr>
{{range .}}
<tr>
<td>{{.ID}}</td>
<td>{{.Exercise}}</td>
<td>{{.Weight}} kg</td>
<td>{{.Reps}}</td>
<td>{{.Sets}}</td>
<td>{{.Date}}</td>
</tr>
{{end}}
</table>
<p>
<a href="/download" class="download-link">CSVでダウンロード</a>
</p>
<h2>新しいトレーニング記録を追加</h2>
<form action="/add" method="POST">
<label for="exercise">種目:</label>
<input type="text" name="exercise" id="exercise" required><br>
<label for="weight">重量 (kg):</label>
<input type="number" name="weight" id="weight" required><br>
<label for="reps">回数:</label>
<input type="number" name="reps" id="reps" required><br>
<label for="sets">セット数:</label>
<input type="number" name="sets" id="sets" required><br>
<button type="submit">追加</button>
</form>
</body>
</html>
解説:
- シンプルなHTMLテンプレートです。
-
{{range .}}
: Goのテンプレート言語を使用して、ワークアウト記録のリストをループ処理します。 - フォーム:新しいワークアウト記録を追加するためのフォームを提供します。
7. Dockerfile
FROM golang:1.18-alpine
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
RUN go mod verify
COPY . .
RUN go build -o main .
CMD ["./main"]
解説:
-
FROM golang:1.18-alpine
: 軽量なAlpineLinuxベースのGo 1.18イメージを使用します。 -
WORKDIR /app
: 作業ディレクトリを設定します。 - 依存関係のダウンロードと検証を行います。
- ソースコードをコピーし、アプリケーションをビルドします。
-
CMD ["./main"]
: コンテナ起動時にアプリケーションを実行します。
8. docker-compose.yml
version: '3'
services:
db:
image: postgres:13
environment:
- POSTGRES_DB=workoutdb
- POSTGRES_USER=root
- POSTGRES_PASSWORD=strongpassword
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5432:5432"
go-app:
build: .
depends_on:
- db
environment:
- DB_HOST=db
- DB_NAME=workoutdb
- DB_USER=root
- DB_PASSWORD=strongpassword
ports:
- "8080:8080"
volumes:
postgres_data:
解説:
-
db
: PostgreSQLデータベースサービスを定義します。 -
go-app
: Goアプリケーションサービスを定義します。 - 環境変数:データベース接続情報を環境変数として設定します。
-
depends_on
: データベースサービスが起動してからアプリケーションを起動するように設定します。
9. init.sql
CREATE TABLE IF NOT EXISTS workout_records (
id SERIAL PRIMARY KEY,
exercise VARCHAR(255) NOT NULL,
weight INT NOT NULL,
reps INT NOT NULL,
sets INT NOT NULL,
date DATE NOT NULL
);
INSERT INTO workout_records (exercise, weight, reps, sets, date) VALUES
('Push-ups', 20, 3, 3, '2024-10-01'),
('Squats', 15, 4, 4, '2024-10-02'),
('Pull-ups', 10, 3, 3, '2024-10-03');
解説:
- テーブルの作成:
workout_records
テーブルを作成します。 - 初期データの挿入:サンプルデータを挿入します。
アプリケーションの実行
-
依存関係の更新:
go mod tidy
このコマンドは、
go.mod
ファイルを読み取り、必要な依存関係をダウンロードし、go.sum
ファイルを更新します。 -
Dockerコンテナのビルドと起動:
docker-compose up --build
このコマンドは、Dockerイメージをビルドし、コンテナを起動します。
まとめ
今回は、Go言語を使用してシンプルなワークアウト記録Webアプリケーションを作成してました。Go初心者としてデータベース操作、HTTPサーバーの設定、HTMLテンプレートの使用など、Webアプリケーション開発により基本的な要素を理解することに注力しました。
Goは強力で並列処理など効率的な言語です。このプロジェクトをベースに、さらに機能を追加したり、デザインを改善したりして、Goをより深く学んでいきたいと思います。