課題2: プロジェクト構築とツールチェーン実践

課題概要

この課題では、Go Modulesを使用してプロジェクトを作成し、標準ツールチェーンを活用してコード品質を向上させます。依存関係管理、テスト、フォーマット、静的解析など、実務で必須となるスキルを習得します。

マンダトリー要件

要件1: プロジェクトの作成

簡単な計算機アプリケーションを作成します。

プロジェクト構造:

calculator/
├── go.mod
├── go.sum
├── main.go
├── calc/
│   ├── calc.go
│   └── calc_test.go
└── README.md

ステップ1: プロジェクト初期化

mkdir calculator
cd calculator
go mod init github.com/yourname/calculator

ステップ2: calc/calc.go の実装

package calc

// Add は2つの整数を加算します
func Add(a, b int) int {
    return a + b
}

// Subtract は2つの整数を減算します
func Subtract(a, b int) int {
    return a - b
}

// Multiply は2つの整数を乗算します
func Multiply(a, b int) int {
    return a * b
}

// Divide は2つの整数を除算します
// ゼロ除算の場合はエラーを返します
func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, ErrDivisionByZero
    }
    return a / b, nil
}

// ErrDivisionByZero はゼロ除算エラーです
var ErrDivisionByZero = errors.New("division by zero")

ステップ3: calc/calc_test.go の実装

package calc

import "testing"

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 5, 3, 8},
        {"negative numbers", -5, -3, -8},
        {"mixed signs", 5, -3, 2},
        {"with zero", 0, 5, 5},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

func TestDivide(t *testing.T) {
    // 正常系
    result, err := Divide(10, 2)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if result != 5 {
        t.Errorf("Divide(10, 2) = %d; want 5", result)
    }

    // ゼロ除算
    _, err = Divide(10, 0)
    if err != ErrDivisionByZero {
        t.Errorf("expected ErrDivisionByZero, got %v", err)
    }
}

// ベンチマーク
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(100, 200)
    }
}

ステップ4: main.go の実装

package main

import (
    "fmt"
    "os"
    "strconv"

    "github.com/yourname/calculator/calc"
)

func main() {
    if len(os.Args) < 4 {
        fmt.Println("Usage: calculator <operation> <num1> <num2>")
        fmt.Println("Operations: add, sub, mul, div")
        os.Exit(1)
    }

    operation := os.Args[1]
    a, err := strconv.Atoi(os.Args[2])
    if err != nil {
        fmt.Println("Error: invalid number:", os.Args[2])
        os.Exit(1)
    }

    b, err := strconv.Atoi(os.Args[3])
    if err != nil {
        fmt.Println("Error: invalid number:", os.Args[3])
        os.Exit(1)
    }

    var result int
    switch operation {
    case "add":
        result = calc.Add(a, b)
        fmt.Printf("%d + %d = %d\n", a, b, result)
    case "sub":
        result = calc.Subtract(a, b)
        fmt.Printf("%d - %d = %d\n", a, b, result)
    case "mul":
        result = calc.Multiply(a, b)
        fmt.Printf("%d × %d = %d\n", a, b, result)
    case "div":
        result, err := calc.Divide(a, b)
        if err != nil {
            fmt.Println("Error:", err)
            os.Exit(1)
        }
        fmt.Printf("%d ÷ %d = %d\n", a, b, result)
    default:
        fmt.Println("Error: unknown operation:", operation)
        os.Exit(1)
    }
}

要件2: テストとカバレッジ

すべてのテストが成功し、カバレッジが80%以上であることを確認してください。

# テスト実行
go test ./...

# 詳細出力
go test -v ./...

# カバレッジ測定
go test -cover ./...

# カバレッジレポート生成
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html

# ベンチマーク実行
go test -bench=. ./...

要件3: コード品質チェック

以下のツールを実行し、すべての警告を解決してください。

# フォーマット
go fmt ./...

# 静的解析
go vet ./...

# レースコンディション検出(並行処理がある場合)
go test -race ./...

要件4: 実行とビルド

# 直接実行
go run main.go add 10 20

# ビルド
go build -o calculator

# 実行
./calculator add 10 20
# 出力: 10 + 20 = 30

./calculator div 10 0
# 出力: Error: division by zero

要件5: ドキュメント作成

README.md を作成してください。

# Calculator

シンプルなコマンドライン計算機

## インストール

bash go install github.com/yourname/calculator@latest

## 使い方

bash calculator

### 操作

- `add`: 加算
- `sub`: 減算
- `mul`: 乗算
- `div`: 除算

### 例

bash calculator add 10 20 # 10 + 20 = 30 calculator sub 10 5 # 10 - 5 = 5 calculator mul 3 4 # 3 × 4 = 12 calculator div 10 2 # 10 ÷ 2 = 5

## テスト

bash go test ./...

## ライセンス

MIT

期待される出力

テスト結果

go test -v ./...
=== RUN   TestAdd
=== RUN   TestAdd/positive_numbers
=== RUN   TestAdd/negative_numbers
=== RUN   TestAdd/mixed_signs
=== RUN   TestAdd/with_zero
--- PASS: TestAdd (0.00s)
    --- PASS: TestAdd/positive_numbers (0.00s)
    --- PASS: TestAdd/negative_numbers (0.00s)
    --- PASS: TestAdd/mixed_signs (0.00s)
    --- PASS: TestAdd/with_zero (0.00s)
