mcpField
G
golang-web

golang-web

github.com/majiayu000開発28899 回インストール1 日前に更新
リポジトリ

golang-web について

golang-webは、サーバーサイドアプリケーションを開発する人向けのGo Webアーキテクチャスキルです。プロジェクト構成、依存関係の配線、レイヤー設計、設定、運用パターンを示し、LLM APIにはLiteLLMの利用を推奨します。

主な機能

  1. 01cmd/internal/pkgのプロジェクト構成
  2. 02明示的な依存関係の配線
  3. 03インターフェース中心のレイヤー設計
  4. 04コンテキスト付きエラーラッピング
  5. 05Viperによる設定パターン
  6. 06LLM API向けLiteLLMプロキシ

活用シーン

  1. 01新規Go Webプロジェクトの構成
  2. 02ハンドラー、サービス、リポジトリーの分離
  3. 03YAMLと環境変数によるアプリ設定
  4. 04LiteLLM経由でのLLM API接続

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/majiayu000/spellbook --skill golang-web

インストール後、Claude Code で/golang-webを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

reference/
architecture.md
patterns.md
tech-stack.md
templates/project/
config.yaml
go.mod
Makefile
templates/project/cmd/myapp/
main.go
templates/project/configs/
config.go
templates/project/internal/handlers/
user.go
templates/project/internal/models/
user.go
templates/project/internal/repositories/
user.go
templates/project/internal/router/
router.go
templates/project/internal/services/
user.go
templates/project/pkg/database/
database.go
templates/project/pkg/errors/
errors.go
templates/project/pkg/response/
response.go
templates/project/pkg/server/
server.go

詳細

Go Web アーキテクチャ

基本原則

  • 標準レイアウト — cmd/internal/pkg の規約に従います
  • 依存関係の明示 — main.go で依存関係を配線し、グローバル変数は使いません
  • インターフェース駆動 — インターフェースは実装する場所ではなく、使用する場所で定義します
  • エラーのラッピング — コンテキストを付けてエラーをラップし、エラーコードを使います
  • 後方互換性は不要 — 非推奨にするのではなく削除します。直接変更します
  • LLM API には LiteLLM — すべての LLM 統合に LiteLLM プロキシを使います

後方互換性は不要

未使用のコードを削除します。直接変更します。互換性レイヤーは設けません。

// ❌ BAD: Deprecated function kept around
// Deprecated: Use NewUserService instead
func CreateUserService() *UserService { ... }

// ❌ BAD: Alias for renamed types
type OldName = NewName // "for backwards compatibility"

// ❌ BAD: Unused parameters
func Process(_ context.Context, data Data) { ... }

// ✅ GOOD: Just delete and update all usages
func NewUserService(repo UserRepository) *UserService { ... }

LLM API には LiteLLM

LiteLLM プロキシを使います。プロバイダーの API を直接呼び出してはいけません。

// adapters/llm/client.go
package llm

import (
    "github.com/sashabaranov/go-openai"
)

// Connect to LiteLLM proxy using OpenAI-compatible SDK
func NewClient(cfg Config) *openai.Client {
    config := openai.DefaultConfig(cfg.APIKey)
    config.BaseURL = cfg.BaseURL // LiteLLM proxy URL
    return openai.NewClientWithConfig(config)
}

クイックスタート

1. プロジェクトを初期化する
mkdir myapp && cd myapp
go mod init github.com/yourname/myapp

# Install core dependencies
go get github.com/gin-gonic/gin
go get github.com/spf13/viper
go get github.com/sirupsen/logrus
go get gorm.io/gorm
2. 技術スタックを適用する
レイヤー推奨
HTTP フレームワークGin / Chi / Echo
設定Viper
ロギングLogrus / Zap / Slog
データベース ORMGORM / sqlx / sqlc
バリデーションgo-playground/validator
テストtestify / go test
バージョン戦略

常に最新のものを取得します。テンプレートでバージョンを固定してはいけません。

