Day 2: イディオマティックGo - 講義

今日の目標

  • Goらしいコーディングスタイルを理解する
  • 効果的な命名規則を学ぶ
  • パッケージ設計の基本を理解する
  • defer, panic, recoverの適切な使用法を学ぶ
  • コードレビューで指摘される典型的な問題を回避する

---

イディオマティックGoとは

Idiomatic Go(イディオマティックGo)は、Go言語コミュニティで広く受け入れられている「Goらしい」コードの書き方です。

なぜ重要か

  • コードの一貫性が保たれる
  • 他の開発者が読みやすい
  • バグが入りにくい
  • メンテナンスしやすい

参考資料

---

命名規則

基本原則

// 良い命名
var userCount int
var maxRetries int
var httpClient *http.Client

// 悪い命名
var user_count int      // スネークケースは使わない
var MaxRetries int      // パッケージレベルの変数で大文字はNG
var HTTPclient *http.Client  // 略語は全部大文字

パッケージ名

// 良いパッケージ名
package http
package json
package strings

// 悪いパッケージ名
package httputils     // utilsは避ける
package json_parser   // アンダースコア禁止
package myPackage     // キャメルケース禁止

ルール

  • 小文字のみ
  • 短く、明確
  • 複数形は避ける(stringではなくstringsは例外)

変数名の長さ

// スコープが小さい → 短い名前
for i := 0; i < 10; i++ {
    // i, j, k などでOK
}

// スコープが大きい → 説明的な名前
var maxConnectionRetries = 5
var defaultHTTPTimeout = 30 * time.Second

// レシーバー名は1-2文字
func (u *User) GetName() string {
    return u.Name
}

func (db *Database) Query() error {
    return db.connection.Ping()
}

インターフェース名

// 1メソッド → メソッド名 + er
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

// 複数メソッド → 適切な名前
type File interface {
    Reader
    Writer
    Closer
}

ゲッター/セッター

// 悪い例(Java風)
type Person struct {
    name string
}

func (p *Person) GetName() string {
    return p.name
}

func (p *Person) SetName(name string) {
    p.name = name
}

// 良い例(Go風)
type Person struct {
    name string
}

// ゲッターにGetは不要
func (p *Person) Name() string {
    return p.name
}

// セッターはSetを付ける
func (p *Person) SetName(name string) {
    p.name = name
}

---

エラー処理のイディオム

早期リターン

// 悪い例:ネストが深い
func processData(data []byte) error {
    if len(data) > 0 {
        if valid := validate(data); valid {
            if result := process(data); result != nil {
                return save(result)
            } else {
                return errors.New("process failed")
            }
        } else {
            return errors.New("invalid data")
        }
    } else {
        return errors.New("empty data")
    }
}

// 良い例:早期リターン
func processData(data []byte) error {
    if len(data) == 0 {
        return errors.New("empty data")
    }

    if !validate(data) {
        return errors.New("invalid data")
    }

    result := process(data)
    if result == nil {
        return errors.New("process failed")
    }

    return save(result)
}

エラーメッセージ

// 悪い例
return errors.New("Failed to connect to database!")  // 大文字、感嘆符

// 良い例
return errors.New("failed to connect to database")   // 小文字、簡潔

// さらに良い例:コンテキストを含める
return fmt.Errorf("failed to connect to database %s: %w", dbName, err)

エラーチェックのパターン

// 基本パターン
result, err := doSomething()
if err != nil {
    return err
}

// インライン初期化
if err := doSomething(); err != nil {
    return err
}

// 複数の戻り値を無視する場合
_, err := doSomething()
if err != nil {
    return err
}

// エラーのみの関数
if err := doSomething(); err != nil {
    return err
}

---

deferの効果的な使用

基本的な使用法

package main

import (
    "fmt"
    "os"
)

func readFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()  // 関数終了時に必ず実行される

    // ファイル処理
    // エラーが起きても file.Close() は実行される

    return nil
}

