Go言語Webアプリケーション開発完全ガイド2026
Go言語(Golang)はGoogleが2009年に公開したシステムプログラミング言語だ。静的型付け・コンパイル型・ガベージコレクション付きという特性を持ちながら、C言語に匹敵する実行速度とPythonに近い開発生産性を両立させている。特にWebサーバーやマイクロサービスの領域では、その真価が際立つ。
本記事では、GoによるWeb開発の基礎から本番運用まで、実際に動作するコード例を豊富に交えながら体系的に解説する。標準ライブラリのnet/httpから始まり、Gin・Echoといったフレームワーク、GORMによるデータベース操作、JWT認証、WebSocket、そしてDockerとマイクロサービスまで、現代のWeb開発に必要な知識を網羅する。
1. Go言語とWeb開発
なぜGoがWeb開発で選ばれるのか
Goがバックエンド開発者の間で急速に普及している理由は、以下の点に集約される。
コンパイル速度と実行速度の両立
Goはコンパイル言語でありながら、コンパイル速度が極めて速い。大規模なプロジェクトでも数秒でビルドが完了し、生成されたバイナリは単一ファイルで依存関係を含まない。実行速度はJavaや.NETに匹敵し、インタープリタ型言語であるPythonやRubyを大幅に上回る。
並行処理モデル(Goroutine)
Goの最大の特徴は、Goroutineと呼ばれる軽量スレッドだ。OSスレッドと異なり、初期スタックサイズはわずか2KBほどで、数十万のGoroutineを同時に起動できる。WebサーバーでHTTPリクエストを処理する際、各リクエストをGoroutineで処理することで、高い並行性を低コストで実現できる。
シンプルな言語仕様
Goのキーワード数は25個しかない(Pythonは35個、Javaは50個以上)。クラス継承・ジェネリクス(Go 1.18以降は追加)・例外処理がなく、シンプルで読みやすいコードが書ける。チームでの開発において、コードの可読性と保守性が高い。
標準ライブラリの充実
net/httpパッケージだけで本格的なWebサーバーを構築できる。JSON処理・暗号化・テスト・ベンチマークなど、Web開発に必要な機能の多くが標準ライブラリで提供されている。
Goのパフォーマンスベンチマーク
TechEmpower Framework Benchmarksのデータによると、GoのWebフレームワーク(FiberやEcho)は毎秒100万リクエスト以上を処理できるケースがある。これはNode.jsの約3〜5倍、Python(FastAPI)の約10倍の性能に相当する。
言語/フレームワーク 毎秒リクエスト数(概算)
Fiber (Go) 1,200,000 req/s
Gin (Go) 950,000 req/s
Actix-web (Rust) 1,000,000 req/s
Spring Boot (Java) 400,000 req/s
Express (Node.js) 280,000 req/s
FastAPI (Python) 100,000 req/s
Django (Python) 40,000 req/s
(上記は参考値。実際の性能はハードウェアや設定に依存する)
Goを採用している主要企業
- Google: Kubernetes、Docker(一部)、各種内部サービス
- Docker: コンテナランタイム本体
- Cloudflare: DNSサービス、CDNインフラ
- Uber: 位置情報サービス、マイクロサービス群
- Dropbox: バックエンドサービスのPythonからGoへの移行
- HashiCorp: Terraform、Vault、Consul
2. 開発環境のセットアップ
Goのインストール
公式サイト(https://go.dev/dl/)から最新版をダウンロードするか、パッケージマネージャーを使用する。
macOS(Homebrew)
brew install go
Linux(Ubuntu/Debian)
# 公式バイナリをダウンロード
wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz
# PATHに追加(~/.bashrc または ~/.zshrc に追記)
export PATH=$PATH:/usr/local/go/bin
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin
バージョン確認
go version
# go version go1.23.0 linux/amd64
プロジェクトの初期化(Goモジュール)
Go 1.11以降、モジュールシステムが標準となった。プロジェクトのルートディレクトリで以下を実行する。
mkdir mywebapp
cd mywebapp
go mod init github.com/yourusername/mywebapp
これによりgo.modファイルが生成される。
module github.com/yourusername/mywebapp
go 1.23.0
推奨ディレクトリ構成
mywebapp/
├── cmd/
│ └── server/
│ └── main.go # エントリーポイント
├── internal/
│ ├── handler/ # HTTPハンドラー
│ ├── middleware/ # ミドルウェア
│ ├── model/ # データモデル
│ ├── repository/ # データベース操作
│ └── service/ # ビジネスロジック
├── pkg/
│ ├── config/ # 設定管理
│ ├── database/ # DB接続
│ └── logger/ # ログ設定
├── migrations/ # DBマイグレーション
├── tests/ # 統合テスト
├── Dockerfile
├── docker-compose.yml
├── go.mod
└── go.sum
この構成はGo公式のStandard Go Project Layoutに基づいている。internal/ディレクトリ内のパッケージは、同じモジュール内からしかインポートできないため、APIの境界を明確に定義できる。
3. 標準net/httpパッケージ
基本的なHTTPサーバー
Goの標準ライブラリだけで、機能的なWebサーバーを構築できる。
// cmd/server/main.go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
// レスポンス用の構造体
type Response struct {
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
Status int `json:"status"`
}
// JSONレスポンスを返すヘルパー関数
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(data); err != nil {
log.Printf("JSONエンコードエラー: %v", err)
}
}
// ハンドラー関数
func helloHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, Response{
Message: "GETメソッドのみ許可されています",
Status: http.StatusMethodNotAllowed,
})
return
}
resp := Response{
Message: "Hello, Go Web Server!",
Timestamp: time.Now(),
Status: http.StatusOK,
}
writeJSON(w, http.StatusOK, resp)
}
// ヘルスチェックハンドラー
func healthHandler(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{
"status": "healthy",
"time": time.Now().Format(time.RFC3339),
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", helloHandler)
mux.HandleFunc("/health", healthHandler)
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
fmt.Println("サーバーを起動中: http://localhost:8080")
if err := server.ListenAndServe(); err != nil {
log.Fatalf("サーバー起動エラー: %v", err)
}
}
ミドルウェアの実装
標準パッケージでもミドルウェアパターンを実装できる。
// ロギングミドルウェア
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// レスポンスライターをラップしてステータスコードをキャプチャ
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r)
duration := time.Since(start)
log.Printf(
"[%s] %s %s - %d (%v)",
r.Method,
r.RemoteAddr,
r.URL.Path,
wrapped.statusCode,
duration,
)
})
}
// レスポンスライターのラッパー
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// CORSミドルウェア
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// ミドルウェアチェーンを適用
func applyMiddleware(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
URLパスパラメータの処理(Go 1.22以降)
Go 1.22からServeMuxがパスパラメータをサポートするようになった。
func main() {
mux := http.NewServeMux()
// {id} でパスパラメータを定義(Go 1.22+)
mux.HandleFunc("GET /users/{id}", getUserHandler)
mux.HandleFunc("POST /users", createUserHandler)
mux.HandleFunc("PUT /users/{id}", updateUserHandler)
mux.HandleFunc("DELETE /users/{id}", deleteUserHandler)
server := &http.Server{
Addr: ":8080",
Handler: applyMiddleware(mux, loggingMiddleware, corsMiddleware),
}
log.Fatal(server.ListenAndServe())
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
// パスパラメータの取得
id := r.PathValue("id")
writeJSON(w, http.StatusOK, map[string]string{
"id": id,
"message": fmt.Sprintf("ユーザー %s の情報", id),
})
}
4. Ginフレームワーク
Ginの概要とインストール
GinはGoで最も広く使われているWebフレームワークの一つだ。高速なルーティング(httprouterベース)・ミドルウェアサポート・バリデーション・JSONバインディングなど、Web開発に必要な機能を揃えている。
go get -u github.com/gin-gonic/gin
基本的なGinアプリケーション
// cmd/server/main.go
package main
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
// ユーザーモデル
type User struct {
ID int `json:"id"`
Name string `json:"name" binding:"required,min=2,max=50"`
Email string `json:"email" binding:"required,email"`
Age int `json:"age" binding:"required,min=18,max=120"`
Role string `json:"role"`
Password string `json:"password,omitempty" binding:"required,min=8"`
}
// インメモリのユーザーストア(実際はDBを使用する)
var users = []User{
{ID: 1, Name: "田中太郎", Email: "tanaka@example.com", Age: 30, Role: "admin"},
{ID: 2, Name: "佐藤花子", Email: "sato@example.com", Age: 25, Role: "user"},
}
func main() {
// リリースモードで実行(本番環境)
// gin.SetMode(gin.ReleaseMode)
r := gin.Default() // LoggerとRecoveryミドルウェアが自動設定される
// CORSミドルウェア
r.Use(corsMiddleware())
// ルートグループ
api := r.Group("/api/v1")
{
users := api.Group("/users")
{
users.GET("", listUsers)
users.GET("/:id", getUser)
users.POST("", createUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
}
}
// ヘルスチェック
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
})
if err := r.Run(":8080"); err != nil {
panic(err)
}
}
// ユーザー一覧取得
func listUsers(c *gin.Context) {
// クエリパラメータの取得
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
search := c.Query("search")
_ = page
_ = limit
_ = search
c.JSON(http.StatusOK, gin.H{
"users": users,
"total": len(users),
})
}
// ユーザー取得
func getUser(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "無効なIDです"})
return
}
for _, user := range users {
if user.ID == id {
c.JSON(http.StatusOK, user)
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "ユーザーが見つかりません"})
}
// ユーザー作成
func createUser(c *gin.Context) {
var newUser User
if err := c.ShouldBindJSON(&newUser); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "バリデーションエラー",
"details": err.Error(),
})
return
}
newUser.ID = len(users) + 1
newUser.Role = "user"
newUser.Password = "" // パスワードはレスポンスに含めない
users = append(users, newUser)
c.JSON(http.StatusCreated, newUser)
}
// ユーザー更新
func updateUser(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "無効なIDです"})
return
}
var updateData User
if err := c.ShouldBindJSON(&updateData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for i, user := range users {
if user.ID == id {
users[i].Name = updateData.Name
users[i].Email = updateData.Email
c.JSON(http.StatusOK, users[i])
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "ユーザーが見つかりません"})
}
// ユーザー削除
func deleteUser(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "無効なIDです"})
return
}
for i, user := range users {
if user.ID == id {
users = append(users[:i], users[i+1:]...)
c.JSON(http.StatusOK, gin.H{"message": "削除しました"})
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "ユーザーが見つかりません"})
}
Ginミドルウェアの実装
// internal/middleware/cors.go
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
)
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Header("Access-Control-Allow-Methods", "POST, HEAD, PATCH, OPTIONS, GET, PUT, DELETE")
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
// レート制限ミドルウェア(シンプルな実装)
// pkg/middleware/ratelimit.go
package middleware
import (
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
)
type RateLimiter struct {
mu sync.Mutex
requests map[string][]time.Time
limit int
window time.Duration
}
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
return &RateLimiter{
requests: make(map[string][]time.Time),
limit: limit,
window: window,
}
}
func (rl *RateLimiter) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
now := time.Now()
rl.mu.Lock()
defer rl.mu.Unlock()
// ウィンドウ外の古いリクエストを削除
windowStart := now.Add(-rl.window)
var recent []time.Time
for _, t := range rl.requests[ip] {
if t.After(windowStart) {
recent = append(recent, t)
}
}
if len(recent) >= rl.limit {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "レート制限を超えました。しばらく待ってから再試行してください。",
})
c.Abort()
return
}
rl.requests[ip] = append(recent, now)
c.Next()
}
}
// 認証ミドルウェア(JWTは後述)
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "認証トークンが必要です"})
c.Abort()
return
}
// JWTの検証は次のセクションで詳述
c.Next()
}
}
5. Echoフレームワーク
Echoの特徴
EchoはGinと並ぶ人気フレームワークで、より型安全なAPIと優れたパフォーマンスが特徴だ。特にカスタムHTTPエラーハンドリングとデータバインディングが強力だ。
go get github.com/labstack/echo/v4
go get github.com/labstack/echo/v4/middleware
Echoによる実装
// cmd/server/main.go
package main
import (
"errors"
"net/http"
"strconv"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type Product struct {
ID int `json:"id"`
Name string `json:"name" validate:"required,min=2,max=100"`
Description string `json:"description" validate:"required"`
Price float64 `json:"price" validate:"required,gt=0"`
Stock int `json:"stock" validate:"min=0"`
Category string `json:"category" validate:"required"`
}
// カスタムHTTPエラーハンドラー
func customHTTPErrorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
message := "内部サーバーエラーが発生しました"
var he *echo.HTTPError
if errors.As(err, &he) {
code = he.Code
if msg, ok := he.Message.(string); ok {
message = msg
}
}
c.JSON(code, map[string]interface{}{
"error": message,
"code": code,
})
}
func main() {
e := echo.New()
// カスタムエラーハンドラー
e.HTTPErrorHandler = customHTTPErrorHandler
// ミドルウェアの設定
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))
// GZip圧縮
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Level: 5,
}))
// リクエストIDの自動付与
e.Use(middleware.RequestID())
// ルーティング
api := e.Group("/api/v1")
products := api.Group("/products")
products.GET("", listProducts)
products.GET("/:id", getProduct)
products.POST("", createProduct, authMiddleware)
products.PUT("/:id", updateProduct, authMiddleware)
products.DELETE("/:id", deleteProduct, authMiddleware)
// サーバー起動
e.Logger.Fatal(e.Start(":8080"))
}
var products = []Product{
{ID: 1, Name: "MacBook Pro", Description: "高性能ノートPC", Price: 298000, Stock: 10, Category: "electronics"},
{ID: 2, Name: "iPhone 16", Description: "最新スマートフォン", Price: 129800, Stock: 50, Category: "electronics"},
}
func listProducts(c echo.Context) error {
category := c.QueryParam("category")
var result []Product
if category != "" {
for _, p := range products {
if p.Category == category {
result = append(result, p)
}
}
} else {
result = products
}
return c.JSON(http.StatusOK, map[string]interface{}{
"products": result,
"total": len(result),
})
}
func getProduct(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "無効なIDです")
}
for _, p := range products {
if p.ID == id {
return c.JSON(http.StatusOK, p)
}
}
return echo.NewHTTPError(http.StatusNotFound, "商品が見つかりません")
}
func createProduct(c echo.Context) error {
var p Product
if err := c.Bind(&p); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "リクエストの形式が不正です")
}
// バリデーション
if p.Name == "" || p.Price <= 0 {
return echo.NewHTTPError(http.StatusUnprocessableEntity, "必須フィールドが不足しています")
}
p.ID = len(products) + 1
products = append(products, p)
return c.JSON(http.StatusCreated, p)
}
func updateProduct(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "無効なIDです")
}
var updateData Product
if err := c.Bind(&updateData); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "リクエストの形式が不正です")
}
for i, p := range products {
if p.ID == id {
products[i].Name = updateData.Name
products[i].Price = updateData.Price
products[i].Stock = updateData.Stock
return c.JSON(http.StatusOK, products[i])
}
}
return echo.NewHTTPError(http.StatusNotFound, "商品が見つかりません")
}
func deleteProduct(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "無効なIDです")
}
for i, p := range products {
if p.ID == id {
products = append(products[:i], products[i+1:]...)
return c.JSON(http.StatusOK, map[string]string{"message": "削除しました"})
}
}
return echo.NewHTTPError(http.StatusNotFound, "商品が見つかりません")
}
func authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")
if token == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "認証が必要です")
}
return next(c)
}
}
6. GORMによるデータベース操作
GORMのインストールと設定
GORMはGoで最も人気のあるORMライブラリだ。PostgreSQL・MySQL・SQLite・SQL Serverに対応している。
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres
go get -u gorm.io/driver/mysql
データベース接続の設定
// pkg/database/db.go
package database
import (
"fmt"
"log"
"os"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
type Config struct {
Host string
Port string
User string
Password string
DBName string
SSLMode string
TimeZone string
}
func NewConfig() *Config {
return &Config{
Host: getEnv("DB_HOST", "localhost"),
Port: getEnv("DB_PORT", "5432"),
User: getEnv("DB_USER", "postgres"),
Password: getEnv("DB_PASSWORD", ""),
DBName: getEnv("DB_NAME", "myapp"),
SSLMode: getEnv("DB_SSL_MODE", "disable"),
TimeZone: getEnv("DB_TIMEZONE", "Asia/Tokyo"),
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func Connect(cfg *Config) (*gorm.DB, error) {
dsn := fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=%s",
cfg.Host, cfg.User, cfg.Password, cfg.DBName, cfg.Port, cfg.SSLMode, cfg.TimeZone,
)
// GORMのログ設定
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Info,
IgnoreRecordNotFoundError: true,
Colorful: true,
},
)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
return nil, fmt.Errorf("データベース接続エラー: %w", err)
}
// コネクションプールの設定
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
return db, nil
}
モデルの定義
// internal/model/user.go
package model
import (
"time"
"gorm.io/gorm"
)
type User struct {
gorm.Model // ID, CreatedAt, UpdatedAt, DeletedAt を自動管理
Name string `gorm:"type:varchar(100);not null" json:"name"`
Email string `gorm:"type:varchar(255);uniqueIndex;not null" json:"email"`
Password string `gorm:"type:varchar(255);not null" json:"-"` // JSONには含めない
Role string `gorm:"type:varchar(50);default:'user'" json:"role"`
IsActive bool `gorm:"default:true" json:"is_active"`
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
Profile Profile `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"profile,omitempty"`
Posts []Post `json:"posts,omitempty"`
}
type Profile struct {
gorm.Model
UserID uint `gorm:"not null;uniqueIndex" json:"user_id"`
Bio string `gorm:"type:text" json:"bio"`
AvatarURL string `gorm:"type:varchar(500)" json:"avatar_url"`
Website string `gorm:"type:varchar(255)" json:"website"`
Location string `gorm:"type:varchar(100)" json:"location"`
}
type Post struct {
gorm.Model
UserID uint `gorm:"not null;index" json:"user_id"`
Title string `gorm:"type:varchar(255);not null" json:"title"`
Content string `gorm:"type:text;not null" json:"content"`
Published bool `gorm:"default:false" json:"published"`
Tags []Tag `gorm:"many2many:post_tags;" json:"tags,omitempty"`
User User `json:"user,omitempty"`
}
type Tag struct {
gorm.Model
Name string `gorm:"type:varchar(50);uniqueIndex;not null" json:"name"`
Slug string `gorm:"type:varchar(50);uniqueIndex;not null" json:"slug"`
Posts []Post `gorm:"many2many:post_tags;" json:"posts,omitempty"`
}
リポジトリパターンの実装
// internal/repository/user_repository.go
package repository
import (
"errors"
"fmt"
"github.com/yourusername/mywebapp/internal/model"
"gorm.io/gorm"
)
type UserRepository interface {
Create(user *model.User) error
FindByID(id uint) (*model.User, error)
FindByEmail(email string) (*model.User, error)
FindAll(page, limit int, search string) ([]model.User, int64, error)
Update(user *model.User) error
Delete(id uint) error
SoftDelete(id uint) error
}
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) Create(user *model.User) error {
result := r.db.Create(user)
if result.Error != nil {
return fmt.Errorf("ユーザー作成エラー: %w", result.Error)
}
return nil
}
func (r *userRepository) FindByID(id uint) (*model.User, error) {
var user model.User
result := r.db.Preload("Profile").Preload("Posts").First(&user, id)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("ユーザーが見つかりません: id=%d", id)
}
return nil, fmt.Errorf("ユーザー取得エラー: %w", result.Error)
}
return &user, nil
}
func (r *userRepository) FindByEmail(email string) (*model.User, error) {
var user model.User
result := r.db.Where("email = ?", email).First(&user)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("ユーザーが見つかりません: email=%s", email)
}
return nil, result.Error
}
return &user, nil
}
func (r *userRepository) FindAll(page, limit int, search string) ([]model.User, int64, error) {
var users []model.User
var total int64
query := r.db.Model(&model.User{})
if search != "" {
query = query.Where("name LIKE ? OR email LIKE ?",
"%"+search+"%",
"%"+search+"%",
)
}
// 総件数を取得
query.Count(&total)
// ページネーション
offset := (page - 1) * limit
result := query.Offset(offset).Limit(limit).Preload("Profile").Find(&users)
if result.Error != nil {
return nil, 0, fmt.Errorf("ユーザー一覧取得エラー: %w", result.Error)
}
return users, total, nil
}
func (r *userRepository) Update(user *model.User) error {
result := r.db.Save(user)
if result.Error != nil {
return fmt.Errorf("ユーザー更新エラー: %w", result.Error)
}
return nil
}
func (r *userRepository) Delete(id uint) error {
result := r.db.Unscoped().Delete(&model.User{}, id) // 物理削除
if result.Error != nil {
return fmt.Errorf("ユーザー削除エラー: %w", result.Error)
}
return nil
}
func (r *userRepository) SoftDelete(id uint) error {
result := r.db.Delete(&model.User{}, id) // 論理削除(DeletedAtに日時をセット)
if result.Error != nil {
return fmt.Errorf("ユーザー削除エラー: %w", result.Error)
}
return nil
}
マイグレーション
// pkg/database/migrate.go
package database
import (
"github.com/yourusername/mywebapp/internal/model"
"gorm.io/gorm"
)
func AutoMigrate(db *gorm.DB) error {
return db.AutoMigrate(
&model.User{},
&model.Profile{},
&model.Post{},
&model.Tag{},
)
}
7. JWT認証の実装
JWTとは
JSON Web Token(JWT)は、JSONオブジェクトとして情報を安全に伝達するためのオープン標準(RFC 7519)だ。Header・Payload・Signatureの3部分から構成され、サーバーはトークンを検証するだけで認証を完了できるため、ステートレスなAPI認証に適している。
go get -u github.com/golang-jwt/jwt/v5
go get -u golang.org/x/crypto
JWT認証の完全実装
// internal/service/auth_service.go
package service
import (
"errors"
"fmt"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/yourusername/mywebapp/internal/model"
"github.com/yourusername/mywebapp/internal/repository"
"golang.org/x/crypto/bcrypt"
)
type Claims struct {
UserID uint `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
jwt.RegisteredClaims
}
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt int64 `json:"expires_at"`
}
type AuthService struct {
userRepo repository.UserRepository
jwtSecret []byte
jwtRefreshKey []byte
}
func NewAuthService(userRepo repository.UserRepository) *AuthService {
return &AuthService{
userRepo: userRepo,
jwtSecret: []byte(os.Getenv("JWT_SECRET")),
jwtRefreshKey: []byte(os.Getenv("JWT_REFRESH_SECRET")),
}
}
// パスワードのハッシュ化
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("パスワードハッシュ化エラー: %w", err)
}
return string(bytes), nil
}
// パスワード検証
func CheckPasswordHash(password, hash string) bool {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
}
// アクセストークンの生成
func (s *AuthService) GenerateAccessToken(user *model.User) (string, error) {
expiresAt := time.Now().Add(15 * time.Minute)
claims := &Claims{
UserID: user.ID,
Email: user.Email,
Role: user.Role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "mywebapp",
Subject: fmt.Sprintf("%d", user.ID),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(s.jwtSecret)
if err != nil {
return "", fmt.Errorf("トークン生成エラー: %w", err)
}
return tokenString, nil
}
// リフレッシュトークンの生成
func (s *AuthService) GenerateRefreshToken(user *model.User) (string, error) {
expiresAt := time.Now().Add(7 * 24 * time.Hour) // 7日間有効
claims := &Claims{
UserID: user.ID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "mywebapp",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.jwtRefreshKey)
}
// トークン検証
func (s *AuthService) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("予期しない署名方式: %v", token.Header["alg"])
}
return s.jwtSecret, nil
})
if err != nil {
return nil, fmt.Errorf("トークン検証エラー: %w", err)
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, errors.New("無効なトークン")
}
return claims, nil
}
// ログイン処理
func (s *AuthService) Login(email, password string) (*TokenPair, error) {
user, err := s.userRepo.FindByEmail(email)
if err != nil {
return nil, errors.New("メールアドレスまたはパスワードが正しくありません")
}
if !CheckPasswordHash(password, user.Password) {
return nil, errors.New("メールアドレスまたはパスワードが正しくありません")
}
accessToken, err := s.GenerateAccessToken(user)
if err != nil {
return nil, err
}
refreshToken, err := s.GenerateRefreshToken(user)
if err != nil {
return nil, err
}
return &TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(),
}, nil
}
// ユーザー登録
func (s *AuthService) Register(name, email, password string) (*model.User, error) {
// メールアドレスの重複確認
existing, _ := s.userRepo.FindByEmail(email)
if existing != nil {
return nil, errors.New("このメールアドレスは既に登録されています")
}
hashedPassword, err := HashPassword(password)
if err != nil {
return nil, err
}
user := &model.User{
Name: name,
Email: email,
Password: hashedPassword,
Role: "user",
IsActive: true,
}
if err := s.userRepo.Create(user); err != nil {
return nil, err
}
return user, nil
}
JWT認証ミドルウェア(Gin)
// internal/middleware/auth.go
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/yourusername/mywebapp/internal/service"
)
func JWTAuth(authService *service.AuthService) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "認証トークンが必要です"})
c.Abort()
return
}
// "Bearer <token>" 形式を検証
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "トークン形式が不正です"})
c.Abort()
return
}
claims, err := authService.ValidateToken(parts[1])
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "トークンが無効または期限切れです"})
c.Abort()
return
}
// クレームをコンテキストに保存
c.Set("user_id", claims.UserID)
c.Set("user_email", claims.Email)
c.Set("user_role", claims.Role)
c.Next()
}
}
// ロールベースのアクセス制御
func RequireRole(roles ...string) gin.HandlerFunc {
return func(c *gin.Context) {
userRole, exists := c.Get("user_role")
if !exists {
c.JSON(http.StatusForbidden, gin.H{"error": "権限がありません"})
c.Abort()
return
}
role := userRole.(string)
for _, r := range roles {
if r == role {
c.Next()
return
}
}
c.JSON(http.StatusForbidden, gin.H{"error": "この操作を行う権限がありません"})
c.Abort()
}
}
8. REST API開発(CRUD完全実装)
サービス層の実装
// internal/service/post_service.go
package service
import (
"fmt"
"strings"
"unicode"
"github.com/yourusername/mywebapp/internal/model"
"github.com/yourusername/mywebapp/internal/repository"
)
type CreatePostInput struct {
Title string `json:"title" binding:"required,min=5,max=255"`
Content string `json:"content" binding:"required,min=10"`
Tags []string `json:"tags"`
Published bool `json:"published"`
}
type UpdatePostInput struct {
Title *string `json:"title"`
Content *string `json:"content"`
Tags []string `json:"tags"`
Published *bool `json:"published"`
}
type PostService struct {
postRepo repository.PostRepository
tagRepo repository.TagRepository
}
func NewPostService(postRepo repository.PostRepository, tagRepo repository.TagRepository) *PostService {
return &PostService{
postRepo: postRepo,
tagRepo: tagRepo,
}
}
// スラッグの自動生成
func generateSlug(title string) string {
slug := strings.ToLower(title)
var result []rune
for _, r := range slug {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
result = append(result, r)
} else if r == ' ' || r == '-' {
result = append(result, '-')
}
}
return strings.Trim(string(result), "-")
}
func (s *PostService) CreatePost(userID uint, input CreatePostInput) (*model.Post, error) {
// タグの取得または作成
var tags []model.Tag
for _, tagName := range input.Tags {
tag, err := s.tagRepo.FindOrCreate(tagName, generateSlug(tagName))
if err != nil {
return nil, fmt.Errorf("タグ処理エラー: %w", err)
}
tags = append(tags, *tag)
}
post := &model.Post{
UserID: userID,
Title: input.Title,
Content: input.Content,
Published: input.Published,
Tags: tags,
}
if err := s.postRepo.Create(post); err != nil {
return nil, err
}
return post, nil
}
func (s *PostService) GetPost(id uint) (*model.Post, error) {
return s.postRepo.FindByID(id)
}
func (s *PostService) ListPosts(page, limit int, published *bool) ([]model.Post, int64, error) {
return s.postRepo.FindAll(page, limit, published)
}
func (s *PostService) UpdatePost(id, userID uint, input UpdatePostInput) (*model.Post, error) {
post, err := s.postRepo.FindByID(id)
if err != nil {
return nil, err
}
// 所有者確認
if post.UserID != userID {
return nil, fmt.Errorf("この記事を編集する権限がありません")
}
if input.Title != nil {
post.Title = *input.Title
}
if input.Content != nil {
post.Content = *input.Content
}
if input.Published != nil {
post.Published = *input.Published
}
if err := s.postRepo.Update(post); err != nil {
return nil, err
}
return post, nil
}
func (s *PostService) DeletePost(id, userID uint, isAdmin bool) error {
post, err := s.postRepo.FindByID(id)
if err != nil {
return err
}
if !isAdmin && post.UserID != userID {
return fmt.Errorf("この記事を削除する権限がありません")
}
return s.postRepo.SoftDelete(id)
}
ハンドラー層の実装
// internal/handler/post_handler.go
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/yourusername/mywebapp/internal/service"
)
type PostHandler struct {
postService *service.PostService
}
func NewPostHandler(postService *service.PostService) *PostHandler {
return &PostHandler{postService: postService}
}
func (h *PostHandler) ListPosts(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
var published *bool
if p := c.Query("published"); p != "" {
b := p == "true"
published = &b
}
if page < 1 {
page = 1
}
if limit < 1 || limit > 100 {
limit = 10
}
posts, total, err := h.postService.ListPosts(page, limit, published)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
totalPages := (int(total) + limit - 1) / limit
c.JSON(http.StatusOK, gin.H{
"posts": posts,
"total": total,
"page": page,
"limit": limit,
"total_pages": totalPages,
})
}
func (h *PostHandler) GetPost(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "無効なIDです"})
return
}
post, err := h.postService.GetPost(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "記事が見つかりません"})
return
}
c.JSON(http.StatusOK, post)
}
func (h *PostHandler) CreatePost(c *gin.Context) {
userID, _ := c.Get("user_id")
var input service.CreatePostInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "入力データが不正です",
"details": err.Error(),
})
return
}
post, err := h.postService.CreatePost(userID.(uint), input)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, post)
}
func (h *PostHandler) UpdatePost(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "無効なIDです"})
return
}
userID, _ := c.Get("user_id")
userRole, _ := c.Get("user_role")
var input service.UpdatePostInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
_ = userRole
post, err := h.postService.UpdatePost(uint(id), userID.(uint), input)
if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, post)
}
func (h *PostHandler) DeletePost(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "無効なIDです"})
return
}
userID, _ := c.Get("user_id")
userRole, _ := c.Get("user_role")
isAdmin := userRole.(string) == "admin"
if err := h.postService.DeletePost(uint(id), userID.(uint), isAdmin); err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "記事を削除しました"})
}
func (h *PostHandler) RegisterRoutes(rg *gin.RouterGroup, authMW gin.HandlerFunc) {
posts := rg.Group("/posts")
posts.GET("", h.ListPosts)
posts.GET("/:id", h.GetPost)
posts.Use(authMW)
{
posts.POST("", h.CreatePost)
posts.PUT("/:id", h.UpdatePost)
posts.DELETE("/:id", h.DeletePost)
}
}
9. WebSocket実装
WebSocketによるリアルタイム通信
GoはWebSocketのサポートが優れており、gorilla/websocketが広く使われている。
go get github.com/gorilla/websocket
チャットサーバーの実装
// internal/handler/websocket_handler.go
package handler
import (
"encoding/json"
"log"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
// WebSocketのアップグレード設定
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
// 本番環境では適切なオリジン確認を行う
return true
},
}
// メッセージの型定義
type Message struct {
Type string `json:"type"`
Content string `json:"content"`
UserID string `json:"user_id"`
Username string `json:"username"`
Room string `json:"room"`
Timestamp time.Time `json:"timestamp"`
}
// クライアントの管理
type Client struct {
ID string
Username string
Room string
Conn *websocket.Conn
Send chan Message
Hub *Hub
}
// ハブ(接続管理センター)
type Hub struct {
mu sync.RWMutex
clients map[string]*Client
rooms map[string]map[string]*Client
broadcast chan Message
register chan *Client
unregister chan *Client
}
func NewHub() *Hub {
return &Hub{
clients: make(map[string]*Client),
rooms: make(map[string]map[string]*Client),
broadcast: make(chan Message, 256),
register: make(chan *Client),
unregister: make(chan *Client),
}
}
// ハブの実行(goroutineで起動)
func (h *Hub) Run() {
for {
select {
case client := <-h.register:
h.mu.Lock()
h.clients[client.ID] = client
if h.rooms[client.Room] == nil {
h.rooms[client.Room] = make(map[string]*Client)
}
h.rooms[client.Room][client.ID] = client
h.mu.Unlock()
// 入室メッセージをブロードキャスト
h.broadcast <- Message{
Type: "system",
Content: client.Username + " が入室しました",
Room: client.Room,
Timestamp: time.Now(),
}
case client := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[client.ID]; ok {
delete(h.clients, client.ID)
if room, ok := h.rooms[client.Room]; ok {
delete(room, client.ID)
if len(room) == 0 {
delete(h.rooms, client.Room)
}
}
close(client.Send)
}
h.mu.Unlock()
h.broadcast <- Message{
Type: "system",
Content: client.Username + " が退室しました",
Room: client.Room,
Timestamp: time.Now(),
}
case message := <-h.broadcast:
h.mu.RLock()
room := h.rooms[message.Room]
h.mu.RUnlock()
for _, client := range room {
select {
case client.Send <- message:
default:
close(client.Send)
h.mu.Lock()
delete(h.clients, client.ID)
delete(h.rooms[client.Room], client.ID)
h.mu.Unlock()
}
}
}
}
}
// クライアントのメッセージ読み取り
func (c *Client) ReadPump() {
defer func() {
c.Hub.unregister <- c
c.Conn.Close()
}()
c.Conn.SetReadLimit(512 * 1024) // 512KB
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
c.Conn.SetPongHandler(func(string) error {
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
for {
_, rawMessage, err := c.Conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("WebSocketエラー: %v", err)
}
break
}
var msg Message
if err := json.Unmarshal(rawMessage, &msg); err != nil {
log.Printf("メッセージのパースエラー: %v", err)
continue
}
msg.UserID = c.ID
msg.Username = c.Username
msg.Room = c.Room
msg.Timestamp = time.Now()
c.Hub.broadcast <- msg
}
}
// クライアントへのメッセージ書き込み
func (c *Client) WritePump() {
ticker := time.NewTicker(54 * time.Second)
defer func() {
ticker.Stop()
c.Conn.Close()
}()
for {
select {
case message, ok := <-c.Send:
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if !ok {
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
data, err := json.Marshal(message)
if err != nil {
log.Printf("メッセージのシリアライズエラー: %v", err)
continue
}
if err := c.Conn.WriteMessage(websocket.TextMessage, data); err != nil {
return
}
case <-ticker.C:
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
// WebSocketハンドラー
type WSHandler struct {
hub *Hub
}
func NewWSHandler(hub *Hub) *WSHandler {
return &WSHandler{hub: hub}
}
func (h *WSHandler) HandleWebSocket(c *gin.Context) {
userID := c.Query("user_id")
username := c.Query("username")
room := c.DefaultQuery("room", "general")
if userID == "" || username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "user_idとusernameが必要です"})
return
}
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("WebSocketアップグレードエラー: %v", err)
return
}
client := &Client{
ID: userID,
Username: username,
Room: room,
Conn: conn,
Send: make(chan Message, 256),
Hub: h.hub,
}
h.hub.register <- client
// 読み書きをgoroutineで並行実行
go client.WritePump()
go client.ReadPump()
}
10. GoroutineとGoの並行処理
Goroutineの基礎
Goroutineはgoキーワードで起動する軽量な並行処理単位だ。OSスレッドとは異なり、GoランタイムがM:Nモデルでスケジューリングを管理する。
package main
import (
"fmt"
"sync"
"time"
)
// 基本的なGoroutine
func basicGoroutine() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Goroutine %d が実行中\n", id)
time.Sleep(time.Millisecond * 100)
}(i)
}
wg.Wait()
fmt.Println("全Goroutineが完了")
}
// チャネルを使った通信
func channelExample() {
jobs := make(chan int, 10)
results := make(chan int, 10)
// ワーカーを3つ起動
for w := 0; w < 3; w++ {
go func() {
for job := range jobs {
result := job * job // 2乗を計算
results <- result
}
}()
}
// ジョブを投入
for i := 1; i <= 9; i++ {
jobs <- i
}
close(jobs)
// 結果を収集
for i := 0; i < 9; i++ {
fmt.Printf("結果: %d\n", <-results)
}
}
// selectによる複数チャネルの待機
func selectExample() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "ch1からのメッセージ"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "ch2からのメッセージ"
}()
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
}
}
}
並列HTTPリクエスト処理
// 複数の外部APIを並列で呼び出す例
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
)
type APIResult struct {
URL string
Data interface{}
Error error
Latency time.Duration
}
func fetchAPI(ctx context.Context, url string) APIResult {
start := time.Now()
result := APIResult{URL: url}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
result.Error = err
return result
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
result.Error = err
result.Latency = time.Since(start)
return result
}
defer resp.Body.Close()
var data interface{}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
result.Error = err
} else {
result.Data = data
}
result.Latency = time.Since(start)
return result
}
func fetchAPIsParallel(urls []string) []APIResult {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
results := make([]APIResult, len(urls))
var wg sync.WaitGroup
for i, url := range urls {
wg.Add(1)
go func(index int, u string) {
defer wg.Done()
results[index] = fetchAPI(ctx, u)
}(i, url)
}
wg.Wait()
return results
}
// Worker Poolパターン
type WorkerPool struct {
numWorkers int
jobs chan func()
wg sync.WaitGroup
}
func NewWorkerPool(numWorkers int) *WorkerPool {
pool := &WorkerPool{
numWorkers: numWorkers,
jobs: make(chan func(), numWorkers*10),
}
for i := 0; i < numWorkers; i++ {
go pool.worker()
}
return pool
}
func (p *WorkerPool) worker() {
for job := range p.jobs {
job()
p.wg.Done()
}
}
func (p *WorkerPool) Submit(job func()) {
p.wg.Add(1)
p.jobs <- job
}
func (p *WorkerPool) Wait() {
p.wg.Wait()
}
func (p *WorkerPool) Stop() {
close(p.jobs)
}
func main() {
pool := NewWorkerPool(5)
defer pool.Stop()
for i := 0; i < 20; i++ {
id := i
pool.Submit(func() {
fmt.Printf("タスク %d を処理中\n", id)
time.Sleep(time.Millisecond * 100)
})
}
pool.Wait()
fmt.Println("全タスク完了")
}
contextを使ったキャンセル処理
// internal/service/background_service.go
package service
import (
"context"
"log"
"time"
)
type BackgroundService struct {
ticker *time.Ticker
done chan struct{}
}
func NewBackgroundService(interval time.Duration) *BackgroundService {
return &BackgroundService{
ticker: time.NewTicker(interval),
done: make(chan struct{}),
}
}
func (s *BackgroundService) Start(ctx context.Context) {
go func() {
for {
select {
case <-s.ticker.C:
if err := s.process(ctx); err != nil {
log.Printf("バックグラウンド処理エラー: %v", err)
}
case <-ctx.Done():
log.Println("バックグラウンドサービスを停止します")
s.ticker.Stop()
return
case <-s.done:
s.ticker.Stop()
return
}
}
}()
}
func (s *BackgroundService) process(ctx context.Context) error {
// コンテキストのキャンセルを確認しながら処理
select {
case <-ctx.Done():
return ctx.Err()
default:
}
log.Println("定期処理を実行中...")
// 実際の処理をここに記述
return nil
}
func (s *BackgroundService) Stop() {
close(s.done)
}
11. テスト
テストの基本
Goにはtestingパッケージが標準で含まれており、追加ライブラリなしでテストを書ける。testifyはアサーションを豊かにする人気ライブラリだ。
go get github.com/stretchr/testify/assert
go get github.com/stretchr/testify/mock
go get github.com/stretchr/testify/require
ユニットテスト
// internal/service/auth_service_test.go
package service_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yourusername/mywebapp/internal/service"
)
func TestHashPassword(t *testing.T) {
tests := []struct {
name string
password string
wantErr bool
}{
{
name: "正常なパスワード",
password: "securePassword123",
wantErr: false,
},
{
name: "短いパスワード",
password: "abc",
wantErr: false, // bcryptは短いパスワードも受け付ける
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hash, err := service.HashPassword(tt.password)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.NotEmpty(t, hash)
assert.NotEqual(t, tt.password, hash)
})
}
}
func TestCheckPasswordHash(t *testing.T) {
password := "mySecretPassword"
hash, err := service.HashPassword(password)
require.NoError(t, err)
assert.True(t, service.CheckPasswordHash(password, hash), "正しいパスワードは一致するはず")
assert.False(t, service.CheckPasswordHash("wrongPassword", hash), "誤ったパスワードは不一致のはず")
}
モックを使ったテスト
// internal/service/post_service_test.go
package service_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/yourusername/mywebapp/internal/model"
"github.com/yourusername/mywebapp/internal/service"
)
// PostRepositoryのモック
type MockPostRepository struct {
mock.Mock
}
func (m *MockPostRepository) Create(post *model.Post) error {
args := m.Called(post)
return args.Error(0)
}
func (m *MockPostRepository) FindByID(id uint) (*model.Post, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*model.Post), args.Error(1)
}
func (m *MockPostRepository) FindAll(page, limit int, published *bool) ([]model.Post, int64, error) {
args := m.Called(page, limit, published)
return args.Get(0).([]model.Post), args.Get(1).(int64), args.Error(2)
}
func (m *MockPostRepository) Update(post *model.Post) error {
args := m.Called(post)
return args.Error(0)
}
func (m *MockPostRepository) SoftDelete(id uint) error {
args := m.Called(id)
return args.Error(0)
}
// TagRepositoryのモック
type MockTagRepository struct {
mock.Mock
}
func (m *MockTagRepository) FindOrCreate(name, slug string) (*model.Tag, error) {
args := m.Called(name, slug)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*model.Tag), args.Error(1)
}
func TestCreatePost(t *testing.T) {
mockPostRepo := new(MockPostRepository)
mockTagRepo := new(MockTagRepository)
// モックの期待値を設定
mockTagRepo.On("FindOrCreate", "Go", mock.AnythingOfType("string")).
Return(&model.Tag{ID: 1, Name: "Go", Slug: "go"}, nil)
mockPostRepo.On("Create", mock.AnythingOfType("*model.Post")).
Return(nil)
postService := service.NewPostService(mockPostRepo, mockTagRepo)
input := service.CreatePostInput{
Title: "Go言語入門",
Content: "Goは素晴らしい言語です。この記事では基礎から解説します。",
Tags: []string{"Go"},
Published: true,
}
post, err := postService.CreatePost(1, input)
require.NoError(t, err)
assert.NotNil(t, post)
assert.Equal(t, "Go言語入門", post.Title)
assert.Equal(t, uint(1), post.UserID)
// モックが期待どおりに呼ばれたか確認
mockPostRepo.AssertExpectations(t)
mockTagRepo.AssertExpectations(t)
}
func TestDeletePost_Unauthorized(t *testing.T) {
mockPostRepo := new(MockPostRepository)
mockTagRepo := new(MockTagRepository)
existingPost := &model.Post{
UserID: 2, // 別のユーザーの記事
Title: "他のユーザーの記事",
Content: "内容",
}
existingPost.ID = 1
mockPostRepo.On("FindByID", uint(1)).Return(existingPost, nil)
postService := service.NewPostService(mockPostRepo, mockTagRepo)
err := postService.DeletePost(1, 1, false) // userID=1、管理者ではない
assert.Error(t, err)
assert.Contains(t, err.Error(), "権限がありません")
mockPostRepo.AssertExpectations(t)
}
HTTPハンドラーのテスト
// internal/handler/post_handler_test.go
package handler_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupTestRouter() *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
return r
}
func TestListPostsHandler(t *testing.T) {
r := setupTestRouter()
r.GET("/api/v1/posts", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"posts": []interface{}{},
"total": 0,
})
})
req, err := http.NewRequest(http.MethodGet, "/api/v1/posts", nil)
require.NoError(t, err)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Contains(t, response, "posts")
assert.Contains(t, response, "total")
}
func TestCreatePostHandler_Unauthorized(t *testing.T) {
r := setupTestRouter()
r.POST("/api/v1/posts", func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "認証が必要です"})
return
}
c.JSON(http.StatusCreated, gin.H{"id": 1})
})
body := strings.NewReader(`{"title":"テスト記事","content":"内容"}`)
req, err := http.NewRequest(http.MethodPost, "/api/v1/posts", body)
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
// ベンチマークテスト
func BenchmarkListPostsHandler(b *testing.B) {
r := setupTestRouter()
r.GET("/api/v1/posts", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"posts": []interface{}{}})
})
b.ResetTimer()
for i := 0; i < b.N; i++ {
req, _ := http.NewRequest(http.MethodGet, "/api/v1/posts", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
}
}
テストの実行
# 全テストを実行
go test ./...
# 特定パッケージのテスト
go test ./internal/service/...
# 詳細出力
go test -v ./...
# カバレッジレポート
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# 特定のテストのみ実行
go test -run TestCreatePost ./internal/service/...
# ベンチマーク実行
go test -bench=. -benchmem ./...
# レース条件の検出
go test -race ./...
12. Dockerによるコンテナ化
マルチステージビルドDockerfile
# Dockerfile
# ビルドステージ
FROM golang:1.23-alpine AS builder
# 必要なツールのインストール
RUN apk add --no-cache git ca-certificates tzdata
WORKDIR /app
# 依存関係のコピーとダウンロード(レイヤーキャッシュを活用)
COPY go.mod go.sum ./
RUN go mod download
# ソースコードのコピー
COPY . .
# CGO無効でスタティックバイナリをビルド
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build \
-ldflags="-w -s -X main.version=$(git describe --tags --always) -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o /app/server \
./cmd/server
# 本番ステージ(最小イメージ)
FROM scratch
# タイムゾーンとCA証明書のコピー
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# バイナリのコピー
COPY --from=builder /app/server /server
# ポートの公開
EXPOSE 8080
# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/server", "-health-check"]
# 非rootユーザーで実行
USER 65532:65532
ENTRYPOINT ["/server"]
Docker Compose設定
# docker-compose.yml
version: '3.9'
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
environment:
- DB_HOST=postgres
- DB_PORT=5432
- DB_USER=appuser
- DB_PASSWORD=${DB_PASSWORD}
- DB_NAME=myapp
- DB_SSL_MODE=disable
- JWT_SECRET=${JWT_SECRET}
- JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET}
- REDIS_URL=redis:6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
networks:
- app-network
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_USER=appuser
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=myapp
volumes:
- postgres-data:/var/lib/postgresql/data
- ./migrations:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d myapp"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- app
networks:
- app-network
volumes:
postgres-data:
redis-data:
networks:
app-network:
driver: bridge
Makefile(開発効率化)
# Makefile
.PHONY: all build test clean docker-build docker-up docker-down lint
# 変数
APP_NAME := mywebapp
BUILD_DIR := ./bin
DOCKER_IMAGE := $(APP_NAME):latest
all: build
# ビルド
build:
@echo "ビルド中..."
CGO_ENABLED=0 go build -o $(BUILD_DIR)/$(APP_NAME) ./cmd/server
# テスト実行
test:
@echo "テスト実行中..."
go test -v -race -coverprofile=coverage.out ./...
# テストカバレッジの表示
test-coverage: test
go tool cover -html=coverage.out -o coverage.html
@echo "カバレッジレポート: coverage.html"
# ベンチマーク
bench:
go test -bench=. -benchmem ./...
# Lintチェック
lint:
golangci-lint run ./...
# ホットリロード(air使用)
dev:
air
# Dockerビルド
docker-build:
docker build -t $(DOCKER_IMAGE) .
# Docker Compose起動
docker-up:
docker-compose up -d
# Docker Compose停止
docker-down:
docker-compose down
# データベースマイグレーション
migrate-up:
go run ./cmd/migrate up
migrate-down:
go run ./cmd/migrate down
# 依存関係の整理
tidy:
go mod tidy
# クリーンアップ
clean:
rm -rf $(BUILD_DIR) coverage.out coverage.html
13. マイクロサービスアーキテクチャ
マイクロサービスの設計原則
マイクロサービスでは、各サービスが独立してデプロイ・スケールできるように設計する。GoはマイクロサービスのサイドカーパターンやAPIゲートウェイに最適だ。
// APIゲートウェイの実装例
// cmd/gateway/main.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
type ServiceConfig struct {
Name string
URL string
Timeout time.Duration
}
type Gateway struct {
services map[string]*ServiceConfig
mu sync.RWMutex
}
func NewGateway() *Gateway {
return &Gateway{
services: map[string]*ServiceConfig{
"users": {
Name: "users",
URL: getEnv("USER_SERVICE_URL", "http://user-service:8081"),
Timeout: 10 * time.Second,
},
"posts": {
Name: "posts",
URL: getEnv("POST_SERVICE_URL", "http://post-service:8082"),
Timeout: 10 * time.Second,
},
"notifications": {
Name: "notifications",
URL: getEnv("NOTIFICATION_SERVICE_URL", "http://notification-service:8083"),
Timeout: 5 * time.Second,
},
},
}
}
func getEnv(key, defaultVal string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultVal
}
// リバースプロキシハンドラーの作成
func (g *Gateway) createProxy(serviceName string) gin.HandlerFunc {
return func(c *gin.Context) {
g.mu.RLock()
svc, exists := g.services[serviceName]
g.mu.RUnlock()
if !exists {
c.JSON(http.StatusNotFound, gin.H{"error": "サービスが見つかりません"})
return
}
target, err := url.Parse(svc.URL)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "サービス設定エラー"})
return
}
proxy := httputil.NewSingleHostReverseProxy(target)
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("プロキシエラー [%s]: %v", serviceName, err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadGateway)
fmt.Fprintf(w, `{"error":"サービスが一時的に利用できません"}`)
}
// 元のパスからサービスプレフィックスを除去
c.Request.URL.Path = c.Param("path")
if c.Request.URL.Path == "" {
c.Request.URL.Path = "/"
}
proxy.ServeHTTP(c.Writer, c.Request)
}
}
func main() {
gateway := NewGateway()
r := gin.Default()
// 各サービスへのルーティング
r.Any("/api/users/*path", gateway.createProxy("users"))
r.Any("/api/posts/*path", gateway.createProxy("posts"))
r.Any("/api/notifications/*path", gateway.createProxy("notifications"))
// ヘルスチェック
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"services": len(gateway.services),
})
})
srv := &http.Server{
Addr: ":8080",
Handler: r,
}
// グレースフルシャットダウン
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("サーバー起動エラー: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("サーバーを停止しています...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("強制シャットダウン: %v", err)
}
log.Println("サーバーが正常に停止しました")
}
gRPCによるサービス間通信
go get google.golang.org/grpc
go get google.golang.org/protobuf
// proto/user.proto
syntax = "proto3";
package user;
option go_package = "github.com/yourusername/mywebapp/proto/user";
service UserService {
rpc GetUser (GetUserRequest) returns (UserResponse);
rpc CreateUser (CreateUserRequest) returns (UserResponse);
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
}
message GetUserRequest {
uint64 id = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
string password = 3;
}
message UserResponse {
uint64 id = 1;
string name = 2;
string email = 3;
string role = 4;
string created_at = 5;
}
message ListUsersRequest {
int32 page = 1;
int32 limit = 2;
string search = 3;
}
message ListUsersResponse {
repeated UserResponse users = 1;
int64 total = 2;
}
// internal/grpc/user_server.go
package grpc
import (
"context"
"fmt"
"github.com/yourusername/mywebapp/internal/repository"
pb "github.com/yourusername/mywebapp/proto/user"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type UserGRPCServer struct {
pb.UnimplementedUserServiceServer
userRepo repository.UserRepository
}
func NewUserGRPCServer(userRepo repository.UserRepository) *UserGRPCServer {
return &UserGRPCServer{userRepo: userRepo}
}
func (s *UserGRPCServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.UserResponse, error) {
if req.Id == 0 {
return nil, status.Error(codes.InvalidArgument, "IDが必要です")
}
user, err := s.userRepo.FindByID(uint(req.Id))
if err != nil {
return nil, status.Error(codes.NotFound, fmt.Sprintf("ユーザーが見つかりません: %v", err))
}
return &pb.UserResponse{
Id: uint64(user.ID),
Name: user.Name,
Email: user.Email,
Role: user.Role,
CreatedAt: user.CreatedAt.String(),
}, nil
}
func (s *UserGRPCServer) ListUsers(ctx context.Context, req *pb.ListUsersRequest) (*pb.ListUsersResponse, error) {
page := int(req.Page)
if page < 1 {
page = 1
}
limit := int(req.Limit)
if limit < 1 || limit > 100 {
limit = 10
}
users, total, err := s.userRepo.FindAll(page, limit, req.Search)
if err != nil {
return nil, status.Error(codes.Internal, fmt.Sprintf("ユーザー一覧取得エラー: %v", err))
}
var pbUsers []*pb.UserResponse
for _, u := range users {
pbUsers = append(pbUsers, &pb.UserResponse{
Id: uint64(u.ID),
Name: u.Name,
Email: u.Email,
Role: u.Role,
})
}
return &pb.ListUsersResponse{
Users: pbUsers,
Total: total,
}, nil
}
14. パフォーマンス最適化とベンチマーク
プロファイリング
GoにはPProfという標準的なプロファイリングツールが含まれている。
// cmd/server/main.go(プロファイリング有効化)
package main
import (
"log"
"net/http"
_ "net/http/pprof" // プロファイラーを有効化
"time"
"github.com/gin-gonic/gin"
)
func main() {
// プロファイリングエンドポイントを別ポートで起動(本番では認証が必要)
go func() {
log.Println("PProfサーバーを起動: http://localhost:6060/debug/pprof/")
if err := http.ListenAndServe(":6060", nil); err != nil {
log.Printf("PProfサーバーエラー: %v", err)
}
}()
r := gin.Default()
// ... ルーティング設定
if err := r.Run(":8080"); err != nil {
log.Fatal(err)
}
}
# CPUプロファイルの取得(30秒間)
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# メモリプロファイルの取得
go tool pprof http://localhost:6060/debug/pprof/heap
# goroutineの一覧
curl http://localhost:6060/debug/pprof/goroutine?debug=1
メモリ最適化
// pkg/pool/buffer_pool.go
package pool
import (
"bytes"
"sync"
)
// sync.Poolを使ったバッファプール
var BufferPool = &sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
// バッファを取得
func GetBuffer() *bytes.Buffer {
buf := BufferPool.Get().(*bytes.Buffer)
buf.Reset()
return buf
}
// バッファを返却
func PutBuffer(buf *bytes.Buffer) {
if buf.Cap() > 1024*1024 { // 1MB超のバッファは返却しない
return
}
BufferPool.Put(buf)
}
// 使用例
func processRequest(data []byte) []byte {
buf := GetBuffer()
defer PutBuffer(buf)
buf.Write(data)
// 処理...
result := make([]byte, buf.Len())
copy(result, buf.Bytes())
return result
}
キャッシュの実装
// pkg/cache/cache.go
package cache
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
type Cache struct {
client *redis.Client
prefix string
}
func NewCache(addr, password string, db int, prefix string) *Cache {
rdb := redis.NewClient(&redis.Options{
Addr: addr,
Password: password,
DB: db,
PoolSize: 10,
MinIdleConns: 5,
})
return &Cache{
client: rdb,
prefix: prefix,
}
}
func (c *Cache) key(k string) string {
return fmt.Sprintf("%s:%s", c.prefix, k)
}
// データの保存
func (c *Cache) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
data, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("キャッシュシリアライズエラー: %w", err)
}
return c.client.Set(ctx, c.key(key), data, ttl).Err()
}
// データの取得
func (c *Cache) Get(ctx context.Context, key string, dest interface{}) error {
data, err := c.client.Get(ctx, c.key(key)).Bytes()
if err != nil {
if err == redis.Nil {
return fmt.Errorf("キャッシュミス: %s", key)
}
return fmt.Errorf("キャッシュ取得エラー: %w", err)
}
return json.Unmarshal(data, dest)
}
// データの削除
func (c *Cache) Delete(ctx context.Context, keys ...string) error {
fullKeys := make([]string, len(keys))
for i, k := range keys {
fullKeys[i] = c.key(k)
}
return c.client.Del(ctx, fullKeys...).Err()
}
// キャッシュを使ったリポジトリラッパー
type CachedUserRepository struct {
repo repository.UserRepository
cache *Cache
ttl time.Duration
}
func NewCachedUserRepository(repo repository.UserRepository, cache *Cache, ttl time.Duration) *CachedUserRepository {
return &CachedUserRepository{
repo: repo,
cache: cache,
ttl: ttl,
}
}
func (r *CachedUserRepository) FindByID(id uint) (*model.User, error) {
ctx := context.Background()
cacheKey := fmt.Sprintf("user:%d", id)
var user model.User
if err := r.cache.Get(ctx, cacheKey, &user); err == nil {
return &user, nil // キャッシュヒット
}
// キャッシュミス:DBから取得
dbUser, err := r.repo.FindByID(id)
if err != nil {
return nil, err
}
// キャッシュに保存
if err := r.cache.Set(ctx, cacheKey, dbUser, r.ttl); err != nil {
// キャッシュ保存失敗はログに記録するが、エラーは返さない
fmt.Printf("キャッシュ保存エラー: %v\n", err)
}
return dbUser, nil
}
ベンチマークテスト
// pkg/cache/cache_benchmark_test.go
package cache_test
import (
"context"
"fmt"
"testing"
)
func BenchmarkCacheSet(b *testing.B) {
cache := NewCache("localhost:6379", "", 0, "bench")
ctx := context.Background()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
key := fmt.Sprintf("key-%d", i)
cache.Set(ctx, key, map[string]string{"data": "value"}, 60*time.Second)
i++
}
})
}
func BenchmarkCacheGet(b *testing.B) {
cache := NewCache("localhost:6379", "", 0, "bench")
ctx := context.Background()
// 事前にデータを投入
for i := 0; i < 1000; i++ {
key := fmt.Sprintf("key-%d", i)
cache.Set(ctx, key, map[string]string{"data": "value"}, 60*time.Second)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
key := fmt.Sprintf("key-%d", i%1000)
var result map[string]string
cache.Get(ctx, key, &result)
i++
}
})
}
パフォーマンスチューニングのポイント
1. コネクションプールの適切な設定
sqlDB, err := db.DB()
sqlDB.SetMaxIdleConns(25) // アイドル接続数
sqlDB.SetMaxOpenConns(100) // 最大接続数
sqlDB.SetConnMaxLifetime(5 * time.Minute) // 接続の最大寿命
2. HTTPクライアントの再利用
// BAD: リクエストごとに新しいクライアントを作成
func fetchData() {
client := &http.Client{} // 毎回作成はコスト高
// ...
}
// GOOD: 共有クライアントを使用
var httpClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxConnsPerHost: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
},
}
3. JSONエンコードの最適化
// 大量データのストリーミングエンコード
func streamJSONResponse(w http.ResponseWriter, data []interface{}) {
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
encoder.SetEscapeHTML(false) // HTMLエスケープ不要な場合は無効化
encoder.Encode(data)
}
4. StringBuilderの活用
// BAD: 文字列の連結(毎回メモリ確保が発生)
result := ""
for i := 0; i < 1000; i++ {
result += fmt.Sprintf("item-%d,", i)
}
// GOOD: strings.Builderを使用
var sb strings.Builder
sb.Grow(10000) // 事前に容量確保
for i := 0; i < 1000; i++ {
fmt.Fprintf(&sb, "item-%d,", i)
}
result := sb.String()
関連記事
まとめ
本記事では、Go言語によるWeb開発の全体像を解説した。
学んだ主なポイント:
net/http標準パッケージだけでも実用的なWebサーバーを構築できる- GinとEchoはそれぞれ異なるトレードオフを持つ優れたフレームワークだ
- GORMとリポジトリパターンを組み合わせることで、保守性の高いデータ層を実現できる
- JWT認証はステートレスなAPI認証に最適で、アクセストークンとリフレッシュトークンの組み合わせが鉄板だ
- GoroutineとチャネルはGoの並行処理の核心であり、Worker Poolパターンで効率よくリソースを管理できる
- テストはユニット・統合・ベンチマークを組み合わせ、
go test -raceでレース条件も検出する - Dockerのマルチステージビルドで、小さく安全なコンテナイメージを作成できる
- マイクロサービスではgRPCがサービス間通信の強力な選択肢となる
- PProfとRedisキャッシュでパフォーマンスを大幅に改善できる
Goは学習コストが低く、習得後の生産性が高い言語だ。特にAPI開発・マイクロサービス・CLIツールの分野では、今後もその存在感を増していくだろう。
開発効率をさらに高めるツール
Go開発の生産性を上げるには、適切なツールセットが欠かせない。DevToolBox は、Web開発者向けのオールインワンツールプラットフォームだ。JSON整形・Base64エンコード・JWT デコード・正規表現テスター・カラーピッカー・タイムスタンプ変換など、開発中に繰り返し使うユーティリティが一か所にまとまっている。
特にGoのAPIデバッグ時には、JWTデコーダーでトークンの中身を確認したり、JSONフォーマッターでレスポンスを整形したりする場面が多い。ブックマークしておくと日々の開発がスムーズになる。
参考リンク
- Go公式ドキュメント
- Go By Example
- Gin フレームワーク
- Echo フレームワーク
- GORM ドキュメント
- golang-jwt/jwt
- gorilla/websocket
- DevToolBox — 開発者向けツール集
スキルアップ・キャリアアップのおすすめリソース
Go言語のスキルはバックエンド・クラウドインフラ領域で非常に高く評価される。キャリアアップに活用してほしい。
転職・キャリアアップ
- レバテックキャリア — ITエンジニア専門の転職エージェント。GoエンジニアはAPI開発・マイクロサービス・インフラ領域で高単価案件が豊富。無料相談可能。
- Findy — GitHubのGoプロジェクトが評価対象。スカウト型でスタートアップ・大手Tech企業からのオファーが届きやすい。リモート求人が充実。
オンライン学習
- Udemy — Go言語の入門から応用(Gin・gRPC・マイクロサービス)まで実践コースが充実。現場で使われるパターンを体系的に習得できる。セール時は90%オフになることも。