# Always fetch latest
go get -u github.com/gin-gonic/gin
go get -u ./...

# go.mod handles version locking
# go.sum ensures reproducible builds
3. 標準構成を使う
myapp/
├── cmd/
│   └── myapp/
│       └── main.go            # Entry point, dependency wiring
├── configs/
│   └── config.go              # Configuration struct + loader
├── internal/                  # Private application code
│   ├── handlers/              # HTTP handlers
│   ├── services/              # Business logic
│   ├── repositories/          # Data access
│   ├── models/                # Domain models
│   ├── middleware/            # HTTP middleware
│   └── router/                # Route definitions
├── pkg/                       # Public reusable packages
│   ├── errors/                # Error types
│   ├── logger/                # Logging setup
│   ├── response/              # Unified response format
│   └── database/              # Database connection
├── config.yaml                # Configuration file
├── Makefile                   # Build automation
├── Dockerfile
└── go.mod

アーキテクチャレイヤー

cmd/ — エントリーポイント

ここですべての依存関係を配線します。ビジネスロジックは含めません。

// cmd/myapp/main.go
func main() {
    // Load config
    cfg := configs.Load()

    // Initialize infrastructure
    db := database.New(cfg.Database)
    cache := cache.New(cfg.Redis)
    logger := logger.New(cfg.Log)

    // Initialize repositories
    userRepo := repositories.NewUserRepository(db)

    // Initialize services
    userService := services.NewUserService(userRepo)

    // Initialize handlers
    userHandler := handlers.NewUserHandler(userService)

    // Setup router
    r := router.Setup(cfg, userHandler)

    // Start server with graceful shutdown
    server.Run(r, cfg.Server)
}
internal/ — 非公開のビジネスコード
handlers/ — HTTP レイヤー
// internal/handlers/user.go
type UserHandler struct {
    service services.UserService
}

func NewUserHandler(s services.UserService) *UserHandler {
    return &UserHandler{service: s}
}

func (h *UserHandler) Create(c *gin.Context) {
    var input CreateUserInput
    if err := c.ShouldBindJSON(&input); err != nil {
        response.Error(c, errors.ErrInvalidParams)
        return
    }

    user, err := h.service.Create(c.Request.Context(), input)
    if err != nil {
        response.Error(c, err)
        return
    }

    response.Success(c, user)
}
services/ — ビジネスロジック
// internal/services/user.go
type UserService interface {
    Create(ctx context.Context, input CreateUserInput) (*models.User, error)
    GetByID(ctx context.Context, id string) (*models.User, error)
}

type userService struct {
    repo repositories.UserRepository
}

func NewUserService(repo repositories.UserRepository) UserService {
    return &userService{repo: repo}
}

func (s *userService) Create(ctx context.Context, input CreateUserInput) (*models.User, error) {
    existing, _ := s.repo.FindByEmail(ctx, input.Email)
    if existing != nil {
        return nil, errors.ErrUserExists
    }

    user := &models.User{
        ID:    uuid.New().String(),
        Email: input.Email,
        Name:  input.Name,
    }

    return s.repo.Save(ctx, user)
}
repositories/ — データアクセス
// internal/repositories/user.go
type UserRepository interface {
    FindByID(ctx context.Context, id string) (*models.User, error)
    FindByEmail(ctx context.Context, email string) (*models.User, error)
    Save(ctx context.Context, user *models.User) (*models.User, error)
    Delete(ctx context.Context, id string) error
}

type userRepository struct {
    db *gorm.DB
}

func NewUserRepository(db *gorm.DB) UserRepository {
    return &userRepository{db: db}
}

func (r *userRepository) FindByID(ctx context.Context, id string) (*models.User, error) {
    var user models.User
    if err := r.db.WithContext(ctx).First(&user, "id = ?", id).Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, nil
        }
        return nil, err
    }
    return &user, nil
}
pkg/ — 再利用可能なパッケージ
errors/ — エラー処理
// pkg/errors/errors.go
type AppError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Cause   error  `json:"-"`
}

