プログラミング学習において「書いたコードをすぐに実行できる」環境は非常に重要です。本記事では、Go言語の学習に特化したWebベースの学習プラットフォームの設計について詳しく解説します。
プロジェクト概要
ビジョン
「Go言語を段階的に、実践的に学べるインタラクティブな学習環境」を提供し、初心者から中級者まで効率的にGo言語をマスターできるプラットフォームを構築します。
主要機能
- ブラウザ上でのコード実行: インストール不要でGo言語を実行
- 段階的な学習パス: 基礎から応用まで体系的なカリキュラム
- リアルタイムフィードバック: コード実行結果の即座な表示
- 進捗管理: 学習の進行状況を可視化
- インタラクティブな課題: 実践的な問題を解きながら学習
技術スタック
フロントエンド
UI Framework: Next.js 14 (App Router)
言語: TypeScript
スタイリング: Tailwind CSS + shadcn/ui
コードエディタ: Monaco Editor (VS Code エディタ)
状態管理: Zustand
認証: NextAuth.js
リアルタイム通信: WebSocket (Socket.io)
バックエンド
APIサーバー: Go (Echo or Gin framework)
コード実行環境: Docker + Kubernetes
サンドボックス: gVisor or Firecracker
キャッシュ: Redis
データベース: PostgreSQL
メッセージキュー: RabbitMQ or NATS
オブジェクトストレージ: MinIO or S3
インフラストラクチャ
コンテナオーケストレーション: Kubernetes
CI/CD: GitHub Actions + ArgoCD
モニタリング: Prometheus + Grafana
ログ管理: ELK Stack
CDN: CloudFlare
リバースプロキシ: Nginx or Traefik
システムアーキテクチャ
全体構成図
┌──────────────────────────────────────────────────────┐
│ CDN (CloudFlare) │
└────────────────────────┬─────────────────────────────┘
│
┌────────────────────────▼─────────────────────────────┐
│ Load Balancer (Nginx/Traefik) │
└────────────────────────┬─────────────────────────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌───────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Frontend │ │ API │ │ WebSocket │
│ (Next.js) │ │ Server │ │ Server │
└──────────────┘ └──────┬──────┘ └──────────────┘
│
┌─────────────┼─────────────┐
│ │ │
┌─────────▼────┐ ┌─────▼────┐ ┌─────▼─────┐
│ PostgreSQL │ │ Redis │ │ RabbitMQ │
└──────────────┘ └──────────┘ └─────┬─────┘
│
┌──────▼──────┐
│ Execution │
│ Workers │
│ (Docker) │
└─────────────┘
コード実行フロー
sequenceDiagram
participant User
participant Frontend
participant API
participant Queue
participant Worker
participant Docker
User->>Frontend: コード入力
Frontend->>API: 実行リクエスト
API->>Queue: ジョブ追加
Queue->>Worker: ジョブ取得
Worker->>Docker: コンテナ作成
Docker->>Docker: コード実行
Docker-->>Worker: 実行結果
Worker-->>API: 結果送信
API-->>Frontend: WebSocket通知
Frontend-->>User: 結果表示
コア機能の実装
1. セキュアなコード実行環境
Docker設定 (Dockerfile.sandbox)
FROM golang:1.21-alpine AS builder
# セキュリティ向上のための非rootユーザー作成
RUN adduser -D -g '' appuser
WORKDIR /app
# 実行環境の制限設定
RUN apk add --no-cache \
ca-certificates \
&& rm -rf /var/cache/apk/*
FROM scratch
# 必要最小限のファイルのみコピー
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
USER appuser
WORKDIR /sandbox
# リソース制限のデフォルト値
ENV GOMEMLIMIT=128MiB
ENV GOMAXPROCS=1
ENTRYPOINT ["/bin/sh"]
コード実行サービス (Go)
package executor
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
)
type ExecutionRequest struct {
ID string `json:"id"`
Code string `json:"code"`
Language string `json:"language"`
Timeout int `json:"timeout"`
}
type ExecutionResult struct {
ID string `json:"id"`
Output string `json:"output"`
Error string `json:"error"`
ExitCode int `json:"exit_code"`
Duration int64 `json:"duration"`
}
type CodeExecutor struct {
dockerClient *client.Client
maxTimeout time.Duration
}
func NewCodeExecutor() (*CodeExecutor, error) {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
return &CodeExecutor{
dockerClient: cli,
maxTimeout: 30 * time.Second,
}, nil
}
func (e *CodeExecutor) Execute(req ExecutionRequest) (*ExecutionResult, error) {
ctx, cancel := context.WithTimeout(context.Background(), e.maxTimeout)
defer cancel()
startTime := time.Now()
// コードを一時ファイルに保存
tmpFile, err := os.CreateTemp("", "code-*.go")
if err != nil {
return nil, err
}
defer os.Remove(tmpFile.Name())
if _, err := tmpFile.WriteString(req.Code); err != nil {
return nil, err
}
tmpFile.Close()
// Dockerコンテナの設定
config := &container.Config{
Image: "golang-sandbox:latest",
Cmd: []string{"go", "run", "/code/main.go"},
Env: []string{
"GOMEMLIMIT=128MiB",
"GOMAXPROCS=1",
},
WorkingDir: "/code",
// リソース制限
Memory: 128 * 1024 * 1024, // 128MB
MemorySwap: 128 * 1024 * 1024, // スワップ無効
CpuShares: 512, // CPU制限
}
hostConfig := &container.HostConfig{
AutoRemove: true,
// セキュリティ設定
ReadonlyRootfs: true,
SecurityOpt: []string{"no-new-privileges"},
CapDrop: []string{"ALL"},
NetworkMode: "none", // ネットワーク無効
// ファイルマウント
Binds: []string{
fmt.Sprintf("%s:/code/main.go:ro", tmpFile.Name()),
},
}
// コンテナ作成と実行
resp, err := e.dockerClient.ContainerCreate(
ctx,
config,
hostConfig,
nil,
nil,
"",
)
if err != nil {
return nil, err
}
if err := e.dockerClient.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
return nil, err
}
// 実行結果の取得
statusCh, errCh := e.dockerClient.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning)
select {
case err := <-errCh:
if err != nil {
return nil, err
}
case status := <-statusCh:
// ログ取得
logs, err := e.dockerClient.ContainerLogs(ctx, resp.ID, types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
})
if err != nil {
return nil, err
}
defer logs.Close()
output, _ := io.ReadAll(logs)
return &ExecutionResult{
ID: req.ID,
Output: string(output),
ExitCode: int(status.StatusCode),
Duration: time.Since(startTime).Milliseconds(),
}, nil
}
return nil, fmt.Errorf("execution failed")
}
2. 学習コンテンツ管理システム
データベース設計
-- ユーザーテーブル
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(100) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- コーステーブル
CREATE TABLE courses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
description TEXT,
difficulty_level INTEGER CHECK (difficulty_level BETWEEN 1 AND 5),
estimated_hours INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- レッスンテーブル
CREATE TABLE lessons (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
course_id UUID REFERENCES courses(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
order_index INTEGER NOT NULL,
lesson_type VARCHAR(50) CHECK (lesson_type IN ('tutorial', 'exercise', 'project')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 課題テーブル
CREATE TABLE challenges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
lesson_id UUID REFERENCES lessons(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
starter_code TEXT,
solution_code TEXT,
test_cases JSONB,
hints JSONB,
difficulty_level INTEGER CHECK (difficulty_level BETWEEN 1 AND 5),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 進捗管理テーブル
CREATE TABLE user_progress (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
lesson_id UUID REFERENCES lessons(id) ON DELETE CASCADE,
challenge_id UUID REFERENCES challenges(id) ON DELETE CASCADE,
status VARCHAR(50) CHECK (status IN ('not_started', 'in_progress', 'completed')),
score INTEGER,
attempts INTEGER DEFAULT 0,
completed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, lesson_id, challenge_id)
);
-- 実行履歴テーブル
CREATE TABLE execution_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
challenge_id UUID REFERENCES challenges(id) ON DELETE CASCADE,
code TEXT NOT NULL,
output TEXT,
error TEXT,
execution_time_ms INTEGER,
success BOOLEAN,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- インデックス
CREATE INDEX idx_user_progress_user_id ON user_progress(user_id);
CREATE INDEX idx_user_progress_status ON user_progress(status);
CREATE INDEX idx_execution_history_user_id ON execution_history(user_id);
CREATE INDEX idx_execution_history_created_at ON execution_history(created_at);
3. フロントエンド実装
コードエディタコンポーネント (React/TypeScript)
import React, { useState, useRef, useEffect } from 'react';
import Editor, { Monaco } from '@monaco-editor/react';
import { Button } from '@/components/ui/button';
import { Play, RotateCcw, Save } from 'lucide-react';
interface CodeEditorProps {
initialCode?: string;
challengeId: string;
onExecute: (code: string) => Promise<ExecutionResult>;
onSave?: (code: string) => void;
}
interface ExecutionResult {
output: string;
error?: string;
executionTime: number;
success: boolean;
}
export const CodeEditor: React.FC<CodeEditorProps> = ({
initialCode = '',
challengeId,
onExecute,
onSave,
}) => {
const [code, setCode] = useState(initialCode);
const [output, setOutput] = useState<string>('');
const [isExecuting, setIsExecuting] = useState(false);
const [executionTime, setExecutionTime] = useState<number>(0);
const editorRef = useRef<any>(null);
const defaultCode = `package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}`;
useEffect(() => {
// ローカルストレージから保存されたコードを読み込み
const savedCode = localStorage.getItem(`challenge-${challengeId}`);
if (savedCode) {
setCode(savedCode);
}
}, [challengeId]);
const handleEditorDidMount = (editor: any, monaco: Monaco) => {
editorRef.current = editor;
// Go言語のシンタックスハイライト設定
monaco.languages.register({ id: 'go' });
monaco.languages.setMonarchTokensProvider('go', {
keywords: [
'break', 'case', 'chan', 'const', 'continue', 'default',
'defer', 'else', 'fallthrough', 'for', 'func', 'go',
'goto', 'if', 'import', 'interface', 'map', 'package',
'range', 'return', 'select', 'struct', 'switch', 'type', 'var',
],
typeKeywords: [
'bool', 'byte', 'complex64', 'complex128', 'error', 'float32',
'float64', 'int', 'int8', 'int16', 'int32', 'int64', 'rune',
'string', 'uint', 'uint8', 'uint16', 'uint32', 'uint64', 'uintptr',
],
operators: [
'=', '>', '<', '!', '~', '?', ':', '==', '<=', '>=', '!=',
'&&', '||', '++', '--', '+', '-', '*', '/', '&', '|', '^',
'%', '<<', '>>', '>>>', '+=', '-=', '*=', '/=', '&=', '|=',
'^=', '%=', '<<=', '>>=', '>>>=',
],
tokenizer: {
root: [
[/[a-z_$][\w$]*/, {
cases: {
'@typeKeywords': 'keyword',
'@keywords': 'keyword',
'@default': 'identifier',
},
}],
[/[A-Z][\w$]*/, 'type.identifier'],
{ include: '@whitespace' },
[/[{}()\[\]]/, '@brackets'],
[/[<>](?!@symbols)/, '@brackets'],
[/@symbols/, {
cases: {
'@operators': 'operator',
'@default': '',
},
}],
[/\d*\.\d+([eE][\-+]?\d+)?/, 'number.float'],
[/0[xX][0-9a-fA-F]+/, 'number.hex'],
[/\d+/, 'number'],
[/[;,.]/, 'delimiter'],
[/"([^"\\]|\\.)*$/, 'string.invalid'],
[/"/, { token: 'string.quote', bracket: '@open', next: '@string' }],
[/'[^\\']'/, 'string'],
[/(')(@escapes)(')/, ['string', 'string.escape', 'string']],
[/'/, 'string.invalid'],
],
whitespace: [
[/[ \t\r\n]+/, 'white'],
[/\/\*/, 'comment', '@comment'],
[/\/\/.*$/, 'comment'],
],
comment: [
[/[^\/*]+/, 'comment'],
[/\/\*/, 'comment', '@push'],
['\\*/', 'comment', '@pop'],
[/[\/*]/, 'comment'],
],
string: [
[/[^\\"]+/, 'string'],
[/@escapes/, 'string.escape'],
[/\\./, 'string.escape.invalid'],
[/"/, { token: 'string.quote', bracket: '@close', next: '@pop' }],
],
},
});
// ショートカットキーの設定
editor.addAction({
id: 'execute-code',
label: 'Execute Code',
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
run: () => handleExecute(),
});
};
const handleExecute = async () => {
setIsExecuting(true);
setOutput('実行中...');
const startTime = Date.now();
try {
const result = await onExecute(code);
setExecutionTime(Date.now() - startTime);
if (result.success) {
setOutput(result.output);
} else {
setOutput(`エラー: ${result.error || '実行に失敗しました'}`);
}
} catch (error) {
setOutput(`エラー: ${error}`);
} finally {
setIsExecuting(false);
}
};
const handleSave = () => {
localStorage.setItem(`challenge-${challengeId}`, code);
if (onSave) {
onSave(code);
}
};
const handleReset = () => {
setCode(defaultCode);
setOutput('');
setExecutionTime(0);
};
return (
<div className="flex flex-col h-full">
<div className="flex items-center justify-between p-2 bg-gray-100 dark:bg-gray-800 border-b">
<div className="flex items-center gap-2">
<Button
onClick={handleExecute}
disabled={isExecuting}
size="sm"
className="flex items-center gap-2"
>
<Play className="w-4 h-4" />
実行 (Cmd+Enter)
</Button>
<Button
onClick={handleSave}
variant="outline"
size="sm"
className="flex items-center gap-2"
>
<Save className="w-4 h-4" />
保存
</Button>
<Button
onClick={handleReset}
variant="outline"
size="sm"
className="flex items-center gap-2"
>
<RotateCcw className="w-4 h-4" />
リセット
</Button>
</div>
{executionTime > 0 && (
<span className="text-sm text-gray-600">
実行時間: {executionTime}ms
</span>
)}
</div>
<div className="flex-1 grid grid-cols-2 gap-2">
<div className="border rounded-lg overflow-hidden">
<Editor
height="100%"
defaultLanguage="go"
value={code}
onChange={(value) => setCode(value || '')}
onMount={handleEditorDidMount}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 14,
lineNumbers: 'on',
roundedSelection: false,
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 4,
insertSpaces: false,
}}
/>
</div>
<div className="border rounded-lg bg-gray-900 text-gray-100 p-4 overflow-auto">
<div className="font-mono text-sm whitespace-pre-wrap">
{output || 'ここに実行結果が表示されます'}
</div>
</div>
</div>
</div>
);
};
4. APIサーバー実装 (Go/Echo)
package main
import (
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/gorilla/websocket"
)
type Server struct {
executor *CodeExecutor
db *sql.DB
cache *redis.Client
upgrader websocket.Upgrader
}
func NewServer() (*Server, error) {
executor, err := NewCodeExecutor()
if err != nil {
return nil, err
}
// データベース接続
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
return nil, err
}
// Redis接続
cache := redis.NewClient(&redis.Options{
Addr: os.Getenv("REDIS_URL"),
})
return &Server{
executor: executor,
db: db,
cache: cache,
upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 本番環境では適切に設定
},
},
}, nil
}
func (s *Server) ExecuteCode(c echo.Context) error {
var req ExecutionRequest
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request",
})
}
// レート制限チェック
userID := c.Get("user_id").(string)
if err := s.checkRateLimit(userID); err != nil {
return c.JSON(http.StatusTooManyRequests, map[string]string{
"error": "Rate limit exceeded",
})
}
// コードの検証
if err := s.validateCode(req.Code); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": err.Error(),
})
}
// 実行
result, err := s.executor.Execute(req)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Execution failed",
})
}
// 実行履歴を保存
go s.saveExecutionHistory(userID, req, result)
return c.JSON(http.StatusOK, result)
}
func (s *Server) checkRateLimit(userID string) error {
key := fmt.Sprintf("rate_limit:%s", userID)
// 1分間に10回まで
count, err := s.cache.Incr(ctx, key).Result()
if err != nil {
return err
}
if count == 1 {
s.cache.Expire(ctx, key, time.Minute)
}
if count > 10 {
return fmt.Errorf("rate limit exceeded")
}
return nil
}
func (s *Server) validateCode(code string) error {
// 危険なパッケージのインポートをチェック
dangerousPackages := []string{
"os/exec",
"syscall",
"unsafe",
"net",
"net/http",
}
for _, pkg := range dangerousPackages {
if strings.Contains(code, fmt.Sprintf(`import "%s"`, pkg)) ||
strings.Contains(code, fmt.Sprintf(`import ( "%s"`, pkg)) {
return fmt.Errorf("package %s is not allowed", pkg)
}
}
// コードサイズ制限(10KB)
if len(code) > 10*1024 {
return fmt.Errorf("code size exceeds limit")
}
return nil
}
func main() {
e := echo.New()
// ミドルウェア
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"http://localhost:3000"},
AllowMethods: []string{http.MethodGet, http.MethodPost},
}))
server, err := NewServer()
if err != nil {
e.Logger.Fatal(err)
}
// ルート定義
api := e.Group("/api/v1")
api.POST("/execute", server.ExecuteCode)
api.GET("/challenges/:id", server.GetChallenge)
api.POST("/challenges/:id/submit", server.SubmitSolution)
api.GET("/progress", server.GetUserProgress)
// WebSocket
e.GET("/ws", server.HandleWebSocket)
e.Logger.Fatal(e.Start(":8080"))
}
学習コンテンツの設計
カリキュラム構成
基礎編(Beginner):
1. Hello World:
- Goの基本構文
- パッケージとインポート
- main関数
2. 変数とデータ型:
- 変数宣言
- 基本データ型
- 型推論
3. 制御構造:
- if文
- for文
- switch文
4. 関数:
- 関数定義
- 引数と戻り値
- 複数戻り値
中級編(Intermediate):
1. 配列とスライス:
- 配列の基本
- スライスの操作
- make関数
2. マップ:
- マップの作成
- 要素の追加・削除
- 範囲for文
3. 構造体:
- 構造体定義
- メソッド
- ポインタレシーバー
4. インターフェース:
- インターフェース定義
- 実装
- 型アサーション
上級編(Advanced):
1. 並行処理:
- goroutine
- channel
- select文
2. エラーハンドリング:
- error型
- panicとrecover
- カスタムエラー
3. テスト:
- 単体テスト
- ベンチマーク
- テストカバレッジ
4. パッケージ管理:
- go mod
- 依存関係管理
- バージョニング
課題例
// レベル1: Hello World
// 課題: "Hello, [名前]!" を出力する関数を作成
package main
import "fmt"
func greet(name string) string {
// ここにコードを書いてください
return ""
}
func main() {
result := greet("Gopher")
fmt.Println(result) // 期待値: "Hello, Gopher!"
}
// テストケース
func TestGreet(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"Gopher", "Hello, Gopher!"},
{"World", "Hello, World!"},
{"", "Hello, !"},
}
for _, test := range tests {
result := greet(test.input)
if result != test.expected {
t.Errorf("greet(%q) = %q; want %q",
test.input, result, test.expected)
}
}
}
セキュリティ対策
1. コード実行環境の隔離
- Dockerコンテナ: 各実行を独立したコンテナで実行
- リソース制限: CPU、メモリ、実行時間の制限
- ネットワーク隔離: 外部ネットワークへのアクセスを遮断
- 読み取り専用ファイルシステム: システムファイルの改変を防止
2. 入力検証
func validateUserInput(code string) error {
// SQLインジェクション対策
if strings.Contains(strings.ToLower(code), "drop table") {
return errors.New("suspicious SQL command detected")
}
// OSコマンドインジェクション対策
dangerousCommands := []string{
"rm ", "dd ", "mkfs", "chmod", "chown",
}
for _, cmd := range dangerousCommands {
if strings.Contains(code, cmd) {
return errors.New("dangerous system command detected")
}
}
return nil
}
3. レート制限
type RateLimiter struct {
mu sync.Mutex
requests map[string][]time.Time
}
func (r *RateLimiter) Allow(userID string) bool {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
windowStart := now.Add(-time.Minute)
// 古いリクエストを削除
if requests, exists := r.requests[userID]; exists {
var filtered []time.Time
for _, t := range requests {
if t.After(windowStart) {
filtered = append(filtered, t)
}
}
r.requests[userID] = filtered
}
// リクエスト数をチェック
if len(r.requests[userID]) >= 10 {
return false
}
// 新しいリクエストを追加
r.requests[userID] = append(r.requests[userID], now)
return true
}
パフォーマンス最適化
1. コード実行のキャッシング
func (s *Server) getCachedResult(codeHash string) (*ExecutionResult, error) {
key := fmt.Sprintf("execution:%s", codeHash)
val, err := s.cache.Get(ctx, key).Result()
if err == redis.Nil {
return nil, nil
}
if err != nil {
return nil, err
}
var result ExecutionResult
if err := json.Unmarshal([]byte(val), &result); err != nil {
return nil, err
}
return &result, nil
}
func (s *Server) cacheResult(codeHash string, result *ExecutionResult) error {
key := fmt.Sprintf("execution:%s", codeHash)
data, err := json.Marshal(result)
if err != nil {
return err
}
// 1時間キャッシュ
return s.cache.Set(ctx, key, data, time.Hour).Err()
}
2. コンテナプールの実装
type ContainerPool struct {
mu sync.Mutex
containers chan string
maxSize int
}
func NewContainerPool(size int) *ContainerPool {
pool := &ContainerPool{
containers: make(chan string, size),
maxSize: size,
}
// 事前にコンテナを作成
for i := 0; i < size; i++ {
go pool.createContainer()
}
return pool
}
func (p *ContainerPool) Get() (string, error) {
select {
case containerID := <-p.containers:
return containerID, nil
case <-time.After(5 * time.Second):
// タイムアウト時は新規作成
return p.createNewContainer()
}
}
func (p *ContainerPool) Put(containerID string) {
select {
case p.containers <- containerID:
// プールに戻す
default:
// プールが満杯の場合は削除
p.removeContainer(containerID)
}
}
モニタリングとログ
Prometheus メトリクス
var (
executionCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "code_executions_total",
Help: "Total number of code executions",
},
[]string{"status", "language"},
)
executionDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "code_execution_duration_seconds",
Help: "Code execution duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"language"},
)
)
func init() {
prometheus.MustRegister(executionCounter)
prometheus.MustRegister(executionDuration)
}
構造化ログ
import "go.uber.org/zap"
func setupLogger() *zap.Logger {
config := zap.NewProductionConfig()
config.OutputPaths = []string{"stdout"}
config.ErrorOutputPaths = []string{"stderr"}
logger, _ := config.Build()
return logger
}
// 使用例
logger.Info("Code execution started",
zap.String("user_id", userID),
zap.String("challenge_id", challengeID),
zap.Int("code_length", len(code)),
)
デプロイメント
Kubernetes マニフェスト
apiVersion: apps/v1
kind: Deployment
metadata:
name: go-learning-api
spec:
replicas: 3
selector:
matchLabels:
app: go-learning-api
template:
metadata:
labels:
app: go-learning-api
spec:
containers:
- name: api
image: go-learning/api:latest
ports:
- containerPort: 8080
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
- name: REDIS_URL
value: redis-service:6379
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: go-learning-api-service
spec:
selector:
app: go-learning-api
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
CI/CD パイプライン (GitHub Actions)
name: Deploy
on:
push:
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: Run tests
run: |
go test -v ./...
go test -race -coverprofile=coverage.txt ./...
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.txt
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: |
docker build -t go-learning/api:${{ github.sha }} .
docker tag go-learning/api:${{ github.sha }} go-learning/api:latest
- name: Push to registry
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker push go-learning/api:${{ github.sha }}
docker push go-learning/api:latest
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/go-learning-api api=go-learning/api:${{ github.sha }}
kubectl rollout status deployment/go-learning-api
まとめ
このGo言語学習プラットフォームの設計では、以下の点に重点を置きました:
- セキュリティ: Dockerコンテナによる隔離、入力検証、レート制限
- スケーラビリティ: Kubernetes、コンテナプール、キャッシング
- ユーザー体験: リアルタイム実行、即座のフィードバック、進捗管理
- 教育的価値: 段階的な学習パス、実践的な課題、詳細な解説
これらの要素を組み合わせることで、安全で効率的、かつ教育効果の高い学習プラットフォームを構築できます。
インフラストラクチャと運用
データベースの選定
1. メインデータベース: PostgreSQL
-
選定理由:
- ACIDトランザクションによる高い信頼性
- JSONBサポートによる柔軟なデータ構造
- 豊富なインデックス機能(B-tree, Hash, GiST, GIN)
- パーティショニングによる大規模データ対応
-
利用サービス: Amazon RDS for PostgreSQL
- マルチAZ構成で高可用性を実現
- 自動バックアップとポイントインタイムリカバリ
- Read Replicaによる読み取り性能の向上
-- テーブル設計例
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(50) UNIQUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE challenges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
description TEXT,
difficulty VARCHAR(20) CHECK (difficulty IN ('beginner', 'intermediate', 'advanced')),
test_cases JSONB NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE submissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
challenge_id UUID REFERENCES challenges(id),
code TEXT NOT NULL,
status VARCHAR(20) CHECK (status IN ('pending', 'running', 'success', 'failure')),
output TEXT,
execution_time_ms INTEGER,
memory_usage_bytes BIGINT,
submitted_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_challenge (user_id, challenge_id)
) PARTITION BY RANGE (submitted_at);
-- パーティション作成(月次)
CREATE TABLE submissions_2025_01 PARTITION OF submissions
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
2. キャッシュデータベース: Redis
-
選定理由:
- インメモリによる高速アクセス
- Pub/Sub機能によるリアルタイム通信
- セッション管理に最適
-
利用サービス: Amazon ElastiCache for Redis
- クラスターモードで自動スケーリング
- 自動フェイルオーバー
3. 時系列データ: TimescaleDB
- 用途: メトリクス、ログ、実行履歴の保存
- 選定理由:
- PostgreSQLの拡張で既存インフラと統合しやすい
- 自動パーティショニングと圧縮
ホスティングサービス
AWS構成
# インフラストラクチャ構成
Infrastructure:
Region: ap-northeast-1 (東京)
Compute:
- Service: Amazon EKS
NodeGroups:
- Name: api-servers
InstanceType: t3.medium
MinSize: 2
MaxSize: 10
DesiredSize: 3
- Name: code-executors
InstanceType: c5.large
MinSize: 1
MaxSize: 20
DesiredSize: 5
Database:
- Service: Amazon RDS (PostgreSQL)
InstanceClass: db.t3.medium
MultiAZ: true
Storage: 100GB (gp3)
- Service: Amazon ElastiCache (Redis)
NodeType: cache.t3.micro
NumNodes: 2
Storage:
- Service: Amazon S3
Buckets:
- user-code-storage
- submission-logs
- static-assets
CDN:
- Service: Amazon CloudFront
Origins:
- S3 (static assets)
- ALB (API)
Container Registry:
- Service: Amazon ECR
Repositories:
- go-learning-api
- code-executor
Monitoring:
- CloudWatch
- Prometheus + Grafana (EKS上)
- ELK Stack (Elasticsearch, Logstash, Kibana)
運用コスト試算(月額)
小規模(〜1,000ユーザー)
Amazon EKS:
- Control Plane: $72
- t3.medium × 2: $60
- c5.large × 1: $62
Amazon RDS:
- db.t3.medium (Multi-AZ): $140
- Storage 100GB: $25
Amazon ElastiCache:
- cache.t3.micro × 2: $25
Amazon S3 + CloudFront:
- Storage & Transfer: $20
Amazon ECR:
- Storage: $10
その他(ALB, NAT Gateway等): $50
合計: 約$464/月(約7万円/月)
中規模(〜10,000ユーザー)
Amazon EKS:
- Control Plane: $72
- t3.medium × 4: $120
- c5.large × 3: $186
Amazon RDS:
- db.r5.large (Multi-AZ): $460
- Storage 500GB: $125
- Read Replica: $230
Amazon ElastiCache:
- cache.r5.large × 2: $340
Amazon S3 + CloudFront:
- Storage & Transfer: $150
Amazon ECR:
- Storage: $30
その他: $200
合計: 約$1,913/月(約29万円/月)
大規模(〜100,000ユーザー)
Amazon EKS:
- Control Plane × 2: $144
- t3.large × 10: $600
- c5.xlarge × 10: $1,240
Amazon RDS:
- db.r5.2xlarge (Multi-AZ): $1,840
- Storage 2TB: $500
- Read Replica × 3: $2,760
Amazon ElastiCache:
- cache.r5.xlarge × 4: $1,360
Amazon S3 + CloudFront:
- Storage & Transfer: $1,000
Amazon ECR:
- Storage: $100
その他: $500
合計: 約$10,044/月(約150万円/月)
コスト最適化戦略
1. Reserved Instances / Savings Plans
- 1年または3年契約で最大72%割引
- 予測可能なワークロードに適用
2. Spot Instances
- コード実行用のワーカーノードに利用
- 最大90%のコスト削減
3. Auto Scaling
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: go-learning-api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
4. コンテナ最適化
# マルチステージビルドでイメージサイズ削減
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
CMD ["./main"]
災害復旧計画(DRP)
バックアップ戦略
BackupStrategy:
Database:
- Type: Automated Backup
Frequency: Daily
Retention: 30 days
- Type: Manual Snapshot
Frequency: Weekly
Retention: 90 days
CodeRepository:
- Type: S3 Cross-Region Replication
Target: us-west-2
Configurations:
- Type: AWS Backup
Frequency: Daily
Retention: 7 days
復旧目標
- RPO (Recovery Point Objective): 1時間
- RTO (Recovery Time Objective): 4時間
監視とアラート
Monitoring:
Metrics:
- API Response Time: < 200ms (p95)
- Code Execution Time: < 5s (p95)
- Error Rate: < 1%
- Availability: > 99.9%
Alerts:
Critical:
- Service Down
- Database Connection Failed
- Error Rate > 5%
Warning:
- Response Time > 500ms
- CPU Usage > 80%
- Memory Usage > 90%
Info:
- New Deployment
- Scaling Event
- Backup Completed
次のステップ
- MVP開発: 基本機能の実装とテスト
- コンテンツ作成: 学習教材と課題の充実
- ユーザーテスト: フィードバックの収集と改善
- 機能拡張: AI による学習支援、コードレビュー機能
- コミュニティ機能: フォーラム、コード共有、メンタリング
このプラットフォームを通じて、多くの開発者がGo言語を効率的に学び、実践的なスキルを身につけることができるでしょう。