課題9: 構造体を使ったユーザー管理システム

課題概要

構造体を使って、ユーザー管理システムを実装します。構造体の定義、埋め込み、JSONシリアライゼーション、バリデーション、CRUDオペレーションを学びます。

マンダトリー要件

要件1: ユーザーモデル

ユーザーとアドレスの構造体を定義してください。

ファイル: models/user.go

package models

import (
    "encoding/json"
    "time"
)

type Address struct {
    Street  string `json:"street"`
    City    string `json:"city"`
    State   string `json:"state"`
    ZipCode string `json:"zip_code"`
    Country string `json:"country"`
}

type User struct {
    ID        int       `json:"id"`
    Username  string    `json:"username"`
    Email     string    `json:"email"`
    Password  string    `json:"-"`  // JSONに含めない
    FirstName string    `json:"first_name"`
    LastName  string    `json:"last_name"`
    Age       int       `json:"age,omitempty"`
    Address   *Address  `json:"address,omitempty"`
    IsActive  bool      `json:"is_active"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

// NewUser はユーザーを作成
func NewUser(username, email, password string) (*User, error) {
    // TODO: 実装してください
    // - バリデーション(username、email、password)
    // - CreatedAt と UpdatedAt を現在時刻に設定
    // - IsActive を true に設定
}

// FullName は氏名を返す
func (u *User) FullName() string {
    // TODO: 実装してください
}

// Validate はユーザー情報をバリデーション
func (u *User) Validate() error {
    // TODO: 実装してください
    // - Username: 3文字以上
    // - Email: @ を含む
    // - Password: 8文字以上
    // - Age: 0以上150以下(設定されている場合)
}

// ToJSON はJSON文字列に変換
func (u *User) ToJSON() (string, error) {
    // TODO: 実装してください
}

// FromJSON はJSON文字列から復元
func FromJSON(jsonStr string) (*User, error) {
    // TODO: 実装してください
}

// Clone はユーザーのコピーを作成
func (u *User) Clone() *User {
    // TODO: 実装してください
    // Address もディープコピー
}

要件2: ユーザーリポジトリ

ユーザーの保存・検索機能を実装してください。

ファイル: repository/user_repository.go

package repository

import (
    "errors"
    "yourproject/models"
)

var (
    ErrUserNotFound     = errors.New("user not found")
    ErrDuplicateUser    = errors.New("user already exists")
    ErrInvalidID        = errors.New("invalid user ID")
)

type UserRepository struct {
    users   map[int]*models.User
    nextID  int
    byEmail map[string]*models.User
}

// NewUserRepository はリポジトリを作成
func NewUserRepository() *UserRepository {
    // TODO: 実装してください
}

// Create はユーザーを作成
func (r *UserRepository) Create(user *models.User) error {
    // TODO: 実装してください
    // - IDを自動採番
    // - Email重複チェック
    // - byEmail にも登録
}

// GetByID はIDでユーザーを取得
func (r *UserRepository) GetByID(id int) (*models.User, error) {
    // TODO: 実装してください
}

// GetByEmail はEmailでユーザーを取得
func (r *UserRepository) GetByEmail(email string) (*models.User, error) {
    // TODO: 実装してください
}

// Update はユーザー情報を更新
func (r *UserRepository) Update(user *models.User) error {
    // TODO: 実装してください
    // - 存在チェック
    // - Email変更時は byEmail も更新
    // - UpdatedAt を更新
}

// Delete はユーザーを削除
func (r *UserRepository) Delete(id int) error {
    // TODO: 実装してください
    // - users と byEmail の両方から削除
}

// List はすべてのユーザーを返す
func (r *UserRepository) List() []*models.User {
    // TODO: 実装してください
}

// FindByCity は都市でユーザーを検索
func (r *UserRepository) FindByCity(city string) []*models.User {
    // TODO: 実装してください
}

// Count はユーザー数を返す
func (r *UserRepository) Count() int {
    // TODO: 実装してください
}

// Clear はすべてのユーザーを削除
func (r *UserRepository) Clear() {
    // TODO: 実装してください
}

要件3: ユーザーサービス

ビジネスロジックを含むサービス層を実装してください。

ファイル: service/user_service.go

package service

import (
    "errors"
    "yourproject/models"
    "yourproject/repository"
)

var (
    ErrInvalidCredentials = errors.New("invalid credentials")
    ErrUnauthorized       = errors.New("unauthorized")
)

type UserService struct {
    repo *repository.UserRepository
}

// NewUserService はサービスを作成
func NewUserService(repo *repository.UserRepository) *UserService {
    // TODO: 実装してください
}

// Register は新規ユーザーを登録
func (s *UserService) Register(username, email, password string) (*models.User, error) {
    // TODO: 実装してください
    // - バリデーション
    // - パスワードのハッシュ化(簡易的にsha256を使用)
    // - リポジトリに保存
}

// Login はログイン認証
func (s *UserService) Login(email, password string) (*models.User, error) {
    // TODO: 実装してください
    // - Email でユーザーを検索
    // - パスワード照合
    // - IsActive チェック
}

// UpdateProfile はプロフィールを更新
func (s *UserService) UpdateProfile(id int, firstName, lastName string, age int) error {
    // TODO: 実装してください
    // - ユーザーを取得
    // - フィールドを更新
    // - リポジトリに保存
}

// UpdateAddress はアドレスを更新
func (s *UserService) UpdateAddress(id int, address *models.Address) error {
    // TODO: 実装してください
}

// DeactivateUser はユーザーを無効化
func (s *UserService) DeactivateUser(id int) error {
    // TODO: 実装してください
    // IsActive を false に設定
}

// ActivateUser はユーザーを有効化
func (s *UserService) ActivateUser(id int) error {
    // TODO: 実装してください
}

// GetUsersByCity は都市でユーザーを検索
func (s *UserService) GetUsersByCity(city string) []*models.User {
    // TODO: 実装してください
}

// GetStatistics はユーザー統計を返す
func (s *UserService) GetStatistics() *UserStats {
    // TODO: 実装してください
}

type UserStats struct {
    TotalUsers   int            `json:"total_users"`
    ActiveUsers  int            `json:"active_users"`
    UsersByCity  map[string]int `json:"users_by_city"`
    AverageAge   float64        `json:"average_age"`
}

要件4: CLIインターフェース

コマンドラインから操作できるツールを実装してください。

ファイル: main.go

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
    "yourproject/models"
    "yourproject/repository"
    "yourproject/service"
)

func main() {
    repo := repository.NewUserRepository()
    svc := service.NewUserService(repo)

    // サンプルデータ
    seedData(svc)

    reader := bufio.NewReader(os.Stdin)
    for {
        fmt.Println("\nUser Management System")
        fmt.Println("1. Register user")
        fmt.Println("2. Login")
        fmt.Println("3. List users")
        fmt.Println("4. Search by city")
        fmt.Println("5. Statistics")
        fmt.Println("6. Exit")
        fmt.Print("Select option: ")

        input, _ := reader.ReadString('\n')
        input = strings.TrimSpace(input)

        switch input {
        case "1":
            handleRegister(svc, reader)
        case "2":
            handleLogin(svc, reader)
        case "3":
            handleList(repo)
        case "4":
            handleSearch(svc, reader)
        case "5":
            handleStats(svc)
        case "6":
            fmt.Println("Goodbye!")
            return
        default:
            fmt.Println("Invalid option")
        }
    }
}

func handleRegister(svc *service.UserService, reader *bufio.Reader) {
    // TODO: 実装してください
}

func handleLogin(svc *service.UserService, reader *bufio.Reader) {
    // TODO: 実装してください
}

func handleList(repo *repository.UserRepository) {
    // TODO: 実装してください
}

func handleSearch(svc *service.UserService, reader *bufio.Reader) {
    // TODO: 実装してください
}

func handleStats(svc *service.UserService) {
    // TODO: 実装してください
}

func seedData(svc *service.UserService) {
    // TODO: サンプルデータを作成
}

期待される出力

$ go run main.go

User Management System
1. Register user
2. Login
3. List users
4. Search by city
5. Statistics
6. Exit
Select option: 1

=== Register User ===
Username: alice
Email: alice@example.com
Password: ********
User registered successfully! ID: 1

Select option: 3

=== User List ===
ID: 1, Username: alice, Email: alice@example.com, Active: true
ID: 2, Username: bob, Email: bob@example.com, Active: true

Select option: 5

=== Statistics ===
Total Users: 2
Active Users: 2
Average Age: 27.5
Users by City:
  Tokyo: 1
  Osaka: 1

ボーナス課題

ボーナス1: JSON永続化

ユーザーデータをJSONファイルに保存・読み込みする機能を実装してください。

package repository

// SaveToFile はユーザーをJSONファイルに保存
func (r *UserRepository) SaveToFile(filename string) error {
    // TODO: 実装してください
}

// LoadFromFile はJSONファイルからユーザーを読み込み
func (r *UserRepository) LoadFromFile(filename string) error {
    // TODO: 実装してください
}

// Export はユーザーをJSONで出力
func (r *UserRepository) Export() (string, error) {
    // TODO: 実装してください
}

// Import はJSONからユーザーをインポート
func (r *UserRepository) Import(jsonStr string) error {
    // TODO: 実装してください
}

ボーナス2: クエリビルダー

柔軟な検索機能を実装してください。

package repository

type UserQuery struct {
    repo      *UserRepository
    minAge    *int
    maxAge    *int
    city      *string
    isActive  *bool
}

// Query はクエリビルダーを作成
func (r *UserRepository) Query() *UserQuery {
    // TODO: 実装してください
}

// WithMinAge は最小年齢条件を追加
func (q *UserQuery) WithMinAge(age int) *UserQuery {
    // TODO: 実装してください
}

// WithMaxAge は最大年齢条件を追加
func (q *UserQuery) WithMaxAge(age int) *UserQuery {
    // TODO: 実装してください
}

// WithCity は都市条件を追加
func (q *UserQuery) WithCity(city string) *UserQuery {
    // TODO: 実装してください
}

// WithActiveStatus は有効/無効条件を追加
func (q *UserQuery) WithActiveStatus(isActive bool) *UserQuery {
    // TODO: 実装してください
}

// Execute はクエリを実行
func (q *UserQuery) Execute() []*models.User {
    // TODO: 実装してください
}

// 使用例
users := repo.Query().
    WithMinAge(20).
    WithMaxAge(30).
    WithCity("Tokyo").
    WithActiveStatus(true).
    Execute()

ボーナス3: イベントシステム

ユーザーの作成・更新・削除時にイベントを発火する仕組みを実装してください。

package events

type EventType string

const (
    EventUserCreated EventType = "user.created"
    EventUserUpdated EventType = "user.updated"
    EventUserDeleted EventType = "user.deleted"
)

type Event struct {
    Type      EventType
    UserID    int
    Timestamp time.Time
    Data      interface{}
}

type EventListener func(Event)

type EventBus struct {
    listeners map[EventType][]EventListener
}

// NewEventBus はイベントバスを作成
func NewEventBus() *EventBus {
    // TODO: 実装してください
}

// Subscribe はイベントを購読
func (eb *EventBus) Subscribe(eventType EventType, listener EventListener) {
    // TODO: 実装してください
}

// Publish はイベントを発行
func (eb *EventBus) Publish(event Event) {
    // TODO: 実装してください
}

// 使用例
bus := events.NewEventBus()

bus.Subscribe(events.EventUserCreated, func(e events.Event) {
    fmt.Printf("User created: %v\n", e.Data)
})

bus.Publish(events.Event{
    Type:      events.EventUserCreated,
    UserID:    1,
    Timestamp: time.Now(),
    Data:      user,
})

評価基準

項目 配点 詳細
ユーザーモデル 25点 構造体定義、バリデーション、JSON
リポジトリ 25点 CRUD操作が正しく実装されている
サービス層 25点 ビジネスロジックが適切
CLIツール 25点 ユーザーインターフェースが動作
**ボーナス1** 10点 JSON永続化が実装されている
**ボーナス2** 10点 クエリビルダーが動作する
**ボーナス3** 5点 イベントシステムが実装されている

提出方法

submission/
├── go.mod
├── main.go
├── models/
│   ├── user.go
│   └── user_test.go
├── repository/
│   ├── user_repository.go
│   └── user_repository_test.go
├── service/
│   ├── user_service.go
│   └── user_service_test.go
└── bonus/
    ├── persistence/
    ├── query/
    └── events/

ヒント

  • バリデーション: 別関数に分離して再利用
  • パスワード: crypto/sha256 でハッシュ化
  • JSON: json.Marshaljson.Unmarshal
  • コピー: Address もディープコピー
  • 統計: ループで集計
  • イベント: Publishはすべてのリスナーを呼ぶ
  • テストケース

    // models/user_test.go
    func TestNewUser(t *testing.T) {
        user, err := NewUser("alice", "alice@example.com", "password123")
        if err != nil {
            t.Fatalf("Failed to create user: %v", err)
        }
    
        if user.Username != "alice" {
            t.Errorf("Username = %s; want alice", user.Username)
        }
    
        if !user.IsActive {
            t.Error("User should be active by default")
        }
    }
    
    // repository/user_repository_test.go
    func TestCreateUser(t *testing.T) {
        repo := NewUserRepository()
        user := &models.User{
            Username: "test",
            Email:    "test@example.com",
        }
    
        err := repo.Create(user)
        if err != nil {
            t.Fatalf("Failed to create user: %v", err)
        }
    
        if user.ID == 0 {
            t.Error("User ID should be assigned")
        }
    
        // 重複エラー
        err = repo.Create(user)
        if err != ErrDuplicateUser {
            t.Errorf("Expected duplicate error, got %v", err)
        }
    }
    

    学習リソース

  • Effective Go - Data
  • Go by Example - Structs
  • Go by Example - JSON
  • Struct Tags in Go