Kubernetesから学ぶコード読解術
Wenhao Wang
Dev Intern · Leapcell

Kubernetesのコードベースでは、コードのカプセル化に関するさまざまな理論やテクニックが実際に活用されています。その結果、コードを読む際に、何を理解したいかを直感的に推測でき、コードの背後にある意図を素早く把握できます。
Kubernetesのソースコード内では、優れたコメントと変数名が、開発者が設計意図を理解するのにさらに役立ちます。では、Kubernetesのソースコードのコメントと変数名から、私たちは何を学ぶことができるでしょうか?
変数について
変数名:長ければ良いというものではない
変数名がその意味を正確に表現する必要がある場合、避けられない問題の1つは、名前が長くなりすぎることです。しかし、非常に長い変数名がコード内で繰り返し現れると、まるでBelyozhskyとTverskyという名前の「運転手」がたくさんいるかのように感じられ、頭痛の種になります。
このような過度に正確で反復的な命名による混乱を避けるために、セマンティックなコンテキストを活用して、簡潔な変数名がより多くの意味を表現できるようにすることができます。
func (q *graceTerminateRSList) remove(rs *listItem) bool{ //... }
KubernetesのgraceTerminateRSList
構造体の定義では、graceTerminateRealServerList
と書く必要はありません。なぜなら、対応するlistItem
を参照するとき、フルネームはすでに内部で定義されているからです。
type listItem struct { VirtualServer *utilipvs.VirtualServer RealServer *utilipvs.RealServer }
したがって、このコンテキストでは、rs
はrealServer
のみを指し、replicaSet
や他のものを指すことはできません。そのような曖昧さが生じる可能性がある場合は、このような省略形を使用すべきではありません。
また、graceTerminateRSList
のremove
メソッドでは、removeRS
やremoveRealServer
と名付ける必要はありません。なぜなら、パラメータのシグネチャにはすでにrs *listItem
が含まれているからです。したがって、このメソッドはrs
のみを削除できます。メソッド名にrs
を追加するのは冗長です。
命名するときは、短い名前がより多くの意味を持つように心がけてください。
func CountNumber(nums []int, n int) (count int) { for i := 0; i < len(nums); i++ { // 代入したい場合は、v := nums[i] if nums[i] == n { count++ } } return } func CountNumberBad(nums []int, n int) (count int) { for index := 0; index < len(nums); index++ { value := nums[index] if value == n { count++ } } return }
index
はi
よりも多くの情報を伝えていませんし、value
はv
よりも優れていません。したがって、この例では、省略形を代わりに使用できます。ただし、省略形が常に有益であるとは限りません。それらを使用するかどうかは、特定のシナリオで曖昧さが生じるかどうかに依存します。
変数名は曖昧さを避けるべき
イベントに参加しているユーザーの数(int
型)を表現したいとします。userCount
を使用する方が、user
やusers
を使用するよりも優れています。なぜなら、user
はユーザーオブジェクトを指す可能性があり、users
はユーザーオブジェクトのスライスを指す可能性があるからです。どちらを使用しても曖昧さを引き起こす可能性があります。
別の例を見てみましょう。min
は、コンテキストによっては「minimum」(最小値)または「minutes」(分)を意味することがあります。特定のシナリオで2つが混同しやすい場合は、省略形を使用するのではなく、完全な単語を使用する方が良いでしょう。
// 最小価格と残りのプロモーション時間を計算します func main() { // 製品価格のリスト prices := []float64{12.99, 9.99, 15.99, 8.49} // 各製品の残りのプロモーション時間(分) remainingMinutes := []int{30, 45, 10, 20} // min := findMinPrice(prices) // 変数「min」:最小価格を指します minPrice := findMinPrice(prices) fmt.Printf("最も安い製品価格:$%.2f\n", min) // min = findMinTime(remainingMinutes) // 変数「min」:最短の残り時間を指します remainingMinute := findMinTime(remainingMinutes) fmt.Printf("最短の残りのプロモーション時間:%d 分\n", min) }
この例では、min
は最も安い製品価格を指すことも、プロモーションの最短の残り時間を指すこともできます。このような場合では、省略形を避け、最小価格を求めているのか、最小時間を求めているのかを明確に区別できるようにしてください。
同じ意味を持つ変数名は一貫性を保つべき
プロジェクト全体で同じ意味を表す変数名は、できる限り一貫性を保つべきです。たとえば、プロジェクトでユーザーIDをUserId
と書いている場合、変数をコピーまたは再利用するときに、別の場所でそれをUid
に変更すべきではありません。これは、UserId
とUid
が同じものを指しているのかどうかについて混乱を引き起こす可能性があるからです。
この問題を過小評価しないでください。複数のシステムがすべてユーザーIDを持っているため、それらすべてを保存する必要がある場合があります。区別するためのプレフィックスを追加しないと、必要なときにどれを使用すべきかわからなくなります。
たとえば、ユーザーAが購入者で、販売者から製品を購入し、製品が運転手によって配達されるとします。
ここで、購入者、販売者、運転手の3つのユーザーIDに遭遇します。
この時点で、モジュールプレフィックスを追加して区別できます:BuyerId
、SellerId
、DriverId
。
そして、できる限り、これらを省略すべきではありません。なぜなら、これらはすでに十分に簡潔だからです。関数パラメータのSellerId
をSid
に省略すると、後でショップID(ShopId
)を導入したときに、Sid
がSellerId
を指すのかShopId
を指すのか疑問に思うかもしれません。販売者がたまたまShopId
にSellerId
を入力した場合、本番環境でバグが発生する可能性があります。
コメントについて
コメントはコードが表現できないことを説明すべき
関数の内部ロジックが複雑すぎる場合、コメントを使用して、コードの読者が詳細を掘り下げる必要がないようにし、それによって時間を節約し、コードを案内する役割を果たすことができます。
Kubernetesの同期Podループは非常に複雑であるため、コメントを使用してメソッドを説明しています。
// syncLoopIteration reads from various channels and dispatches pods to the // given handler. // // ...... // // With that in mind, in truly no particular order, the different channels // are handled as follows: // // - configCh: dispatch the pods for the config change to the appropriate // handler callback for the event type // - plegCh: update the runtime cache; sync pod // - syncCh: sync all pods waiting for sync // - housekeepingCh: trigger cleanup of pods // - health manager: sync pods that have failed or in which one or more // containers have failed health checks func (kl *Kubelet) syncLoopIteration(ctx context.Context, configCh <-chan kubetypes.PodUpdate, handler SyncHandler, syncCh <-chan time.Time, housekeepingCh <-chan time.Time, plegCh <-chan *pleg.PodLifecycleEvent) bool { }
冒頭で、コメントはこの関数がさまざまなチャネルから受信したPod情報を処理し、それらを適切な処理ロジックにディスパッチすることを説明しています。コメントは、各チャネルを処理するための一般的なロジックも要約しています。
コードが特に複雑ではなく、その意図がコード自体を読むだけで理解できる場合は、コメントを追加する必要はありません。
次の例では、ユーザーがサインインすると、通常のVIPは基本的な10ポイントを取得し、VIPユーザーは追加で100ポイントを取得します。
const ( basePoints = 10 vipBonus = 100 ) type User struct { IsVIP bool Points int } // SignIn handles user sign-in and increases points based on VIP status func (u *User) SignIn() { pointsToAdd := basePoints if u.IsVIP { pointsToAdd += vipBonus } u.Points += pointsToAdd }
コード自体がすでに関数の意図を示しており、ロジックは簡単であるため、追加のコメントは必要ありません。
同時に、ビジネス要件がまだ不安定な場合は、曖昧さを引き起こす可能性のある主要な操作に対してのみコメントを追加することをお勧めします。ビジネスロジックが頻繁に変更され、内部ロジックが更新されたにもかかわらずコメントが更新されていない場合、読者を誤解させる可能性があります。
ただし、コードが過度に複雑な場合は、単にガイダンスコメントに頼るよりも、リファクタリングするか、メソッドに抽象化する方が良い場合がよくあります。KubernetesのKubeletが構成信号(configCh
)を処理する例を見てみましょう。
func (kl *Kubelet) syncLoopIteration(...) bool { select { case u, open := <-configCh: switch u.Op { case kubetypes.ADD: klog.V(2).InfoS("SyncLoop ADD", "source", u.Source, "pods", klog.KObjSlice(u.Pods)) handler.HandlePodAdditions(u.Pods) case kubetypes.UPDATE: klog.V(2).InfoS("SyncLoop UPDATE", "source", u.Source, "pods", klog.KObjSlice(u.Pods)) handler.HandlePodUpdates(u.Pods) case kubetypes.REMOVE: klog.V(2).InfoS("SyncLoop REMOVE", "source", u.Source, "pods", klog.KObjSlice(u.Pods)) handler.HandlePodRemoves(u.Pods) case kubetypes.RECONCILE: klog.V(4).InfoS("SyncLoop RECONCILE", "source", u.Source, "pods", klog.KObjSlice(u.Pods)) handler.HandlePodReconcile(u.Pods) case kubetypes.DELETE: klog.V(2).InfoS("SyncLoop DELETE", "source", u.Source, "pods", klog.KObjSlice(u.Pods)) handler.HandlePodUpdates(u.Pods) case kubetypes.SET: // TODO: Do we want to support this? klog.ErrorS(nil, "Kubelet does not support snapshot update") default: klog.ErrorS(nil, "Invalid operation type received", "operation", u.Op) } } }
各イベント操作を独自のメソッドに抽象化することで、すべてのロジックがswitch分岐内にフラットに配置されるのを防ぎます。
たとえば、kubetypes.ADD
の操作はHandlePodAdditions
メソッドにカプセル化されており、コード全体がディレクトリのように見えます。
Podを追加するプロセスを理解したい場合は、HandlePodAdditions
メソッドを直接見れば済みます。
別の例を見てみましょう。KubernetesのBoundedFrequencyRunner
のRun
メソッドのコメントです。
// Run the function as soon as possible. If this is called while Loop is not // running, the call may be deferred indefinitely. // If there is already a queued request to call the underlying function, it // may be dropped - it is just guaranteed that we will try calling the // underlying function as soon as possible starting from now. func (bfr *BoundedFrequencyRunner) Run() { select { case bfr.run <- struct{}{}: default: } }
ここでは、コメントはメソッド本体から直接は見えない2つのことを教えてくれます。
Loop
が実行されていない場合、それを処理するコンシューマーがないため、実行のシグナルは無期限に延期されます。- すでにキューに入れられたリクエストがあるときに
Run
が呼び出された場合、新しいシグナルはドロップされる可能性があります。メソッドは、今からできるだけ早く関数を実行しようとすることを_試みる_ことのみを保証します。
これらの2つの情報は、コード自体を読むだけではわかりません。著者はこれらの隠された詳細をコメントを通じて教えてくれ、このメソッドの重要な使用上の考慮事項をすばやく理解するのに役立ちます。
次に、正規表現のコンパイルに関する例を見てみましょう。
func ExecRegex(value string, regex string) bool { regex, err := decodeUnicode(regex) if err != nil { return false } if regex == "" { return true } rx := regexp.MustCompile(regex) return rx.MatchString(value) }
ここから、渡された正規表現がdecodeUnicode
によって処理されることがわかります。このメソッドを見てみましょう。
func decodeUnicode(inputString string) (string, error) { re := regexp.MustCompile(`\\u[0-9a-fA-F]{4}`) matches := re.FindAllString(inputString, -1) for _, match := range matches { unquoted, err := strconv.Unquote(`"` + match + `"`) if err != nil { return "", err } inputString = strings.Replace(inputString, match, unquoted, -1) } return inputString, nil }
このメソッドだけを見ると、渡された文字列をエスケープしていることがわかりますが、なぜそれが必要なのか、またはそうしないとどうなるかを理解できません。これにより、将来のメンテナーは混乱します。ここで、対応するコメントを追加して、メソッドをもう一度確認してみましょう。
// decodeUnicode escapes regular expression strings to avoid panic when passing regex patterns like [\\u4e00-\\u9fa5] to match CJK characters. func decodeUnicode(inputString string) (string, error) { //... }
これで、すべてが明確になります。GoがCJK文字の正規表現を解析する場合、正規表現文字列がエスケープされていないと、[\\u4e00-\\u9fa5]
のようなパターンを渡すとパニックが発生する可能性があります。
この1行のコメントは、デコードの背後にある意図をすぐに明確にするだけでなく、後続の開発者にエスケープされた文字列を不注意に操作しないように警告し、新しいバグを防ぎます。
コードでは、変数名とコメントが自然言語に最も近い部分であるため、理解しやすいです。これらの側面を慎重に検討すると、可読性は劇的に向上します。
空白行もコメントの一種です。それらはコードを論理的に分割し、読者にロジックのセクションが完了したことを示します。
たとえば、上記のdecodeUnicode
メソッドでは、空白行は、プリプロセスが必要な一致する正規表現、メインの処理ループ、および最後のreturnステートメントを区切ります。視覚的に、これによりコードが3つのセクションに分割され、より直感的で明確になります。
KubernetesのgraceTerminateRSList
がRSが存在するかどうかを確認する例を見てみましょう。
func (q *graceTerminateRSList) exist(uniqueRS string) (*listItem, bool) { q.lock.Lock() defer q.lock.Unlock() if rs, ok := q.list[uniqueRS]; ok { return rs, true } return nil, false }
ここでは、ロックロジックの後に空白行が挿入され、ロック/アンロックアクションが完了したことを示し、次のセクションは存在チェック用です。これにより、ロジックが視覚的に分離されるため、読者は後者のロジックに集中し、要点をすばやく把握できます。
func (q *graceTerminateRSList) exist(uniqueRS string) (*listItem, bool) { q.lock.Lock() defer q.lock.Unlock() if rs, ok := q.list[uniqueRS]; ok { return rs, true } return nil, false }
空白行を削除すると、コードにジャンプしたときにメソッドの焦点をすばやく確認することがはるかに難しくなります。
未使用のコードはコメントアウトせずに削除すべき
ほとんどの場合、人々は将来便利に再利用したいと考えてコードをコメントアウトします。
しかし、別の状況があります。後で使用すると思ってコメントアウトしますが、その時が来ると、コードは現在のバージョンと互換性がなくなり、バグが発生する可能性があります。結局、書き直す必要があります。
したがって、最初から未使用のコードを削除する方が良いでしょう。将来再び必要になった場合は、git commit
の履歴を使用してコードを見つけ、書き直すことができます。その時点で、たくさんのコメントアウトされたコードに気を取られる代わりに、テスト中に最適化できます。
この原則は、Kubernetes YAMLファイルを作成するときにも適用されます。
spec: spec: # ... # Mount a configMap volume named server-conf # - name: server-conf-map # configMap: # name: server-conf-map # items: # - key: k8s-conf.yml # path: k8s-conf.yml # defaultMode: 511
このようなYAML定義を読むとき、役に立たないコメントの大きなブロックは邪魔になります。server-conf-map
がすでに削除されている場合、コメントはさらに混乱を招きます。したがって、私たち自身のプロジェクトでは、コードが外部によって依存されていない場合は、削除し、必要に応じて後でgitを使用して復元します。
プロジェクトにサードパーティによって依存されているコードが含まれている場合は、コードを削除するよりも「非推奨」コメントを追加する方が良い場合があります。場合によっては、他の人にパッケージを提供する場合、コードを完全に削除すると、アップグレード時に多くのエラーが発生する可能性があります。この場合、ユーザーが最新のコードに切り替えるように誘導する必要があります。
したがって、Deprecated
コメントを追加して、代わりに何を使用するか、および渡すパラメーターを指定できます。gRPCのWithInsecure
メソッドの非推奨コメントを見てみましょう。
// Deprecated: use WithTransportCredentials and insecure.NewCredentials() // instead. Will be supported throughout 1.x. func WithInsecure() DialOption { return newFuncDialOption(func(o *dialOptions) { o.copts.TransportCredentials = insecure.NewCredentials() }) }
このコメントは、代わりに使用するメソッドを明確に教えてくれます。また、WithTransportCredentials
にはパラメーターが必要なため、著者はそれらを正確に渡す方法を教えてくれます。
これにより、ユーザーは古いメソッドを置き換え、新しい機能をより簡単に採用できます。
結論として
この記事から学んだことを復習しましょう。
- コンテキストに応じて、変数名と関数名に適切な量の省略を使用する方法
- コメントは、コード自体が表現できないことを表現する必要があります
- 将来のコントリビューターがコードを読みやすくするために、適切なガイダンスコメントを使用する。さらに良いのは、メソッド抽出を使用してコードを自己表現的にすることです
- コメントアウトする代わりに、削除できるコードを削除します。(サードパーティの依存関係のために)コメントアウトできないコードについては、明確な非推奨コメントを使用して、ユーザーが新しいメソッドにすばやく切り替えられるようにします。
Leapcell、Goプロジェクトのホスティングに最適な選択肢です。
Leapcellは、Webホスティング、非同期タスク、Redisのための次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、またはRustで開発できます。
無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い、リクエストや料金は発生しません。
最高のコスト効率
- アイドル料金なしの従量課金制。
- 例:25ドルで、平均応答時間60msで694万リクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOps統合。
- 実用的な洞察を得るためのリアルタイムメトリクスとロギング。
簡単なスケーラビリティと高パフォーマンス
- 簡単な自動スケーリングで高並行性を処理します。
- 運用オーバーヘッドゼロ—構築に集中するだけです。
ドキュメントで詳細をご覧ください!
Xでフォローしてください:@LeapcellHQ