2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go言語でシンプルなWebアプリケーションを作る:ワークアウト記録アプリ

Last updated at Posted at 2024-10-06

スクリーンショット 2024-10-05 14.10.40.png

はじめに

今回は、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テーブルを作成します。
  • 初期データの挿入:サンプルデータを挿入します。

アプリケーションの実行

  1. 依存関係の更新:

    go mod tidy
    

    このコマンドは、go.modファイルを読み取り、必要な依存関係をダウンロードし、go.sumファイルを更新します。

  2. Dockerコンテナのビルドと起動:

    docker-compose up --build
    

    このコマンドは、Dockerイメージをビルドし、コンテナを起動します。

  3. ブラウザで http://localhost:8080 にアクセスして、アプリケーションを使用します。
    スクリーンショット 2024-10-06 16.42.43.png
    スクリーンショット 2024-10-06 16.44.04.png

まとめ

今回は、Go言語を使用してシンプルなワークアウト記録Webアプリケーションを作成してました。Go初心者としてデータベース操作、HTTPサーバーの設定、HTMLテンプレートの使用など、Webアプリケーション開発により基本的な要素を理解することに注力しました。
Goは強力で並列処理など効率的な言語です。このプロジェクトをベースに、さらに機能を追加したり、デザインを改善したりして、Goをより深く学んでいきたいと思います。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?