プログラマのための IT 教科書

既存コードを読む

この部の 2 / 9 章 ・ 全体で 14 / 76 章 ・ 読了目安 45 分

この章を読むとできるようになること
  • 初めて見るリポジトリから目的の処理にたどり着ける
  • 変更が何に影響するかを事前に洗い出せる
  • 読むことに時間を使うのが正しいと判断できる

エンジニアの仕事時間の内訳を測ると、コードを書いている時間より、読んでいる時間の方がずっと長いという結果になります。

それなのに、読み方を教わる機会はほとんどありません。 開発の流れでは開発の流れの一部として触れましたが、この章ではその本体を扱います。 どのコマンドを、どの順番で叩くかまで具体的に書きます。

読むことが仕事の大半を占める

新人が配属されて最初にやることは、たいてい「小さなバグ修正」です。 修正そのものは3行かもしれません。でも、その3行を見つけるまでに2日かかります。

チケットを受け取る
  → どこを直せばいいか探す        ← ここが一番長い
    → 直す                          ← 3行
      → 影響範囲を確かめる          ← ここも長い
        → PR を出す

探す時間と確かめる時間が、書く時間の10倍以上あります。 だから読む速度を上げることは、そのまま仕事の速度になります。

なお、10万行のリポジトリを前に「何も分からない」と感じるのは正常です。 5年目の人が別チームのコードに入っても同じことを感じます。 違うのは分からないまま前に進む手順を持っているかだけで、この章はその手順の話です。

全部読もうとしない

最初に捨ててほしい発想が「まず全体を理解してから作業しよう」です。 これは永久に終わりません。そして終わらないまま1週間が過ぎます。

目的のない読書は迷子になる

読み始める前に、答えるべき問いを1つに絞ってください。

ダメな目的良い目的
このリポジトリを理解する商品検索の絞り込み条件は、どこで組み立てられているか
認証まわりを把握するログイン後に発行されるトークンの有効期限は、どこで決まっているか
このサービスを覚える注文がキャンセルされた時、在庫はどの関数で戻されるか

良い目的には共通点があります。「はい、ここです」とファイル名と行番号で答えられることです。 答えられる形の問いを立てると、探索が終わる条件がはっきりします。

問いは2種類しかない

実務で立つ問いは、だいたいこの2つに分類できます。

  1. どこで処理されているか(機能 → コードを探す。上から下へ辿る)
  2. この値はどこから来たか(値 → 出どころを探す。下から上へ辿る)

1 は入口から辿ります。2 は代入箇所を grep して、そこから呼び出し元へ遡ります。 自分が今どちらをやっているのか意識するだけで、無駄な探索が減ります。

そして**「15分探して見つからなかったら、探し方を変える」と決めておいてください。** 同じ場所を延々と往復する事故を防げます。変える先は、grep のキーワードを変える・ 入口から辿り直す・実際に動かしてログを見る、のどれかです。

リポジトリの歩き方

初めてのリポジトリに入った時の、標準的な順番です。

1. README を読む(5分)

期待しすぎてはいけませんが、次の情報があれば拾います。

  • 何をするサービスなのか(1行で説明できるか)
  • 起動方法(make dev / docker compose up など)
  • 依存する外部サービス

2. まず動かす(ここが最速)

理解より先に、手元で動く状態を作ってください。 動く環境があると、以降の全ての疑問を「試して確かめる」に変換できます。

# 起動方法の候補を探す
ls Makefile docker-compose.yml Taskfile.yml 2>/dev/null
cat Makefile | head -30
 
# package.json のスクリプトを見る(Node なら大抵ここに全部ある)
cat package.json | jq .scripts
動かないまま読み進めるのが最悪の手

環境構築で詰まると「とりあえず読むだけ読もう」としたくなります。 これが一番遅くなる選択です。

動かせないと、仮説を検証できません。「たぶんここを通る」を積み重ねた読解は、 だいたいどこかで間違っていて、最後にまとめて崩れます。 環境構築に半日かけてでも、動く状態を先に作る方が速いです。

3. ディレクトリ構成を眺める(10分)

全部開く必要はありません。どこに何があるかの地図を作るだけです。

# 2階層だけ、ノイズを除いて見る
tree -L 2 -I 'node_modules|vendor|.git|dist|build'
 
# tree がなければ
find . -maxdepth 2 -type d -not -path '*/node_modules/*' -not -path '*/.git/*'

よくある構成の読み方です。

