2025-06-14
Goのシンプルな文法は強力な武器ですが、その裏には知らず知らずのうちにコードの品質を下げてしまう「アンチパターン」が潜んでいます。動くコードを書くことと、保守性が高く堅牢なコードを書くことは全く別の話です。
この記事では、多くのGoプロジェクトで見られる代表的なアンチパターンを4つ取り上げ、なぜそれが問題なのか、そしてどうすればより良いコードになるのかを具体的なコード例と共に解説します。初級以上のGoエンジニアが、より「Goらしい」コードを書けるようになることを目指しています。
go1// 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// ...
多くのプロジェクトで、関連性の低い関数を一つのutils
やcommon
パッケージに詰め込んでしまうケースが見られます。
機能やドメインごとにより小さなパッケージに分割しましょう。
go1// 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
の他にも、プロジェクト規模や慣習に応じてclient
、request
、infra/http
のように階層を切ることも多く見られます。
init()
関数の安易な利用go1package 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()
はパッケージ読み込み時に自動実行されるため、テストでのモック差し替えが困難明示的な初期化関数を定義しましょう。
go1package 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}
これにより、呼び出し元でエラーハンドリングが可能になり、依存関係も明確になります。
error
の不適切なハンドリングgo1func 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()
_
を使ってエラーを意図的に無視したり、エラー発生時に他の戻り値が適切でない状態で返してしまうパターンです。
エラーは常にチェックし、適切に処理しましょう。
go1func 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.Is
やerrors.As
を使ってエラーの種類に応じた適切な処理を行います。
go1import ( 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}
context.Context
を使わない長時間処理go1func 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
を第一引数に受け取るように設計しましょう。
go1import ( 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コードは格段に品質が向上するでしょう。
init()
関数の代わりに明示的な初期化関数を使うcontext.Context
を活用するこのような知識は、コードレビューやアーキテクチャ設計の場で大きな力を発揮し、エンジニアとしての信頼と市場価値を高めることに直結します。技術的な深い知見を持つエンジニアほど、より良い条件の案件に出会える機会が増えるのも事実です。
私たちGoForceは、このような深い技術的知見を持つエンジニアが正当に評価される案件を数多く扱っています。あなたの「良いコードを書く力」を、次のステージで試してみませんか?ぜひ一度、お気軽にご相談ください。
最適なGo案件を今すぐチェック!