deferのスタック順序

package main

import "fmt"

func main() {
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")

    fmt.Println("main")
}

// 出力:
// main
// 3
// 2
// 1

deferの落とし穴

package main

import "fmt"

// 問題: deferがループの外で評価される
func badExample() {
    for i := 0; i < 5; i++ {
        // このdeferは5回スタックされる!
        defer fmt.Println(i)
    }
}

// 解決策1: 関数で囲む
func goodExample1() {
    for i := 0; i < 5; i++ {
        func(n int) {
            defer fmt.Println(n)
        }(i)
    }
}

// 解決策2: deferを使わない
func goodExample2() {
    for i := 0; i < 5; i++ {
        fmt.Println(i)
    }
}

戻り値とdefer

package main

import "fmt"

func example() (result int) {
    defer func() {
        result++  // 戻り値を変更できる
    }()

    return 42
}

func main() {
    fmt.Println(example())  // 43
}

---

panicとrecover

panicの使用場面

package main

import "fmt"

// 回復不可能なエラー
func mustConnect(dsn string) *Connection {
    conn, err := connect(dsn)
    if err != nil {
        panic(fmt.Sprintf("failed to connect: %v", err))
    }
    return conn
}

// 契約違反(プログラムのバグ)
func divide(a, b int) int {
    if b == 0 {
        panic("divide by zero")
    }
    return a / b
}

