Code-Lesbarkeit von Kubernetes lernen
Wenhao Wang
Dev Intern · Leapcell

In der Kubernetes-Codebasis werden verschiedene Theorien und Techniken der Code-Kapselung in die Praxis umgesetzt. Dadurch können Sie beim Lesen des Codes intuitiv ableiten, was Sie verstehen möchten, und Sie können auch schnell die Absicht hinter dem Code erfassen.
Innerhalb des Kubernetes-Quellcodes helfen exzellente Kommentare und Variablennamen den Entwicklern zusätzlich, die Designabsichten zu verstehen. Was können wir also von den Kommentaren und Variablennamen im Kubernetes-Quellcode lernen?
Über Variablen
Variablennamen: Nicht je länger, desto besser
Wenn Variablennamen ihre Bedeutung präzise ausdrücken müssen, ist ein unvermeidliches Problem, dass sie zu lang werden können. Wenn jedoch ein sehr langer Variablenname wiederholt im Code vorkommt, fühlt es sich an, als hätte man viele „Fahrer“ namens Belyozhsky und Tversky – es bereitet einem Kopfschmerzen.
Um Verwirrung durch solch übermäßig präzise und repetitive Namensgebung zu vermeiden, können wir den semantischen Kontext nutzen, um prägnante Variablennamen mehr Bedeutung ausdrücken zu lassen.
func (q *graceTerminateRSList) remove(rs *listItem) bool{ //... }
In der Definition der graceTerminateRSList
-Struktur von Kubernetes müssen wir nicht graceTerminateRealServerList
schreiben, da beim Verweis auf das entsprechende listItem
der vollständige Name bereits enthalten ist.
type listItem struct { VirtualServer *utilipvs.VirtualServer RealServer *utilipvs.RealServer }
In diesem Kontext kann sich rs
also nur auf realServer
beziehen, nicht auf replicaSet
oder etwas anderes. Wenn die Möglichkeit einer solchen Mehrdeutigkeit besteht, sollten Sie keine Abkürzungen wie diese verwenden.
Auch in der remove
-Methode von graceTerminateRSList
müssen wir sie nicht removeRS
oder removeRealServer
nennen, da die Parametersignatur bereits rs *listItem
enthält. Diese Methode kann also nur rs
entfernen; das Hinzufügen von rs
im Methodennamen wäre redundant.
Versuchen Sie bei der Namensgebung, kürzere Namen mehr Bedeutung tragen zu lassen.
func CountNumber(nums []int, n int) (count int) { for i := 0; i < len(nums); i++ { // If you want to assign, then 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
vermittelt nicht mehr Informationen als i
, und value
ist nicht besser als v
. In diesem Beispiel können also Abkürzungen als Ersatz verwendet werden. Allerdings sind Abkürzungen nicht immer von Vorteil; ob man sie verwendet oder nicht, hängt davon ab, ob im jeweiligen Szenario Mehrdeutigkeit entsteht.
Variablennamen sollten Mehrdeutigkeit vermeiden
Angenommen, Sie möchten die Anzahl der Benutzer ausdrücken, die an einer Veranstaltung teilnehmen (ein int
-Typ). Die Verwendung von userCount
ist besser als die Verwendung von user
oder users
. Dies liegt daran, dass sich user
auf ein Benutzerobjekt beziehen könnte und users
sich auf einen Slice von Benutzerobjekten beziehen könnte – die Verwendung von beidem kann zu Mehrdeutigkeit führen.
Betrachten wir ein weiteres Beispiel. min
kann in einigen Kontexten „Minimum“ (der kleinste Wert) oder „Minuten“ bedeuten. Wenn es in bestimmten Szenarien leicht ist, die beiden zu verwechseln, ist es besser, das vollständige Wort anstelle einer Abkürzung zu verwenden.
// Calculate the minimum price and the remaining promotion time func main() { // List of product prices prices := []float64{12.99, 9.99, 15.99, 8.49} // Remaining promotion time (in minutes) for each product remainingMinutes := []int{30, 45, 10, 20} // min := findMinPrice(prices) // Variable "min": refers to minimum price minPrice := findMinPrice(prices) fmt.Printf("The lowest product price: $%.2f\n", min) // min = findMinTime(remainingMinutes) // Variable "min": refers to shortest remaining time remainingMinute := findMinTime(remainingMinutes) fmt.Printf("The shortest remaining promotion time: %d minutes\n", min) }
In diesem Beispiel kann sich min
auf den niedrigsten Produktpreis, aber auch auf die kürzesten verbleibenden Minuten für eine Werbeaktion beziehen. Vermeiden Sie in solchen Fällen Abkürzungen, damit Sie klar unterscheiden können, ob Sie den Mindestpreis oder die Mindestminuten suchen.
Variablennamen mit derselben Bedeutung sollten konsistent bleiben
Variablennamen, die im gesamten Projekt dieselbe Bedeutung haben, sollten so konsistent wie möglich gehalten werden. Wenn Sie beispielsweise die Benutzer-ID im Projekt als UserId
schreiben, sollten Sie sie nicht an anderer Stelle in Uid
ändern, wenn Sie die Variable kopieren oder wiederverwenden, da dies zu Verwirrung darüber führen kann, ob sich UserId
und Uid
auf dasselbe beziehen.
Unterschätzen Sie dieses Problem nicht – manchmal müssen wir alle speichern, weil mehrere Systeme alle eine Benutzer-ID haben. Wenn wir keine Präfixe zur Unterscheidung hinzufügen, wird es schwierig zu wissen, welches wir verwenden sollen, wenn wir es brauchen.
Nehmen wir zum Beispiel an, Benutzer A ist ein Käufer, der ein Produkt von einem Verkäufer gekauft hat, und das Produkt wird von einem Fahrer geliefert.
Hier stoßen wir auf drei Benutzer-IDs: Käufer, Verkäufer und Fahrer.
An diesem Punkt können wir sie durch Hinzufügen von Modulpräfixen unterscheiden: BuyerId
, SellerId
und DriverId
.
Und wir sollten diese so weit wie möglich nicht abkürzen, da sie bereits prägnant genug sind. Wenn wir den Funktionsparameter SellerId
auf Sid
abkürzen, werden wir uns später bei der Einführung einer Shop-ID (ShopId
) möglicherweise fragen, ob sich Sid
auf SellerId
oder ShopId
bezieht. Wenn ein Verkäufer zufällig ShopId
mit SellerId
ausfüllt, könnte dies zu Fehlern in der Produktion führen.
Über Kommentare
Kommentare sollten erklären, was Code nicht ausdrücken kann
Wenn die interne Logik einer Funktion zu komplex ist, können wir Kommentare verwenden, um den Code-Lesern zu ersparen, in die Details einzutauchen, wodurch wir Zeit sparen und als Leitfaden durch den Code dienen.
Die Synchronisierungs-Pod-Schleife in Kubernetes ist recht komplex, daher werden Kommentare verwendet, um die Methode zu erklären.
// 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 { }
Gleich zu Beginn erklärt der Kommentar, dass diese Funktion Pod-Informationen verarbeitet, die von verschiedenen Kanälen empfangen werden, und sie an die entsprechende Verarbeitungslogik weiterleitet. Der Kommentar fasst auch die allgemeine Logik für die Behandlung jedes Kanals zusammen.
Wenn der Code nicht besonders komplex ist und seine Absicht allein durch das Lesen des Codes selbst verstanden werden kann, ist es nicht erforderlich, Kommentare hinzuzufügen.
Im folgenden Beispiel erhalten normale VIPs bei der Anmeldung eines Benutzers 10 Basispunkte und VIP-Benutzer erhalten zusätzlich 100 Punkte.
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 }
Der Code selbst zeigt bereits die Absicht der Funktion und die Logik ist unkompliziert, sodass keine zusätzlichen Kommentare erforderlich sind.
Gleichzeitig ist es besser, nur Kommentare für wichtige Operationen hinzuzufügen, die zu Mehrdeutigkeit führen können, wenn die Geschäftsanforderungen noch instabil sind. Wenn sich die Geschäftslogik häufig ändert und die interne Logik aktualisiert wird, die Kommentare jedoch nicht, kann dies die Leser in die Irre führen.
Wenn der Code jedoch zu komplex ist, ist es oft besser, ihn zu refaktorisieren oder in Methoden zu abstrahieren, anstatt sich ausschließlich auf Anmerkungen zu verlassen. Betrachten wir ein Beispiel aus Kubernetes, in dem das Kubelet Konfigurationssignale (configCh
) verarbeitet:
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) } } }
Indem Sie jede Ereignisoperation in eine eigene Methode abstrahieren, vermeiden Sie es, die gesamte Logik flach innerhalb der Switch-Zweige anzuordnen.
Beispielsweise ist die Operation für kubetypes.ADD
in der HandlePodAdditions
-Methode gekapselt, wodurch der Code als Ganzes einem Verzeichnis ähnelt.
Wenn Sie den Prozess zum Hinzufügen von Pods verstehen möchten, können Sie sich direkt die HandlePodAdditions
-Methode ansehen.
Betrachten wir ein weiteres Beispiel – den Kommentar für die Run
-Methode in Kubernetes’s BoundedFrequencyRunner
:
// 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: } }
Hier sagt uns der Kommentar zwei Dinge, die aus dem Methodenkörper nicht direkt ersichtlich sind:
- Wenn
Loop
nicht ausgeführt wird, wird das Signal zur Ausführung auf unbestimmte Zeit verzögert, da es keinen Konsumenten gibt, der es verarbeiten kann. - Wenn
Run
aufgerufen wird, während bereits eine Anfrage in der Warteschlange ist, kann das neue Signal verworfen werden – die Methode garantiert nur, dass sie versuchen wird, die Funktion ab sofort so schnell wie möglich auszuführen.
Diese beiden Informationen sind nicht allein durch das Lesen des Codes selbst ersichtlich. Der Autor teilt uns diese versteckten Details durch Kommentare mit und hilft uns so, wichtige Nutzungsüberlegungen für diese Methode schnell zu verstehen.
Betrachten wir nun ein Beispiel, das die Kompilierung regulärer Ausdrücke beinhaltet:
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) }
Wir können hier sehen, dass der übergebene reguläre Ausdruck von decodeUnicode
verarbeitet wird. Betrachten wir diese Methode:
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 }
Wenn wir uns diese Methode allein ansehen, können wir sehen, dass sie die übergebene Zeichenfolge maskiert, aber wir wissen nicht, warum das erforderlich ist oder was passieren könnte, wenn wir es nicht tun – das verwirrt zukünftige Wartungstechniker. Fügen wir nun den entsprechenden Kommentar hinzu und überprüfen wir die Methode erneut:
// 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) { //... }
Jetzt wird alles klar: Wenn Go reguläre Ausdrücke für CJK-Zeichen analysiert, kann das Übergeben von Mustern wie [\\u4e00-\\u9fa5]
einen Panic verursachen, wenn die Regex-Zeichenfolge nicht maskiert ist.
Diese einzelne Kommentarzeile verdeutlicht nicht nur sofort die Absicht hinter der Dekodierung, sondern warnt auch spätere Entwickler davor, die maskierte Zeichenfolge nicht achtlos zu manipulieren, um neue Fehler zu vermeiden.
Im Code sind Variablennamen und Kommentare die Teile, die der natürlichen Sprache am nächsten stehen, daher sind sie auch am einfachsten zu verstehen. Wenn wir diese Aspekte sorgfältig durchdenken, verbessert sich die Lesbarkeit erheblich.
Leere Zeilen sind auch eine Art Kommentar – sie teilen den Code logisch auf und signalisieren dem Leser, dass ein Abschnitt der Logik abgeschlossen ist.
Beispielsweise trennen im obigen decodeUnicode
-Methode leere Zeilen die übereinstimmende Regex, die vorverarbeitet werden muss, die Hauptverarbeitungsschleife und die abschließende Return-Anweisung. Visuell unterteilt dies den Code in drei Abschnitte, wodurch er intuitiver und übersichtlicher wird.
Betrachten wir ein Beispiel aus Kubernetes, in dem graceTerminateRSList
prüft, ob ein RS vorhanden ist:
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 }
Hier wird nach der Sperrlogik eine Leerzeile eingefügt, die anzeigt, dass die Sperr-/Entsperraktionen abgeschlossen sind und der nächste Abschnitt für die Überprüfung des Vorhandenseins vorgesehen ist. Dies trennt die Logik visuell, sodass sich die Leser mehr auf die letztere Logik konzentrieren und den Hauptpunkt schnell erfassen können.
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 }
Wenn Sie die Leerzeile entfernen, wird es viel schwieriger, den Fokus der Methode beim Springen in den Code schnell zu erkennen.
Unverwendeter Code sollte gelöscht und nicht nur auskommentiert werden
Meistens kommentieren die Leute Code aus, weil sie hoffen, ihn in Zukunft bequem wiederverwenden zu können.
Es gibt aber noch eine andere Situation: Sie kommentieren ihn aus, weil Sie denken, dass Sie ihn später verwenden werden, aber wenn es so weit ist, ist der Code nicht mehr mit der aktuellen Version kompatibel und kann Fehler verursachen. Sie müssen ihn sowieso neu schreiben.
Es ist also besser, ungenutzten Code von Anfang an zu löschen. Wenn Sie ihn in Zukunft wieder benötigen, können Sie den git commit
-Verlauf verwenden, um den Code zu finden und neu zu schreiben. An diesem Punkt können Sie ihn während des Tests optimieren, anstatt sich von vielen auskommentierten Codes ablenken zu lassen.
Dieser Grundsatz gilt auch beim Schreiben von Kubernetes YAML-Dateien:
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
Beim Lesen von YAML-Definitionen wie dieser sind große Blöcke nutzloser Kommentare störend. Wenn server-conf-map
bereits gelöscht wurde, ist der Kommentar noch verwirrender. Wenn also in unseren eigenen Projekten Code nicht von externen Parteien abhängig ist, löschen Sie ihn einfach und verwenden Sie git, um ihn bei Bedarf später wiederherzustellen.
Wenn Ihr Projekt Code enthält, von dem Dritte abhängig sind, ist es möglicherweise besser, einen „Deprecated“-Kommentar hinzuzufügen, als den Code zu löschen. Manchmal kann das vollständige Löschen von Code viele Fehler verursachen, wenn die Benutzer ein Upgrade durchführen, wenn Sie ein Paket für andere bereitstellen. In diesem Fall müssen Sie die Benutzer anleiten, zum neuesten Code zu wechseln.
Sie können also einen Deprecated
-Kommentar hinzufügen, der angibt, was stattdessen verwendet werden soll und welche Parameter übergeben werden sollen. Betrachten wir den Deprecation-Kommentar für die WithInsecure
-Methode in gRPC:
// 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() }) }
Dieser Kommentar teilt uns deutlich mit, welche Methode stattdessen verwendet werden soll. Und da WithTransportCredentials
Parameter benötigt, teilt uns der Autor genau mit, wie man sie übergibt.
Dies erleichtert es den Benutzern erheblich, alte Methoden zu ersetzen und die neuen Funktionen zu übernehmen.
Zusammenfassend
Lassen Sie uns noch einmal zusammenfassen, was wir in diesem Artikel gelernt haben:
- Verwendung des richtigen Maßes an Abkürzungen für Variablen- und Funktionsnamen, je nach Kontext.
- Kommentare sollten das ausdrücken, was Code selbst nicht kann.
- Verwenden Sie geeignete Anmerkungen, um zukünftigen Mitwirkenden beim Lesen des Codes zu helfen – aber noch besser: Verwenden Sie die Methodenextraktion, um den Code selbsterklärend zu machen.
- Löschen Sie Code, der entfernt werden kann, anstatt ihn auszukommentieren. Verwenden Sie für Code, der nicht auskommentiert werden kann (aufgrund von Abhängigkeiten von Drittanbietern), eindeutige Deprecation-Kommentare, um Benutzern zu helfen, schnell auf neue Methoden umzusteigen.
Wir sind Leapcell, Ihre erste Wahl für das Hosting von Go-Projekten.
Leapcell ist die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis:
Multi-Language Support
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Reaktionszeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Kein betrieblicher Overhead – konzentrieren Sie sich einfach auf das Bauen.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