ディレクトリ名だいたい入っているもの
cmd/ main.go src/index.tsエントリポイント
handler/ controller/ api/HTTP や gRPC の受け口
usecase/ service/ domain/ビジネスロジック
repository/ dao/ store/データベースアクセス
proto/ api/*.protogRPC のインターフェース定義
internal/Go では外部から import できない実装本体
migrations/ schema.sqlテーブル定義。データ構造はここで分かる

迷ったらまずスキーマを見てください。 migrations/ や schema.sql は、 そのサービスが何を扱っているかを最短で教えてくれます。 orders order_items shipments というテーブルがあれば、 「注文は明細を持ち、配送は別概念」という設計が3分で分かります。コードを100行読むより速いです。

4. 大きいファイルを見る

行数の多いファイルは、そのリポジトリの中心であることが多いです。

# ソースを行数順に並べる(上位20件)
find . -name '*.go' -not -path './vendor/*' | xargs wc -l | sort -rn | head -20

もう一つ有効なのが、最近よく変更されているファイルを見ることです。 活発に変わっている場所が、今のチームの関心事です。

# 直近3ヶ月で変更回数の多いファイル
git log --since='3 months ago' --name-only --pretty=format: \
  | grep -v '^$' | sort | uniq -c | sort -rn | head -20

入口を見つける

構成が分かったら、次は「自分の関心のある機能はどこから始まるか」です。

HTTP なら URL からハンドラへ

URL のパス文字列を検索します。

# ripgrep(rg)で検索。grep -r より速く、.gitignore も尊重してくれる
rg '"/api/orders' --type go
 
# ルーティング定義そのものを探す
rg 'router\.|mux\.Handle|app\.(get|post)\(' -n

ルーティングが変数で組み立てられていて引っかからない場合は、 パスの一部だけで検索します(/api/v1/orders/:id なら orders だけ)。

gRPC なら .proto から実装へ

gRPC はインターフェースが .proto に集約されているので、入口が最も分かりやすい形式です。

# サービス定義を一覧する
rg 'rpc ' --type proto
 
# 見つけたメソッド名で実装を探す
rg 'func .*CreateOrder\(' --type go
service OrderService {
  rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
}

このメソッド名でコード全体を検索すれば、サーバー実装とクライアント呼び出しの両方が出てきます。 .proto は「このサービスができることの目次」として読めます。

CLI ならコマンド定義から

# Go の cobra なら
rg 'Use:\s+"' --type go
 
# Node の commander なら
rg '\.command\(' --type ts

何よりエラーメッセージが速い

実務で最速の入口は、画面やログに出ている文字列そのものです。

# 画面に出たエラー文をそのまま検索する
rg '在庫が不足しています'
 
# 英語のメッセージなら大文字小文字を無視して部分一致
rg -i 'insufficient stock'

これで一発で該当箇所に着地できることが、驚くほど多いです。 「まずルーティングから丁寧に辿る」より、こちらを先に試してください。

文字列が見つからない時に疑うこと

検索して0件だった時、次の可能性を順に潰します。

  • 文字列が分割されている — "在庫が" + itemName + "不足" のような組み立て。特徴的な一部だけで検索する
  • 翻訳ファイルにある — locales/ja.json などにあり、コードにはキーだけがある。キー名で再検索する
  • 生成コードにある — .gitignore されていて rg の対象外。rg -u で無視設定を外す
  • 別リポジトリにある — マイクロサービスなら、その文言は隣のサービスが出している

道具を使い分ける

読む道具はいくつもありますが、それぞれ答えられる問いが違います。

道具答えられる問い強み弱み
rg(ripgrep)この文字列はどこにある?速い。設定不要。どんな言語でも効く同名の別物も全部出る
LSP の定義ジャンプこの関数の実体はどこ?型を理解して1箇所に飛べる設定が要る。動的言語では外すことがある
LSP の参照検索これを使っているのは誰?影響範囲を洗うのに必須同一リポジトリ内しか見えない
git log -Sこの処理はいつ入った?追加・削除された時点を特定できる変更が多いと結果も多い
git blameこの行はなぜこうなった?PR とコミットに辿り着ける整形コミットで上書きされていることがある
デバッガ本当にここを通る?実際の値と経路が見える準備に手間がかかる
ログ追加同上(簡易版)どんな環境でもできる再ビルド・再起動が要る

rg の実用オプション

rg 'CreateOrder'                 # 全体から検索
rg -t go 'CreateOrder'           # Go のファイルだけ
rg -n -C 3 'CreateOrder'         # 行番号と前後3行を表示
rg -l 'CreateOrder'              # ファイル名だけ(全体像を掴む時)
rg -w 'user'                     # 単語単位(username にマッチしない)
rg 'CreateOrder' -g '!*_test.go' # テストを除外する
rg --stats 'CreateOrder'         # 件数の統計。多すぎる時に絞り込みの判断材料になる

ファイル名だけを先に出す -l は、探索の初手として優秀です。 20ファイルに散っているのか、2ファイルに固まっているのかで、次の動き方が変わります。

LSP は「構造で追う」ための道具

grep は文字列しか見ませんが、LSP(Language Server Protocol)は型を理解しています。 VS Code なら次のキーで使えます。

操作キー使う場面
定義へ移動F12この関数の中身が見たい
参照を検索Shift+F12これを変えたら誰が壊れるか知りたい
呼び出し階層Shift+Alt+H呼び出し元を何段も遡りたい
シンボル検索Cmd+T名前は知っているがファイルが分からない
ファイル内シンボル一覧Cmd+Shift+O巨大なファイルの目次を見る
grep と LSP の使い分け

文字列に心当たりがあるなら grep、構造を辿るなら LSP です。

Get のような一般的すぎる名前を grep すると数百件出て使い物になりませんが、 定義ジャンプなら1箇所に飛べます。逆に、設定ファイルや SQL に埋まっている名前は LSP では追えないので grep の出番です。

両方を行き来できる人が、一番速く読めます。

読むだけでは分からない。動かす

コードを目で追って組み立てた理解は、必ずどこかで間違っています。 条件分岐のどちらを通るか、ループが何回回るか、その値が nil かどうかは、動かさないと分かりません。

ログを1行入れる

最も手軽で、最も効きます。

log.Printf("[debug] ここ通った order=%+v items=%d", order, len(items))

確認したいのは主にこの3つです。

  1. 通るかどうか(分岐のどちらに入ったか)
  2. 値が何か(期待した中身が入っているか)
  3. 何回通るか(ループやリトライの回数)

入れたログは、PR を出す前に必ず消してください。 [debug] のような目印を付けておくと rg '\[debug\]' で一括確認できます。 本番に残ると、ログが埋まって本当に必要な情報が見えなくなります。 個人情報を含んでいた場合は、それ自体が事故になります。

テストを1本だけ走らせる

そのコードを動かす最短の手段は、既にあるテストを1本走らせることです。 サーバーを起動する必要も、画面を操作する必要もありません。

# Go: 特定のテストだけ、詳細ログ付きで
go test ./internal/order/... -run TestCreateOrder -v
 
# Node(jest / vitest)
npx vitest run -t '注文を作成できる'
 
# Python
pytest tests/test_order.py::test_create_order -vv

テストコードはその関数の使い方が書かれた実例でもあります。 引数に何を渡し、何が返るのかが具体的に分かるので、仕様書より役に立つことがよくあります。

デバッガで止める

「なぜかこの値になる」の調査は、デバッガが圧倒的に速いです。 ログは1回の実行で1つの仮説しか試せませんが、デバッガは止めた場所で何でも見られます。

# Go
dlv test ./internal/order -- -test.run TestCreateOrder
 
# Node
node --inspect-brk ./node_modules/.bin/vitest run
 
# Python
python -m pdb -m pytest tests/test_order.py

エディタの GUI デバッガでも同じことができます。詳しくは次章で扱います。

変更の影響範囲を洗う

読む目的の半分は、理解ではなく**「これを変えたら何が壊れるか」を知ること**です。 ここを飛ばした PR が、本番障害になります。

手順

# 1. その名前を使っている箇所を全部出す(テストも含める)
rg -w 'CalculateShippingFee'
 
# 2. 定義元でエディタの「参照を検索」(Shift+F12)を実行する
#    → grep で拾えない、型経由の間接的な使用が見つかる
 
# 3. 設定ファイル・SQL・テンプレートも見る(LSP は追ってくれない)
rg 'shipping_fee' -g '*.sql' -g '*.yaml' -g '*.json'
 
# 4. テストを全部走らせる
go test ./...

呼び出し元を遡る

1段見て終わらせないでください。関数 A を変えるなら、A を呼ぶ B、B を呼ぶ C まで見ます。 どこかで「HTTP ハンドラ」や「バッチのエントリポイント」に到達したら、そこが終点です。

終点が分かると、「この変更は管理画面にしか影響しない」「いや、日次バッチも通る」といった 具体的な影響範囲の説明ができます。これは PR の説明にそのまま書けます。

gRPC と API は、外から呼ばれている前提で考える

モノリスなら参照検索で呼び出し元を全部洗えます。 しかしマイクロサービスでは、呼び出し元は別リポジトリにいます。

  • .proto のフィールド削除・番号変更は、知らないサービスを壊します
  • REST のレスポンスからフィールドを消すと、フロントや別チームのバッチが壊れます
  • 「このリポジトリ内では誰も使っていない」は、使われていない証明にはなりません

外部に公開されているインターフェースを変える時は、必ず「誰が呼んでいますか」を聞いてください。 スキーマと RPCとマイクロサービスの実務で扱った、後方互換の話がそのまま効いてきます。

ある関数のシグネチャを変更したいと考えています。リポジトリ内を参照検索したところ、呼び出し元は3箇所だけでした。次にやるべきことは?

git の履歴から意図を読む

コードは何をしているかしか語りません。 なぜそうなっているかは、ほぼ確実に git の履歴の中にあります。

この行はなぜこうなった

# 1. 該当行を最後に変えたコミットを特定する
git blame -L 40,60 internal/order/service.go
 
# 2. そのコミットの全体を見る(-L で出た hash を使う)
git show a1b2c3d
 
# 3. マージコミット経由で PR を探す
git log --merges --ancestry-path a1b2c3d..HEAD | head -20

git show で見るべきは差分そのものより、コミットメッセージです。 「決済APIが稀に504を返すためリトライを追加」と書いてあれば、 その一見無駄なリトライを消してはいけない理由が分かります。

整形コミットに埋もれた時

git blame の結果が全部「フォーマッタ適用」だった、というのはよくあります。

# 空白の変更を無視して blame する
git blame -w internal/order/service.go
 
# 行の移動・コピーも追跡する(強力。少し遅い)
git blame -w -C -C -M internal/order/service.go

この処理はいつ入った

git log -S は、指定した文字列が追加または削除されたコミットだけを出します。 「この設定値はいつから 3 なんだろう」を調べる時の定番です。

# 文字列の増減があったコミットを探す(pickaxe と呼ばれます)
git log -S 'maxRetryCount' --oneline
 
# 正規表現版。出現回数の変化ではなく、マッチする行の変化を見る
git log -G 'timeout.*1500' --oneline
 
# ファイルの全変更を差分付きで(リネームも追う)
git log -p --follow internal/order/service.go

PR に辿り着く

コミットメッセージが Merge pull request #482 なら、その PR を読みます。 設計の議論はコードではなく PR のコメント欄に残っています。

# GitHub CLI があれば、コミットから PR を辿れる
gh pr list --search 'a1b2c3d' --state merged
gh pr view 482 --comments
「なぜ」はコードに書かれていない

新人が最も驚くのがここです。

  • なぜこのテーブルは正規化されていないのか
  • なぜこの API だけタイムアウトが 30 秒なのか
  • なぜここに一見無意味な sleep(100) があるのか

答えは全部、過去の障害・過去の要件・外部サービスの癖です。 そしてそれはコードには書かれていません。git の履歴、PR、あるいは人の記憶の中にあります。

だから「変だな」と思ったら、消す前に git blame を打つ。これが習慣になると事故が減ります。

読みにくいコードに出会った時

必ず出会います。1000行の関数、意味の分からない変数名、3重にネストした三項演算子。

順番を守る

  1. まず自分の理解不足を疑う — 知らないライブラリの規約や、言語のイディオムかもしれません
  2. 履歴を見る — git blame と git log。理由が書いてあることがあります
  3. それでも分からなければ聞く — ここまで来たら聞くべきです

聞き方が大事

避けたい: 「このコード、意味なくないですか?」
避けたい: 「なんでこんな書き方してるんですか?」

良い:   「service.go の 120 行目のリトライ処理について教えてください。
        git blame で PR #482 まで辿って、決済APIの504対策だと理解しました。
        現在も同じ理由で必要、という認識で合っていますか?」

違いは2つです。調べた形跡があることと、相手を責める形になっていないことです。

そのコードには、たいてい歴史があります

「これ意味なくないですか」と言った相手が、そのコードを書いた本人であることは普通にあります。 そして多くの場合、当時の制約の中では合理的な判断でした。

  • 当時はそのライブラリにバグがあった
  • 特定の大口顧客の要件だった
  • 深夜の障害対応で、まず止血を優先した

書いた人が無能だったケースは、あなたが思うよりずっと少ないです。 「なぜこうなったか教えてください」と聞けば、たいてい面白い話が返ってきます。

読んだことを残す

理解は驚くほど早く消えます。3日前に完全に理解した処理を、3日後には忘れています。

何を残すか

残すもの形式誰のためか
処理の流れ矢印だけの簡単な図3ヶ月後の自分
ファイルの対応表「機能 → ファイルパス」の表次の新人
詰まった点と解決箇条書き同じ場所で詰まる全員

図は凝る必要がありません。テキストで十分です。

POST /api/orders
  → handler/order.go: CreateOrder()        リクエスト検証
    → usecase/order.go: Create()           在庫確認 → 注文作成 → 決済
      → repository/order.go: Insert()      orders / order_items に INSERT
      → client/payment.go: Charge()        外部決済API(タイムアウト1.5秒、2回リトライ)

これを PR の説明に貼るだけで、レビュアーの負担が大きく下がります。

新人だからこそできる貢献

読んで分からなかった箇所は、あなたの能力の問題とは限りません。 ドキュメントが古いか、コードが読みにくいかのどちらかである可能性が十分にあります。

そしてその違和感に気づけるのは、新しく入った人だけです。 半年もすれば、あなたも「そういうもの」として受け入れてしまいます。

気づいたこと出せる貢献
README の手順で環境が立たなかったREADME を直す PR。最初の貢献として最適
関数の意図が分からず30分溶かしたWHY のコメントを1行足す PR
ディレクトリの役割が分からなかった構成の説明を README か図に追加する
用語が社内でしか通じない用語集にエントリを足す

ただし、詰まった記録はその日のうちに書いてください。 「分からなかったこと」は、分かった瞬間に価値が消えます。 分かってしまうと、何が分からなかったのかを思い出せなくなるからです。 読みながら、分からない箇所をその場でメモに書き出す。 そのメモが、そのままドキュメント改善の PR になります。

担当機能のコードを読んでいて、明らかに不要に見える 500ms の sleep を見つけました。どうしますか。

30分ルール

エンジニアとしての立ち振る舞いでも触れましたが、コードを読む場面ではこう運用します。

0-15分   自分で探す(grep、入口から辿る、履歴を見る)
15-30分  探し方を変える(動かす、ログを入れる、テストを走らせる)
30分〜   人に聞く

聞く時は、何を試したかをセットで伝えます。

注文キャンセル時の在庫戻し処理を探しています。 rg '在庫を戻' と CancelOrder の参照検索を試しましたが見つかりませんでした。 別サービスが担当している可能性はありますか?

これなら30秒で答えが返ってきます。「分かりません」だけだと、 相手はまず状況の確認から始めることになり、5分かかります。

実務の落とし穴まとめ

  1. 全体を理解してから始めようとする — 終わらない。答えられる問いを1つ立てる
  2. 動かさずに読む — 目で追った理解は必ずどこかで間違っている
  3. エラーメッセージで grep しない — 最速の入口を使い損ねている
  4. リポジトリ内の参照検索だけで影響範囲を判断する — gRPC / API は外から呼ばれている
  5. git blame を打たずに書き直す — 直っていたバグを復活させる
  6. 「これ意味なくないですか」と言う — たいてい歴史的経緯がある。まず経緯を聞く
  7. 理解したことを残さない — 3ヶ月後の自分が同じ場所で同じ時間を溶かす

まとめ

  • 仕事時間の大半は読む時間。読む速度がそのまま仕事の速度になる
  • 全部読まない。ファイル名と行番号で答えられる問いを1つ立ててから読む
  • 順番は README → まず動かす → ディレクトリ構成 → 入口 → 該当処理
  • 入口は URL・.proto・コマンド定義から。エラーメッセージの grep が最速なことが多い
  • 道具は使い分ける。文字列は rg、構造は LSP、経緯は git blame と git log -S
  • 読むだけでは分からない。ログを入れる・テストを1本走らせる・デバッガで止める
  • 影響範囲は参照検索・grep・テストで洗う。外部から呼ばれている前提を忘れない
  • 「なぜこうなっているか」はコードに書かれていない。履歴と PR と人の中にある
  • 読みにくいコードには歴史がある。調べてから、責めない形で聞く
  • 理解したことを残す。詰まった箇所は、ドキュメント改善の PR にできる

公式ドキュメント

迷ったら一次情報に戻ってください。

章末問題

「注文完了メールが送られない」というバグ調査を任されました。最初にやるべきことは?

`git blame` と `git log -S` の使い分けとして正しいのは?

次の章では、読んで見当をつけた仮説を確実に検証するための技術—— ログの読み方、デバッガの操作、そして再現手順の作り方を扱います。

読み終わったら記録しておくと、目次で進み具合が分かります。