注意: panicは以下の場合のみ使用してください:

  • プログラムが継続できない致命的なエラー
  • プログラミングエラー(バグ)の検出
  • 初期化フェーズの失敗
  • 通常のエラーにはerrorを返してください。

    recoverの使用

    package main
    
    import "fmt"
    
    func safeDivide(a, b int) (result int, err error) {
        defer func() {
            if r := recover(); r != nil {
                err = fmt.Errorf("panic occurred: %v", r)
            }
        }()
    
        result = a / b
        return result, nil
    }
    
    func main() {
        result, err := safeDivide(10, 0)
        if err != nil {
            fmt.Println("エラー:", err)
        } else {
            fmt.Println("結果:", result)
        }
    }
    

    HTTPサーバーでのrecover

    package main
    
    import (
        "fmt"
        "net/http"
    )
    
    func recoverMiddleware(next http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
            defer func() {
                if err := recover(); err != nil {
                    fmt.Printf("panic: %v\n", err)
                    http.Error(w, "Internal Server Error", 500)
                }
            }()
    
            next(w, r)
        }
    }
    
    func dangerousHandler(w http.ResponseWriter, r *http.Request) {
        panic("something went wrong")
    }
    
    func main() {
        http.HandleFunc("/", recoverMiddleware(dangerousHandler))
        http.ListenAndServe(":8080", nil)
    }
    

    ---

    インターフェースのイディオム

    小さいインターフェース

    // 悪い例:巨大なインターフェース
    type Storage interface {
        Get(key string) ([]byte, error)
        Set(key string, value []byte) error
        Delete(key string) error
        List() ([]string, error)
        Clear() error
        Count() (int, error)
    }
    
    // 良い例:小さく分割
    type Reader interface {
        Get(key string) ([]byte, error)
    }
    
    type Writer interface {
        Set(key string, value []byte) error
    }
    
    type Deleter interface {
        Delete(key string) error
    }
    
    // 必要に応じて組み合わせ
    type ReadWriter interface {
        Reader
        Writer
    }
    

    受け入れはインターフェース、返すは具象型

    // 良い例
    func processData(r io.Reader) (*Result, error) {
        // インターフェースを受け取る
        data, err := io.ReadAll(r)
        if err != nil {
            return nil, err
        }
    
        // 具象型を返す
        return &Result{Data: data}, nil
    }
    
    // 悪い例
    func processData(r *bytes.Reader) (io.Reader, error) {
        // 具象型を受け取り、インターフェースを返す
        // これは柔軟性を失う
    }
    

    ---

    構造体の初期化パターン

    コンストラクタ関数

    package main
    
    type Config struct {
        Host    string
        Port    int
        Timeout int
    }
    
    // NewXxx パターン
    func NewConfig(host string, port int) *Config {
        return &Config{
            Host:    host,
            Port:    port,
            Timeout: 30,  // デフォルト値
        }
    }
    
    // オプション引数が多い場合
    type ConfigOption func(*Config)
    
    func WithTimeout(timeout int) ConfigOption {
        return func(c *Config) {
            c.Timeout = timeout
        }
    }
    
    func WithPort(port int) ConfigOption {
        return func(c *Config) {
            c.Port = port
        }
    }
    
    func NewConfigWithOptions(host string, opts ...ConfigOption) *Config {
        config := &Config{
            Host:    host,
            Port:    8080,  // デフォルト
            Timeout: 30,    // デフォルト
        }
    
        for _, opt := range opts {
            opt(config)
        }
    
        return config
    }
    
    func main() {
        // 使用例
        config := NewConfigWithOptions(
            "localhost",
            WithPort(9000),
            WithTimeout(60),
        )
    }
    

    ゼロ値を利用した設計

    package main
    
    import "sync"
    
    // 良い例:ゼロ値で使用可能
    type Counter struct {
        mu    sync.Mutex
        count int
    }
    
    func (c *Counter) Increment() {
        c.mu.Lock()
        defer c.mu.Unlock()
        c.count++
    }
    
    func main() {
        var counter Counter  // ゼロ値で使える
        counter.Increment()
    }
    

    ---

    スライスとマップのイディオム

    スライスのプリアロケーション

    // 悪い例
    func processItems(n int) []int {
        var result []int
        for i := 0; i < n; i++ {
            result = append(result, i)  // 何度も再割り当て
        }
        return result
    }
    
    // 良い例
    func processItems(n int) []int {
        result := make([]int, 0, n)  // 容量を事前確保
        for i := 0; i < n; i++ {
            result = append(result, i)
        }
        return result
    }
    
    // さらに良い例:長さが確定している場合
    func processItems(n int) []int {
        result := make([]int, n)  // 長さと容量を確保
        for i := 0; i < n; i++ {
            result[i] = i
        }
        return result
    }
    

    空スライスとnilスライス

    package main
    
    import (
        "encoding/json"
        "fmt"
    )
    
    func main() {
        var nilSlice []int
        emptySlice := []int{}
    
        fmt.Println("nil:", nilSlice == nil)    // true
        fmt.Println("empty:", emptySlice == nil) // false
    
        // JSONエンコード時の違い
        nilJSON, _ := json.Marshal(nilSlice)
        emptyJSON, _ := json.Marshal(emptySlice)
    
        fmt.Println(string(nilJSON))    // null
        fmt.Println(string(emptyJSON))  // []
    
        // 通常はnilスライスを使う
        var items []string  // nil
        items = append(items, "a")
    }
    

    マップの存在確認

    // 悪い例
    value := m["key"]
    if value != 0 {
        // キーが存在する?値が0でない?
    }
    
    // 良い例
    value, ok := m["key"]
    if ok {
        // キーが存在する
    }
    
    // さらに良い例:値だけ必要な場合
    if value := m["key"]; value != 0 {
        // 値が0でない場合の処理
    }
    

    ---

    並行処理の準備

    sync.Mutexの基本

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    type SafeCounter struct {
        mu    sync.Mutex
        count int
    }
    
    func (c *SafeCounter) Increment() {
        c.mu.Lock()
        defer c.mu.Unlock()
        c.count++
    }
    
    func (c *SafeCounter) Value() int {
        c.mu.Lock()
        defer c.mu.Unlock()
        return c.count
    }
    
    func main() {
        counter := &SafeCounter{}
    
        var wg sync.WaitGroup
        for i := 0; i < 1000; i++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                counter.Increment()
            }()
        }
    
        wg.Wait()
        fmt.Println("Count:", counter.Value())
    }
    

    ---

    コードレビューで指摘される問題

    1. エクスポートされた識別子にコメントがない

    // 悪い例
    type User struct {
        Name string
        Age  int
    }
    
    // 良い例
    // User はシステム内のユーザーを表します。
    type User struct {
        Name string
        Age  int
    }
    
    // Login はユーザーをログインさせます。
    func (u *User) Login() error {
        // ...
    }
    

    2. エラーを無視している

    // 悪い例
    doSomething()  // エラーを無視
    
    // 良い例
    _ = doSomething()  // 意図的に無視していることを明示
    
    // さらに良い例
    if err := doSomething(); err != nil {
        return err
    }
    

    3. 変数のシャドーイング

    // 悪い例
    func process() error {
        user, err := getUser()
        if err != nil {
            return err
        }
    
        if condition {
            user, err := getAnotherUser()  // userをシャドーイング
            if err != nil {
                return err
            }
            // この user は外側の user とは別の変数
        }
    
        // ここの user は最初の user
    }
    
    // 良い例
    func process() error {
        user, err := getUser()
        if err != nil {
            return err
        }
    
        if condition {
            anotherUser, err := getAnotherUser()
            if err != nil {
                return err
            }
            // 変数名を変えて明確にする
        }
    }
    

    ---

    プロダクションレベルのイディオム

    パッケージ設計の原則

    単一責任の原則(Single Responsibility Principle)

    // 悪い例:1つのパッケージに複数の責任
    package app
    
    type UserService struct{}
    type ProductService struct{}
    type OrderService struct{}
    type PaymentService struct{}
    type EmailService struct{}
    
    // 良い例:責任ごとにパッケージを分割
    package user
    type Service struct{}
    
    package product
    type Service struct{}
    
    package order
    type Service struct{}
    
    package payment
    type Service struct{}
    
    package notification
    type EmailService struct{}
    

    循環依存の回避

    // 悪い例:循環依存
    // package user
    type User struct {
        Orders []order.Order  // userがorderに依存
    }
    
    // package order
    type Order struct {
        User user.User  // orderがuserに依存
    }
    
    // 良い例:インターフェースで依存を逆転
    // package user
    type User struct {
        ID   string
        Name string
    }
    
    // package order
    type Order struct {
        UserID string  // IDのみ保持
    }
    
    // または共通パッケージを作成
    // package domain
    type User struct { ID string }
    type Order struct { UserID string }
    

    コンテキストの活用

    コンテキストの適切な使用

    package main
    
    import (
        "context"
        "fmt"
        "time"
    )
    
    // データベース操作の例
    func fetchUser(ctx context.Context, id string) (*User, error) {
        // タイムアウト確認
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        default:
        }
    
        // 実際のDB操作
        // ...
        return &User{ID: id}, nil
    }
    
    // HTTP APIの例
    func handleRequest(ctx context.Context) error {
        // コンテキストに値を設定
        ctx = context.WithValue(ctx, "requestID", "req-12345")
    
        // タイムアウト付きコンテキスト
        ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
        defer cancel()
    
        return processWithContext(ctx)
    }
    
    // ベストプラクティス:コンテキストは最初の引数
    func processWithContext(ctx context.Context) error {
        // コンテキストから値を取得
        if reqID := ctx.Value("requestID"); reqID != nil {
            fmt.Println("Request ID:", reqID)
        }
    
        return nil
    }
    

    コンテキストのアンチパターン

    // アンチパターン1:構造体にコンテキストを保存
    type Service struct {
        ctx context.Context  // NG: コンテキストは保存しない
    }
    
    // 正しい方法:引数として渡す
    type Service struct {
        db *Database
    }
    
    func (s *Service) FetchUser(ctx context.Context, id string) (*User, error) {
        return s.db.QueryUser(ctx, id)
    }
    
    // アンチパターン2:コンテキストをnilで渡す
    func bad() {
        doSomething(nil)  // NG
    }
    
    // 正しい方法
    func good() {
        doSomething(context.Background())  // または context.TODO()
    }
    

    エラーハンドリングの高度なパターン

    カスタムエラー型

    package main
    
    import (
        "errors"
        "fmt"
    )
    
    // エラー型の定義
    type ValidationError struct {
        Field   string
        Message string
    }
    
    func (e *ValidationError) Error() string {
        return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
    }
    
    // センチネルエラー(定数エラー)
    var (
        ErrNotFound     = errors.New("resource not found")
        ErrUnauthorized = errors.New("unauthorized access")
        ErrInvalidInput = errors.New("invalid input")
    )
    
    // エラーのラップ
    func processUser(id string) error {
        user, err := fetchUser(id)
        if err != nil {
            return fmt.Errorf("failed to process user %s: %w", id, err)
        }
    
        if err := validateUser(user); err != nil {
            return fmt.Errorf("user validation failed: %w", err)
        }
    
        return nil
    }
    
    // エラーの判定
    func handleError(err error) {
        // センチネルエラーの確認
        if errors.Is(err, ErrNotFound) {
            fmt.Println("リソースが見つかりません")
            return
        }
    
        // 型アサーションによる詳細取得
        var validationErr *ValidationError
        if errors.As(err, &validationErr) {
            fmt.Printf("バリデーションエラー: %s\n", validationErr.Field)
            return
        }
    
        // その他のエラー
        fmt.Printf("予期しないエラー: %v\n", err)
    }
    

    エラーグループパターン

    package main
    
    import (
        "context"
        "fmt"
        "golang.org/x/sync/errgroup"
    )
    
    // 複数の並行処理でエラーを管理
    func processMultiple(ctx context.Context, urls []string) error {
        g, ctx := errgroup.WithContext(ctx)
    
        // 並行数の制限
        g.SetLimit(10)
    
        for _, url := range urls {
            url := url  // ループ変数のキャプチャ
            g.Go(func() error {
                return processURL(ctx, url)
            })
        }
    
        // 全てのゴルーチンの完了を待つ
        // いずれかがエラーを返したら、残りはキャンセルされる
        if err := g.Wait(); err != nil {
            return fmt.Errorf("processing failed: %w", err)
        }
    
        return nil
    }
    
    func processURL(ctx context.Context, url string) error {
        // コンテキストのキャンセル確認
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
        }
    
        // 実際の処理
        return nil
    }
    

    テスタブルなコード設計

    依存性注入(Dependency Injection)

    package main
    
    import (
        "context"
        "database/sql"
        "fmt"
    )
    
    // インターフェースを定義
    type UserRepository interface {
        FindByID(ctx context.Context, id string) (*User, error)
        Save(ctx context.Context, user *User) error
    }
    
    // 実装1: データベース
    type DBUserRepository struct {
        db *sql.DB
    }
    
    func (r *DBUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
        // 実際のDB操作
        return &User{ID: id}, nil
    }
    
    func (r *DBUserRepository) Save(ctx context.Context, user *User) error {
        // 実際のDB操作
        return nil
    }
    
    // 実装2: インメモリ(テスト用)
    type InMemoryUserRepository struct {
        users map[string]*User
    }
    
    func (r *InMemoryUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
        if user, ok := r.users[id]; ok {
            return user, nil
        }
        return nil, ErrNotFound
    }
    
    func (r *InMemoryUserRepository) Save(ctx context.Context, user *User) error {
        r.users[user.ID] = user
        return nil
    }
    
    // サービス層:リポジトリに依存
    type UserService struct {
        repo UserRepository  // インターフェース型
    }
    
    func NewUserService(repo UserRepository) *UserService {
        return &UserService{repo: repo}
    }
    
    func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
        return s.repo.FindByID(ctx, id)
    }
    
    // テストコード
    func TestUserService(t *testing.T) {
        // モックリポジトリを注入
        repo := &InMemoryUserRepository{
            users: make(map[string]*User),
        }
    
        service := NewUserService(repo)
    
        // テスト実行
        user, err := service.GetUser(context.Background(), "1")
        // ...
    }
    

    ログとオブザーバビリティ

    構造化ロギング

    package main
    
    import (
        "context"
        "log/slog"
        "os"
    )
    
    // JSONハンドラーで構造化ログ
    func setupLogger() *slog.Logger {
        handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
            Level: slog.LevelInfo,
        })
        return slog.New(handler)
    }
    
    func processRequest(ctx context.Context, logger *slog.Logger, userID string) {
        // コンテキスト情報を含めたログ
        logger.InfoContext(ctx, "processing request",
            slog.String("userID", userID),
            slog.String("operation", "fetch_profile"),
        )
    
        // エラーログ
        if err := fetchProfile(userID); err != nil {
            logger.ErrorContext(ctx, "failed to fetch profile",
                slog.String("userID", userID),
                slog.Any("error", err),
            )
            return
        }
    
        // 構造化されたログ
        logger.InfoContext(ctx, "request completed",
            slog.String("userID", userID),
            slog.Duration("duration", time.Since(start)),
        )
    }
    
    // ログレベル付きロガー
    type LeveledLogger struct {
        logger *slog.Logger
    }
    
    func (l *LeveledLogger) Debug(msg string, args ...any) {
        l.logger.Debug(msg, args...)
    }
    
    func (l *LeveledLogger) Info(msg string, args ...any) {
        l.logger.Info(msg, args...)
    }
    
    func (l *LeveledLogger) Warn(msg string, args ...any) {
        l.logger.Warn(msg, args...)
    }
    
    func (l *LeveledLogger) Error(msg string, args ...any) {
        l.logger.Error(msg, args...)
    }
    

    パフォーマンス最適化のイディオム

    文字列連結の最適化

    package main
    
    import (
        "bytes"
        "strings"
    )
    
    // 悪い例:+演算子での連結(遅い)
    func badConcat(strs []string) string {
        result := ""
        for _, s := range strs {
            result += s  // 毎回新しい文字列を作成
        }
        return result
    }
    
    // 良い例1:strings.Builder(推奨)
    func goodConcat1(strs []string) string {
        var sb strings.Builder
        sb.Grow(len(strs) * 10)  // 容量の事前確保
    
        for _, s := range strs {
            sb.WriteString(s)
        }
        return sb.String()
    }
    
    // 良い例2:strings.Join(シンプルな場合)
    func goodConcat2(strs []string) string {
        return strings.Join(strs, "")
    }
    
    // 良い例3:bytes.Buffer(バイト操作が必要な場合)
    func goodConcat3(strs []string) string {
        var buf bytes.Buffer
        for _, s := range strs {
            buf.WriteString(s)
        }
        return buf.String()
    }
    

    メモリアロケーションの削減

    package main
    
    // 悪い例:スライスの頻繁な再割り当て
    func badAllocation(n int) []int {
        var result []int
        for i := 0; i < n; i++ {
            result = append(result, i)  // 容量不足時に再割り当て
        }
        return result
    }
    
    // 良い例:事前にメモリを確保
    func goodAllocation(n int) []int {
        result := make([]int, 0, n)  // 容量を事前確保
        for i := 0; i < n; i++ {
            result = append(result, i)  // 再割り当てなし
        }
        return result
    }
    
    // さらに良い例:インデックスアクセス
    func bestAllocation(n int) []int {
        result := make([]int, n)  // 長さと容量を確保
        for i := 0; i < n; i++ {
            result[i] = i  // appendより高速
        }
        return result
    }
    
    // ポインタの適切な使用
    type LargeStruct struct {
        Data [1024]byte
    }
    
    // 悪い例:大きな構造体を値渡し
    func badPass(s LargeStruct) {
        // 構造体全体がコピーされる
    }
    
    // 良い例:ポインタ渡し
    func goodPass(s *LargeStruct) {
        // ポインタのみコピー(8バイト)
    }
    

    実践的なデザインパターン

    オプションパターン(Functional Options Pattern)

    package main
    
    import "time"
    
    type Server struct {
        host         string
        port         int
        timeout      time.Duration
        maxConns     int
        enableMetrics bool
    }
    
    // オプション関数の型
    type ServerOption func(*Server)
    
    // オプション関数の定義
    func WithPort(port int) ServerOption {
        return func(s *Server) {
            s.port = port
        }
    }
    
    func WithTimeout(timeout time.Duration) ServerOption {
        return func(s *Server) {
            s.timeout = timeout
        }
    }
    
    func WithMaxConns(maxConns int) ServerOption {
        return func(s *Server) {
            s.maxConns = maxConns
        }
    }
    
    func WithMetrics(enable bool) ServerOption {
        return func(s *Server) {
            s.enableMetrics = enable
        }
    }
    
    // コンストラクタ
    func NewServer(host string, opts ...ServerOption) *Server {
        // デフォルト値
        server := &Server{
            host:         host,
            port:         8080,
            timeout:      30 * time.Second,
            maxConns:     100,
            enableMetrics: false,
        }
    
        // オプションを適用
        for _, opt := range opts {
            opt(server)
        }
    
        return server
    }
    
    // 使用例
    func main() {
        // デフォルト値で作成
        server1 := NewServer("localhost")
    
        // カスタム設定で作成
        server2 := NewServer("localhost",
            WithPort(9000),
            WithTimeout(60*time.Second),
            WithMaxConns(200),
            WithMetrics(true),
        )
    }
    

    シングルトンパターン(sync.Onceを使用)

    package main
    
    import (
        "sync"
    )
    
    type Config struct {
        DatabaseURL string
        APIKey      string
    }
    
    var (
        instance *Config
        once     sync.Once
    )
    
    // GetConfig はシングルトンインスタンスを返します
    func GetConfig() *Config {
        once.Do(func() {
            instance = &Config{
                DatabaseURL: loadDatabaseURL(),
                APIKey:      loadAPIKey(),
            }
        })
        return instance
    }
    
    // または、init関数を使用
    var globalConfig *Config
    
    func init() {
        globalConfig = &Config{
            DatabaseURL: loadDatabaseURL(),
            APIKey:      loadAPIKey(),
        }
    }
    
    func GetGlobalConfig() *Config {
        return globalConfig
    }
    

    ビルダーパターン

    package main
    
    import "time"
    
    type Query struct {
        table      string
        columns    []string
        where      map[string]interface{}
        orderBy    string
        limit      int
        offset     int
        timeout    time.Duration
    }
    
    type QueryBuilder struct {
        query *Query
    }
    
    func NewQueryBuilder(table string) *QueryBuilder {
        return &QueryBuilder{
            query: &Query{
                table:   table,
                columns: []string{"*"},
                where:   make(map[string]interface{}),
            },
        }
    }
    
    func (b *QueryBuilder) Select(columns ...string) *QueryBuilder {
        b.query.columns = columns
        return b
    }
    
    func (b *QueryBuilder) Where(field string, value interface{}) *QueryBuilder {
        b.query.where[field] = value
        return b
    }
    
    func (b *QueryBuilder) OrderBy(field string) *QueryBuilder {
        b.query.orderBy = field
        return b
    }
    
    func (b *QueryBuilder) Limit(limit int) *QueryBuilder {
        b.query.limit = limit
        return b
    }
    
    func (b *QueryBuilder) Offset(offset int) *QueryBuilder {
        b.query.offset = offset
        return b
    }
    
    func (b *QueryBuilder) Timeout(timeout time.Duration) *QueryBuilder {
        b.query.timeout = timeout
        return b
    }
    
    func (b *QueryBuilder) Build() *Query {
        return b.query
    }
    
    // 使用例
    func main() {
        query := NewQueryBuilder("users").
            Select("id", "name", "email").
            Where("age", 25).
            Where("active", true).
            OrderBy("created_at").
            Limit(10).
            Offset(0).
            Timeout(5 * time.Second).
            Build()
    }
    

    OSSプロジェクトから学ぶイディオム

    Kubernetesスタイルのクライアントインターフェース

    package main
    
    import "context"
    
    // リソース操作のインターフェース
    type Interface interface {
        Users() UserInterface
        Products() ProductInterface
    }
    
    type UserInterface interface {
        Get(ctx context.Context, name string) (*User, error)
        List(ctx context.Context) (*UserList, error)
        Create(ctx context.Context, user *User) (*User, error)
        Update(ctx context.Context, user *User) (*User, error)
        Delete(ctx context.Context, name string) error
    }
    
    type ProductInterface interface {
        Get(ctx context.Context, name string) (*Product, error)
        List(ctx context.Context) (*ProductList, error)
        Create(ctx context.Context, product *Product) (*Product, error)
        Update(ctx context.Context, product *Product) (*Product, error)
        Delete(ctx context.Context, name string) error
    }
    
    // 実装
    type Client struct {
        baseURL string
    }
    
    func (c *Client) Users() UserInterface {
        return &userClient{client: c}
    }
    
    func (c *Client) Products() ProductInterface {
        return &productClient{client: c}
    }
    
    type userClient struct {
        client *Client
    }
    
    func (u *userClient) Get(ctx context.Context, name string) (*User, error) {
        // 実装
        return nil, nil
    }
    
    // 使用例
    func main() {
        client := &Client{baseURL: "https://api.example.com"}
    
        // Kubernetesライクな使用方法
        user, err := client.Users().Get(context.Background(), "user-1")
        if err != nil {
            // エラーハンドリング
        }
    }
    

    Prometheusスタイルのメトリクス

    package main
    
    import (
        "github.com/prometheus/client_golang/prometheus"
        "github.com/prometheus/client_golang/prometheus/promauto"
    )
    
    var (
        // カウンター:増加のみ
        requestsTotal = promauto.NewCounterVec(
            prometheus.CounterOpts{
                Name: "http_requests_total",
                Help: "Total number of HTTP requests",
            },
            []string{"method", "endpoint", "status"},
        )
    
        // ゲージ:増減可能
        activeConnections = promauto.NewGauge(
            prometheus.GaugeOpts{
                Name: "active_connections",
                Help: "Number of active connections",
            },
        )
    
        // ヒストグラム:分布の測定
        requestDuration = promauto.NewHistogramVec(
            prometheus.HistogramOpts{
                Name:    "http_request_duration_seconds",
                Help:    "HTTP request duration in seconds",
                Buckets: prometheus.DefBuckets,
            },
            []string{"method", "endpoint"},
        )
    )
    
    func handleRequest(method, endpoint string) {
        timer := prometheus.NewTimer(requestDuration.WithLabelValues(method, endpoint))
        defer timer.ObserveDuration()
    
        activeConnections.Inc()
        defer activeConnections.Dec()
    
        // リクエスト処理
        status := "200"
    
        requestsTotal.WithLabelValues(method, endpoint, status).Inc()
    }
    

    ---

    まとめと次のステップ

    重要なポイント

  • 一貫性: コミュニティのスタイルガイドに従う
  • シンプルさ: 複雑さより明確さを優先
  • 可読性: 他の開発者が読みやすいコードを書く
  • テスタビリティ: テストしやすい設計を心がける
  • パフォーマンス: 適切な最適化を行う
  • さらに学ぶべきリソース

  • Effective Go
  • Go Code Review Comments
  • Uber Go Style Guide
  • Google Go Style Guide
  • 100 Go Mistakes and How to Avoid Them

次のステップ

Day 3では、これらのイディオムを活かしてインターフェースの設計と実装を学びます。プロダクションレベルのコードを書くための実践的なスキルを身につけましょう。