目次

    あなたのコードは大丈夫?Goエンジニアが避けるべきアンチパターン4選と解決策
    Goアンチパターンコード品質ベストプラクティス

    2025-06-14

    電球アイコン

    あなたのコードは大丈夫?Goエンジニアが避けるべきアンチパターン4選と解決策

    Goのシンプルな文法は強力な武器ですが、その裏には知らず知らずのうちにコードの品質を下げてしまう「アンチパターン」が潜んでいます。動くコードを書くことと、保守性が高く堅牢なコードを書くことは全く別の話です。

    この記事では、多くのGoプロジェクトで見られる代表的なアンチパターンを4つ取り上げ、なぜそれが問題なのか、そしてどうすればより良いコードになるのかを具体的なコード例と共に解説します。初級以上のGoエンジニアが、より「Goらしい」コードを書けるようになることを目指しています。

    Goでよく見るアンチパターンと解決策

    アンチパターン1:巨大な「utils」パッケージ

    アンチパターン例

    go
    1// utils/utils.go
    2package utils
    3
    4import (
    5    "net/http"
    6    "time"
    7)
    8
    9func FormatDate(t time.Time) string { /* ... */ }
    10func ValidateEmail(email string) bool { /* ... */ }
    11func HTTPGet(url string) (*http.Response, error) { /* ... */ }
    12// ...

    多くのプロジェクトで、関連性の低い関数を一つのutilscommonパッケージに詰め込んでしまうケースが見られます。

    問題点

    • パッケージの見通しが悪くなり、依存関係が複雑化する
    • 責務が曖昧になり、コードの再利用性が低下する
    • テストのしやすさが損なわれる

    解決策

    機能やドメインごとにより小さなパッケージに分割しましょう。

    go
    1// auth/password.go
    2package auth
    3
    4func EncryptPassword(password string) string { /* ... */ }
    5
    6// httpclient/client.go
    7package httpclient
    8
    9import "net/http"
    10
    11func Get(url string) (*http.Response, error) { /* ... */ }
    12
    13// user/validator.go
    14package user
    15
    16func ValidateEmail(email string) bool { /* ... */ }

    HTTPクライアント関連のヘルパーであれば、クライアントを定義しているパッケージの近くに配置するなど、利用する場所との凝集度を高めることが重要です。パッケージ名はhttpclientの他にも、プロジェクト規模や慣習に応じてclientrequestinfra/httpのように階層を切ることも多く見られます。

    アンチパターン2:init()関数の安易な利用

    アンチパターン例

    go
    1package database
    2
    3import (
    4    "database/sql"
    5    "log"
    6    _ "github.com/lib/pq"
    7)
    8
    9var DB *sql.DB
    10
    11func init() {
    12    var err error
    13    DB, err = sql.Open("postgres", "user=postgres dbname=mydb sslmode=disable")
    14    if err != nil {
    15        log.Fatal(err) // エラーハンドリングが困難
    16    }
    17}

    設定ファイルの読み込みやデータベース接続など、副作用を伴う初期化処理をinit()関数で行うパターンです。

    問題点

    • init()はエラーを返せないため、初期化に失敗した際のエラーハンドリングが困難
    • log.Fatalはテスト環境でプロセスが即終了してしまうため危険
    • init()はパッケージ読み込み時に自動実行されるため、テストでのモック差し替えが困難
    • プログラムの起動順序に暗黙的な依存が生まれる

    解決策

    明示的な初期化関数を定義しましょう。

    go
    1package database
    2
    3import (
    4    "database/sql"
    5    "fmt"
    6    _ "github.com/lib/pq"
    7)
    8
    9type DB struct {
    10    conn *sql.DB
    11}
    12
    13func NewDB(dsn string) (*DB, error) {
    14    conn, err := sql.Open("postgres", dsn)
    15    if err != nil {
    16        return nil, fmt.Errorf("failed to open database: %w", err)
    17    }
    18    
    19    if err := conn.Ping(); err != nil {
    20        return nil, fmt.Errorf("failed to ping database: %w", err)
    21    }
    22    
    23    return &DB{conn: conn}, nil
    24}

    これにより、呼び出し元でエラーハンドリングが可能になり、依存関係も明確になります。

    アンチパターン3:errorの不適切なハンドリング

    アンチパターン例

    go
    1func GetUser(id int) (*User, error) {
    2    user, err := db.Query("...")
    3    if err != nil {
    4        return user, err // error時にuserがnilではない状態で返ってしまう
    5    }
    6    return user, nil
    7}
    8// エラーを意図的に無視
    9data, _ := fetchData()

    _を使ってエラーを意図的に無視したり、エラー発生時に他の戻り値が適切でない状態で返してしまうパターンです。

    問題点

    • バグの温床となり、予期せぬnilポインタ参照などを引き起こす
    • エラー発生時のプログラムの状態が不定になる

    解決策

    エラーは常にチェックし、適切に処理しましょう。

    go
    1func GetUser(ctx context.Context, id int) (*User, error) {
    2    user, err := db.QueryRowContext(ctx, "...")
    3    if err != nil {
    4        return nil, fmt.Errorf("failed to get user: %w", err) // エラー時は `user` を `nil` で返す
    5    }
    6    return user, nil
    7}

    エラーが発生した場合は、他の戻り値はゼロ値(ポインタならnil)で返すことを徹底し、errors.Iserrors.Asを使ってエラーの種類に応じた適切な処理を行います。

    go
    1import (
    2    "database/sql"
    3    "errors"
    4    "fmt"
    5    "net"
    6)
    7
    8// カスタムエラーの定義例
    9var ErrNotFound = errors.New("record not found")
    10
    11if errors.Is(err, sql.ErrNoRows) {
    12    return nil, ErrNotFound
    13}
    14
    15var netErr *net.OpError
    16if errors.As(err, &netErr) {
    17    // ネットワークエラーの詳細処理
    18    return nil, fmt.Errorf("network error: %w", err)
    19}

    アンチパターン4:context.Contextを使わない長時間処理

    アンチパターン例

    go
    1func FetchUserData(ctx, context.Context, userID string) (*UserData, error) {
    2    // ネットワークリクエストにcontextを渡さない
    3    resp, err := http.Get("https://api.example.com/users/" + userID)
    4    if err != nil {
    5        return nil, err
    6    }
    7    defer resp.Body.Close()
    8    
    9    // 長時間かかる可能性のある処理
    10    return parseResponse(ctx, resp)
    11}

    ネットワークリクエストやデータベースクエリなど、完了までに時間がかかる可能性のある関数にcontext.Contextを渡さないパターンです。

    問題点

    • タイムアウトや外部からのキャンセル要求を処理できない
    • リクエストのキャンセル時に、裏で動いているゴルーチンがリソースを解放できず、リークの原因となる

    解決策

    ブロックされる可能性のあるI/O処理には、必ずcontext.Contextを第一引数に受け取るように設計しましょう。

    go
    1import (
    2    "context"
    3    "fmt"
    4    "net/http"
    5)
    6
    7func FetchUserData(ctx context.Context, userID string) (*UserData, error) {
    8    req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/users/"+userID, nil)
    9    if err != nil {
    10        return nil, fmt.Errorf("failed to create request: %w", err)
    11    }
    12    
    13    client := &http.Client{Timeout: 10 * time.Second}
    14    resp, err := client.Do(req)
    15    if err != nil {
    16        return nil, fmt.Errorf("failed to fetch user data: %w", err)
    17    }
    18    defer resp.Body.Close()
    19    
    20    return parseResponse(ctx, resp)
    21}
    22
    23func parseResponse(ctx context.Context, resp *http.Response) (*UserData, error) {
    24    // レスポンスの解析処理
    25    return &UserData{}, nil
    26}

    これにより、タイムアウトやキャンセルの伝播が容易になり、リソースの適切な管理が可能になります。

    まとめ|良いコードが、あなたの価値を高める

    CI には go vet ./...Staticcheck を組み込み、今回紹介したアンチパターンをプッシュ時に自動検出できるようにしておくと安心です。

    アンチパターンを避けることは、単に「動くコード」から「保守性が高く、堅牢なコード」へとステップアップすることを意味します。今回紹介した4つのポイントを意識するだけで、あなたのGoコードは格段に品質が向上するでしょう。

    • 巨大なutilsパッケージを避け、責務を明確に分割する
    • init()関数の代わりに明示的な初期化関数を使う
    • エラーハンドリングを徹底し、適切な戻り値を返す
    • 長時間処理にはcontext.Contextを活用する

    このような知識は、コードレビューやアーキテクチャ設計の場で大きな力を発揮し、エンジニアとしての信頼と市場価値を高めることに直結します。技術的な深い知見を持つエンジニアほど、より良い条件の案件に出会える機会が増えるのも事実です。

    私たちGoForceは、このような深い技術的知見を持つエンジニアが正当に評価される案件を数多く扱っています。あなたの「良いコードを書く力」を、次のステージで試してみませんか?ぜひ一度、お気軽にご相談ください。

    会員登録はこちら

    最適なGo案件を今すぐチェック!

    会員登録

    生年月日 *

    /

    /

    Go経験年数 *

    /

    利用規約プライバシーポリシーに同意してお申し込みください。