func (e *AppError) Error() string { return e.Message }
func (e *AppError) Unwrap() error { return e.Cause }

func New(code int, message string) *AppError {
    return &AppError{Code: code, Message: message}
}

func Wrap(err error, code int, message string) *AppError {
    return &AppError{Code: code, Message: message, Cause: err}
}

// Predefined errors
var (
    ErrInternal      = New(500, "internal server error")
    ErrInvalidParams = New(400, "invalid parameters")
    ErrNotFound      = New(404, "resource not found")
    ErrUnauthorized  = New(401, "unauthorized")
    ErrUserExists    = New(409, "user already exists")
)
response/ — 統一レスポンス
// pkg/response/response.go
type Response struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
}

func Success(c *gin.Context, data interface{}) {
    c.JSON(http.StatusOK, Response{
        Code:    0,
        Message: "success",
        Data:    data,
    })
}

func Error(c *gin.Context, err error) {
    var appErr *errors.AppError
    if errors.As(err, &appErr) {
        c.JSON(appErr.Code/100, Response{
            Code:    appErr.Code,
            Message: appErr.Message,
        })
        return
    }
    c.JSON(http.StatusInternalServerError, Response{
        Code:    500,
        Message: "internal server error",
    })
}

設定

Viper + YAML + 環境変数
// configs/config.go
type Config struct {
    Server   ServerConfig   `mapstructure:"server"`
    Database DatabaseConfig `mapstructure:"database"`
    Redis    RedisConfig    `mapstructure:"redis"`
    Log      LogConfig      `mapstructure:"log"`
    LLM      LLMConfig      `mapstructure:"llm"`
}

type LLMConfig struct {
    BaseURL      string `mapstructure:"base_url"`
    APIKey       string `mapstructure:"api_key"`
    DefaultModel string `mapstructure:"default_model"`
}

func Load() *Config {
    viper.SetConfigFile("config.yaml")
    viper.AutomaticEnv()
    viper.SetEnvPrefix("APP")
    viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

    // Defaults
    viper.SetDefault("server.port", 8080)
    viper.SetDefault("llm.base_url", "http://localhost:4000")
    viper.SetDefault("llm.default_model", "gpt-4o")

    viper.ReadInConfig()

    var cfg Config
    viper.Unmarshal(&cfg)
    return &cfg
}

グレースフルシャットダウン

// pkg/server/server.go
func Run(handler http.Handler, cfg ServerConfig) {
    srv := &http.Server{
        Addr:         fmt.Sprintf(":%d", cfg.Port),
        Handler:      handler,
        ReadTimeout:  cfg.ReadTimeout,
        WriteTimeout: cfg.WriteTimeout,
    }

    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    srv.Shutdown(ctx)
}

Makefile

.PHONY: build run test lint clean

APP_NAME=myapp

build:
	go build -o bin/$(APP_NAME) ./cmd/$(APP_NAME)

run:
	go run ./cmd/$(APP_NAME)

dev:
	air

test:
	go test -v ./...

lint:
	golangci-lint run

clean:
	rm -rf bin/

tidy:
	go mod tidy

upgrade:
	go get -u ./...
	go mod tidy

チェックリスト

## Project Setup
- [ ] Go 1.21+ installed
- [ ] Standard directory structure (cmd/internal/pkg)
- [ ] go.mod initialized
- [ ] Makefile created

## Architecture
- [ ] Dependencies wired in main.go
- [ ] Handlers → Services → Repositories layers
- [ ] Interfaces defined at usage site
- [ ] No circular dependencies

## Infrastructure
- [ ] Configuration with Viper
- [ ] Structured logging
- [ ] Custom error types
- [ ] Unified response format
- [ ] Graceful shutdown

## Quality
- [ ] Tests for services
- [ ] golangci-lint configured
- [ ] go vet passes
- [ ] Race detection tested

関連項目

コメント