=== RUN   TestDivide
--- PASS: TestDivide (0.00s)
PASS
ok      github.com/yourname/calculator/calc     0.123s

カバレッジ結果

go test -cover ./...
ok      github.com/yourname/calculator/calc     0.123s  coverage: 95.0% of statements

実行例

./calculator add 10 20
10 + 20 = 30

./calculator div 10 0
Error: division by zero

ボーナス課題

> ボーナス: これらはオプションです。マンダトリー部分が完了してから取り組んでください。

ボーナス1: 外部パッケージの統合

カラー出力機能を追加してください。

# github.com/fatih/color パッケージを追加
go get github.com/fatih/color

main.goを改良:

import (
    "github.com/fatih/color"
    // ...
)

func main() {
    // ...
    switch operation {
    case "add":
        result = calc.Add(a, b)
        color.Green("%d + %d = %d\n", a, b, result)
    case "div":
        result, err := calc.Divide(a, b)
        if err != nil {
            color.Red("Error: %v\n", err)
            os.Exit(1)
        }
        color.Green("%d ÷ %d = %d\n", a, b, result)
    // ...
    }
}

依存関係が正しく管理されていることを確認:

go mod tidy
cat go.mod
cat go.sum

ボーナス2: クロスコンパイル

複数プラットフォーム向けにビルドスクリプトを作成してください。

build.sh:

#!/bin/bash

# バージョン情報
VERSION="1.0.0"
LDFLAGS="-s -w -X main.version=${VERSION}"

# プラットフォームリスト
PLATFORMS=(
    "darwin/amd64"
    "darwin/arm64"
    "linux/amd64"
    "linux/arm64"
    "windows/amd64"
)

# ビルドディレクトリ
BUILD_DIR="build"
rm -rf ${BUILD_DIR}
mkdir -p ${BUILD_DIR}

for PLATFORM in "${PLATFORMS[@]}"; do
    GOOS=${PLATFORM%/*}
    GOARCH=${PLATFORM#*/}
    OUTPUT="${BUILD_DIR}/calculator-${GOOS}-${GOARCH}"

    if [ "$GOOS" = "windows" ]; then
        OUTPUT="${OUTPUT}.exe"
    fi

    echo "Building for ${GOOS}/${GOARCH}..."
    GOOS=${GOOS} GOARCH=${GOARCH} go build \
        -ldflags="${LDFLAGS}" \
        -o ${OUTPUT}

    if [ $? -ne 0 ]; then
        echo "Failed to build for ${GOOS}/${GOARCH}"
        exit 1
    fi
done

echo "Build complete!"
ls -lh ${BUILD_DIR}

実行:

chmod +x build.sh
./build.sh

ボーナス3: CI/CD設定

GitHub Actionsでテストとビルドを自動化してください。

.github/workflows/test.yml:

name: Test

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.21'

      - name: Install dependencies
        run: go mod download

      - name: Format check
        run: |
          go fmt ./...
          git diff --exit-code

      - name: Vet
        run: go vet ./...

      - name: Test
        run: go test -v -race -coverprofile=coverage.out ./...

      - name: Coverage
        run: go tool cover -func=coverage.out

      - name: Build
        run: go build -v ./...

ボーナス4: ベンチマーク最適化

各関数のベンチマークを実装し、パフォーマンスを測定してください。

// calc/calc_test.go に追加

func BenchmarkSubtract(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Subtract(1000, 500)
    }
}

func BenchmarkMultiply(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Multiply(100, 200)
    }
}

func BenchmarkDivide(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Divide(1000, 10)
    }
}

実行:

go test -bench=. -benchmem ./...

評価基準

項目 配点 詳細
プロジェクト構造 15点 適切なディレクトリ構造とファイル配置
関数実装 20点 4つの演算が正しく実装されている
テスト 25点 テストが書かれ、カバレッジ80%以上
コード品質 20点 fmt, vet で警告なし
ドキュメント 20点 README.mdが適切に記述されている
**ボーナス1** 5点 外部パッケージが正しく統合されている
**ボーナス2** 5点 クロスコンパイルが成功している
**ボーナス3** 5点 CI/CDが動作している
**ボーナス4** 5点 ベンチマークが実装されている

提出方法

以下の構造でGitHubリポジトリを作成し、URLを提出してください:

calculator/
├── go.mod
├── go.sum
├── main.go
├── calc/
│   ├── calc.go
│   └── calc_test.go
├── README.md
├── coverage.html          # カバレッジレポート
├── build.sh              # ボーナス2
└── .github/              # ボーナス3
    └── workflows/
        └── test.yml

ヒント

  • テーブルドリブンテスト: 複数のテストケースを効率的に記述できます
  • エラー処理: Goでは戻り値でエラーを返すのが慣習です
  • パッケージ設計: 再利用可能な機能は別パッケージに分離します
  • go mod tidy: 使っていない依存関係を自動削除します
  • -ldflags: ビルド時にバージョン情報などを埋め込めます
  • 学習リソース

  • Go Modules Reference
  • Testing in Go
  • Go Command Documentation
  • Effective Go - Testing
  • GitHub Actions for Go