Learning Code Readability from Kubernetes
Grace Collins
Solutions Engineer · Leapcell

In the Kubernetes codebase, various theories and techniques of code encapsulation are put into practice. As a result, while reading the code, you can intuitively deduce what you want to understand, and you can also quickly grasp the intent behind the code.
Within the Kubernetes source code, excellent comments and variable naming further help developers understand the design intentions. So, what can we learn from the comments and variable naming in the Kubernetes source code?
On Variables
Variable Names: Not the Longer, the Better
If variable names are required to express their meaning precisely, one inevitable problem is that they may become too long. However, when a very long variable name appears repeatedly in the code, it feels just like having many “drivers” named Belyozhsky and Tversky—it gives you a headache.
To avoid confusion caused by such overly precise and repetitive naming, we can leverage semantic context to help concise variable names express more meaning.
func (q *graceTerminateRSList) remove(rs *listItem) bool{ //... }
In the definition of Kubernetes’s graceTerminateRSList
structure, we don’t need to write graceTerminateRealServerList
, because when referring to the corresponding listItem
, the full name is already defined inside.
type listItem struct { VirtualServer *utilipvs.VirtualServer RealServer *utilipvs.RealServer }
So, in this context, rs
can only refer to realServer
, not to replicaSet
or anything else. If there’s a chance for such ambiguity, then you shouldn’t use abbreviations like this.
Also, in the remove
method of graceTerminateRSList
, we don’t need to name it removeRS
or removeRealServer
, because the parameter signature already has rs *listItem
. Thus, this method can only remove rs
; adding rs
in the method name would be redundant.
When naming, try to let shorter names carry more meaning.
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
doesn’t convey more information than i
, and value
isn’t better than v
. So, in this example, abbreviations can be used as substitutes. However, abbreviations are not always beneficial; whether to use them depends on whether ambiguity will arise in the specific scenario.
Variable Names Should Avoid Ambiguity
Suppose you want to express the number of users participating in an event (an int
type). Using userCount
is better than using user
or users
. This is because user
could refer to a user object, and users
might refer to a slice of user objects—using either can cause ambiguity.
Let’s look at another example. min
in some contexts can mean “minimum” (the smallest value) or “minutes”. If it’s easy to confuse the two in certain scenarios, it’s better to use the full word instead of an abbreviation.
// 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 this example, min
can refer to the lowest product price, but also to the shortest remaining minutes for a promotion. In such cases, avoid abbreviations so that you can clearly distinguish whether you’re finding the minimum price or the minimum minutes.
Variable Names with the Same Meaning Should Remain Consistent
Variable names that represent the same meaning throughout a project should be kept as consistent as possible. For example, if you write user ID as UserId
in the project, you shouldn’t change it to Uid
elsewhere when copying or reusing the variable, as this may cause confusion about whether UserId
and Uid
refer to the same thing.
Don’t underestimate this issue—sometimes, because multiple systems all have a user ID, we might need to store all of them. If we don’t add prefixes for differentiation, it will be difficult to know which one to use when needed.
For instance, suppose User A is a buyer, who bought a product from a seller, and the product is delivered by a driver.
Here, we encounter three user IDs: buyer, seller, and driver.
At this point, we can distinguish them by adding module prefixes: BuyerId
, SellerId
, and DriverId
.
And, as much as possible, we shouldn’t abbreviate these, since they are already concise enough. If we abbreviate the function parameter SellerId
to Sid
, then when we later introduce a shop ID (ShopId
), we might wonder whether Sid
refers to SellerId
or ShopId
. If a seller happens to fill in ShopId
with SellerId
, this could lead to bugs in production.
On Comments
Comments Should Explain What Code Cannot Express
When the internal logic of a function is too complex, we can use comments to save code readers from having to dig into the details, thereby saving time and serving as a guide through the code.
The synchronization Pod loop in Kubernetes is quite complex, so comments are used to explain the method.
// 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 { }
At the very beginning, the comment explains that this function processes Pod information received from various channels and dispatches them to the appropriate handling logic. The comment also summarizes the general logic for handling each channel.
If the code is not particularly complex, and its intent can be understood just by reading the code itself, there’s no need to add comments.
In the following example, when a user signs in, ordinary VIPs get a basic 10 points, and VIP users get an additional 100 points.
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 }
The code itself already shows the function’s intent, and the logic is straightforward, so there’s no need for additional comments.
At the same time, when business requirements are still unstable, it’s better to only add comments for key operations that may cause ambiguity. If business logic changes frequently and internal logic is updated but the comments are not, it can mislead readers.
However, if the code is overly complex, it is often better to refactor or abstract it into methods rather than rely solely on guiding comments. Let’s look at an example from Kubernetes where the Kubelet handles configuration signals (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) } } }
By abstracting each event operation into its own method, you avoid having all the logic laid out flat within the switch branches.
For example, the operation for kubetypes.ADD
is encapsulated in the HandlePodAdditions
method, making the code as a whole resemble a directory.
If you want to understand the process for adding Pods, you can just look directly at the HandlePodAdditions
method.
Let’s look at another example—the comment for the Run
method 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: } }
Here, the comment tells us two things that are not directly visible from the method body:
- If
Loop
is not running, the signal to execute will be deferred indefinitely, since there is no consumer to process it. - If
Run
is called while there is already a queued request, the new signal might be dropped—the method only guarantees that it will try to run the function as soon as possible from now on.
These two pieces of information can’t be seen just by reading the code itself. The author tells us these hidden details through comments, helping us quickly understand important usage considerations for this method.
Now, let’s look at an example involving regular expression compilation:
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) }
We can see here that the regular expression passed in is processed by decodeUnicode
. Let’s look at this method:
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 }
Looking at this method alone, we can see it escapes the passed-in string, but we don’t know why that’s needed or what could happen if we don’t do it—this leaves future maintainers confused. Now, let’s add the corresponding comment and review the method again:
// 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) { //... }
Now everything becomes clear: when Go parses regular expressions for CJK characters, if the regex string isn’t escaped, passing patterns like [\\u4e00-\\u9fa5]
can cause a panic.
This single line of comment not only immediately clarifies the intention behind the decoding, but also warns later developers not to manipulate the escaped string carelessly, preventing new bugs.
In code, variable names and comments are the parts closest to natural language, so they’re also the easiest to understand. If we carefully deliberate over these aspects, readability will improve dramatically.
Blank lines are also a kind of comment—they logically split the code, indicating to the reader that a section of logic is complete.
For example, in the decodeUnicode
method above, blank lines separate the matching regex that needs preprocessing, the main processing loop, and the final return statement. Visually, this divides the code into three sections, making it more intuitive and clear.
Let’s look at an example from Kubernetes where graceTerminateRSList
checks whether an RS exists:
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 }
Here, a blank line is inserted after the locking logic, indicating that the lock/unlock actions are done, and the next section is for checking existence. This visually separates the logic, so readers can focus more on the latter logic and quickly grasp the main point.
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 }
If you remove the blank line, it becomes much harder to quickly see the focus of the method when jumping into the code.
Unused Code Should Be Deleted, Not Just Commented Out
Most of the time, people comment out code because they hope to reuse it conveniently in the future.
But there’s another situation: you comment it out thinking you’ll use it later, but when that time comes, the code is no longer compatible with the current version and may introduce bugs. You’ll have to rewrite it anyway.
So it’s better to delete unused code from the start. If you need it again in the future, you can use git commit
history to find the code and rewrite it. At that point, you can optimize it during testing instead of being distracted by lots of commented-out code.
This principle also applies when writing Kubernetes YAML files:
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
When reading YAML definitions like this, large blocks of useless comments are disruptive. If server-conf-map
has already been deleted, the comment is even more confusing. So, in our own projects, if code isn’t depended on by external parties, just delete it and use git to recover it if needed later.
When your project contains code that is depended on by third parties, adding a “deprecated” comment may be better than deleting the code. Sometimes, if you provide a package for others, deleting code outright can cause lots of errors when they upgrade. In this case, you need to guide users to switch to the latest code.
So you can add a Deprecated
comment, specifying what to use instead and what parameters to pass. Let’s look at the deprecation comment for the WithInsecure
method 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() }) }
This comment clearly tells us what method to use instead. And because WithTransportCredentials
requires parameters, the author tells us exactly how to pass them.
This makes it much easier for users to replace old methods and adopt the new features.
In Conclusion
Let’s review what we’ve learned from this article:
- How to use the right amount of abbreviation for variable and function names, according to context.
- Comments should express what code itself cannot.
- Use appropriate guiding comments to help future contributors read the code—but even better, use method extraction to make code self-expressive.
- Delete code that can be removed instead of commenting it out. For code that can’t be commented out (due to third-party dependencies), use clear deprecation comments to help users switch quickly to new methods.
We are Leapcell, your top choice for hosting Go projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the Documentation!
Follow us on X: @LeapcellHQ