ポインタと値の使い分けレビュー完全ガイド:関数設計意図の読み解き方
ポインタと値の使い分けレビュー完全ガイド:関数設計意図の読み解き方
Go言語では、構造体を関数に渡すたびに「ポインタ渡し」と「値渡し」どちらを選ぶか という設計判断が常に発生する。
レビューアーは「どちらでも動く」ではなく、その渡し方が設計意図に一致しているか を構造的に評価する必要がある。
ポインタ vs 値の選択は単なるパフォーマンス問題ではない。
責務・副作用・所有権・API設計 をレビューで読み解く訓練になる。
良い実装例:設計意図が明確な使い分け
type User struct {
ID string
Name string
Email string
}
// 読み取り専用処理(値渡し)
func DisplayName(u User) string {
return u.Name
}
// 状態更新処理(ポインタ渡し)
func UpdateEmail(u *User, email string) {
u.Email = email
}
- 読み取りのみ → 値渡し
- 書き換え発生 → ポインタ渡し
- 役割の差異がAPIシグネチャで自然に伝わる
レビューアーが最も読み取りやすい典型パターンがこの構造。
問題のある実装例とレビュー指摘
① 副作用を伴わないのにポインタを受け取る設計
func Process(u *User) {
fmt.Println(u.Name)
}
@Reviewer処理内容は読み取りのみで更新を伴わないのにポインタを受け取っています。値渡しへ変更し、API設計の副作用明示性を高めてください。
「書き換えはしないが念のためポインタで渡しておく」── この設計は誤解の温床になる。
関数の「責務宣言」としても型は意味を持つ。
② 大型構造体を安易に値渡し
type LargeData struct {
ID string
Name string
Email string
Tags []string
Meta map[string]string
}
func Send(ld LargeData) {
// 実質毎回大型コピーが発生
}
@ReviewerLargeDataはポインタ型メンバを含む複合構造です。頻繁に値渡しするとメモリ負荷が高まるため、ポインタ渡しへ変更しコピーコストを削減してください。
構造体が「小さければ値渡し」「大きければポインタ渡し」──これは現場レビューの最も基本的なコスト評価基準となる。
③ 戻り値で所有権が曖昧な設計
func GetConfig() *Config {
return &Config{
Retry: 3,
Debug: true,
}
}
@Reviewer返却されたConfigが呼び出し元で更新可能構造か否かが設計上不明確です。読み取り専用用途であれば値返却または不変設計を検討してください。
返却値がポインタ型になるだけで「再利用可能か?変更前提か?」という設計意図疑問が必ず発生する。
レビューでは所有権整理観点を持つべき。
④ レシーバー設計の混在問題
func (u User) DisplayName() string {
return u.Name
}
func (u *User) UpdateEmail(email string) {
u.Email = email
}
@Reviewerメソッドで値レシーバー・ポインタレシーバーが混在しています。状態変更を含む型なら全体をポインタレシーバーへ統一し一貫性を保ってください。
Goのプロジェクトでは「ポインタレシーバー原則統一」が推奨される場面が非常に多い。
API設計の読みやすさも安定性も向上する。
⑤ 無意味な二重ポインタ構造
func Update(u **User) {
(*u).Name = "Updated"
}
@Reviewer二重ポインタを使用する設計理由がありません。通常は*Userまでで十分です。API設計の過剰抽象は避けてください。
「ポインタのポインタ」は実務レビューでもたまに現れる危険コード。
言語仕様上は動いても設計破綻のサイン。
ポインタ vs 値選択判断フロー図解
実務レビューでの判断軸
「副作用意図」の明示性
- 書き換え処理を行うならポインタ
- 読み取り専用なら値
「構造体コスト」評価
- サイズ評価(フィールド数・内部ポインタ保有)
- 呼び出し頻度
「所有権責任」整理
- 生成責任がどこにあるか
- 呼び出し元からの再利用有無
「API一貫性」
- レシーバー統一(値/ポインタ混在NG)
「抽象過剰の抑止」
- 二重ポインタ・無目的ポインタ層廃止
レビュー観点チェックリスト
チェック項目 | 内容 |
---|---|
副作用の有無 | 更新伴うならポインタ使用されているか |
読み取り専用か | 値渡し可能なら型を簡潔にしているか |
構造体コスト | 大型構造ならポインタを採用しているか |
レシーバー統一性 | 値/ポインタで混在していないか |
戻り値責務整理 | 返却ポインタで所有権混乱がないか |
無意味なポインタ層 | TやTの誤用が発生していないか |
よくある実務事故事例
① 呼び出し元に意図せず副作用が発生
func Modify(u *User) {
u.Name = "Changed"
}
func main() {
u := User{Name: "Alice"}
Modify(&u)
fmt.Println(u.Name)
}
@Reviewer呼び出し側で副作用が発生することが明文化されていません。副作用設計意図をAPIシグネチャでも表現してください。
② 安易にポインタを返し責務が拡散
func GetUser() *User {
return &User{Name: "Bob"}
}
@Reviewer呼び出し元がこのUserを更新可能か否か設計意図が不明確です。読み取り専用であれば構造体値返却も検討してください。
ポインタレビューは構造読み取り力を鍛える
ポインタ vs 値は「正解はどちらか?」ではなく
「設計意図が読み取れるか?」がレビューでの本質になる。
レビューアーが確認すべきこと:
- 副作用の意図は明確か?
- 呼び出し元・呼び出し先の責務は明快か?
- コスト計算を行った上で型が選ばれているか?
- APIレベルでの一貫性が保たれているか?