Kubernetes isn't just a deployment target. Here's how to program against it with the client-go library.
· 3 min read

Exploring Kubernetes: A Go Library Worth Knowing


Most Go developers treat Kubernetes as a place to put things. You write a service, someone hands you a namespace, you apply some YAML and move on. That undersells it. With over 119,000 stars on GitHub it’s one of the most popular Go projects ever built, and it’s also a library you can program against directly. That second part is the bit worth knowing.

Why Kubernetes is written in Go

Kubernetes is a CNCF graduated project, and the Cloud Native Computing Foundation has standardised on Go for most of what it hosts. The reasons are boring in the best way. Go compiles to a single binary. Concurrency support is genuinely good. Deployment is trivial.

Those traits matter for infrastructure software. A system that runs containers across thousands of nodes needs to be fast, reliable, and easy to distribute. Go fits.

Getting started with client-go

The client-go library is the official Go client for Kubernetes. It lets you create, read, update, and delete Kubernetes resources from your own programs.

First, install it:

go get k8s.io/client-go@latest

Here’s how to connect to a cluster and list all pods:

package main

import (
    "context"
    "fmt"
    "log"

    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/clientcmd"
)

func main() {
    // Load kubeconfig from default location
    config, err := clientcmd.BuildConfigFromFlags("", 
        clientcmd.RecommendedHomeFile)
    if err != nil {
        log.Fatal(err)
    }

    // Create the clientset
    clientset, err := kubernetes.NewForConfig(config)
    if err != nil {
        log.Fatal(err)
    }

    // List pods in all namespaces
    pods, err := clientset.CoreV1().Pods("").List(
        context.Background(), 
        metav1.ListOptions{},
    )
    if err != nil {
        log.Fatal(err)
    }

    for _, pod := range pods.Items {
        fmt.Printf("Pod: %s in namespace %s\n", 
            pod.Name, pod.Namespace)
    }
}

Notice how we pass context to the List call. The API server won’t always answer quickly, so timeouts and cancellation matter here more than in most client code.

Creating resources programmatically

Listing is just the start. You can create entire deployments from Go:

package main

import (
    "context"
    "log"

    appsv1 "k8s.io/api/apps/v1"
    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/clientcmd"
)

func main() {
    config, err := clientcmd.BuildConfigFromFlags("", 
        clientcmd.RecommendedHomeFile)
    if err != nil {
        log.Fatal(err)
    }

    clientset, err := kubernetes.NewForConfig(config)
    if err != nil {
        log.Fatal(err)
    }

    replicas := int32(3)
    deployment := &appsv1.Deployment{
        ObjectMeta: metav1.ObjectMeta{
            Name: "my-app",
        },
        Spec: appsv1.DeploymentSpec{
            Replicas: &replicas,
            Selector: &metav1.LabelSelector{
                MatchLabels: map[string]string{"app": "my-app"},
            },
            Template: corev1.PodTemplateSpec{
                ObjectMeta: metav1.ObjectMeta{
                    Labels: map[string]string{"app": "my-app"},
                },
                Spec: corev1.PodSpec{
                    Containers: []corev1.Container{
                        {
                            Name:  "app",
                            Image: "nginx:latest",
                        },
                    },
                },
            },
        },
    }

    _, err = clientset.AppsV1().Deployments("default").Create(
        context.Background(),
        deployment,
        metav1.CreateOptions{},
    )
    if err != nil {
        log.Fatal(err)
    }

    log.Println("Deployment created!")
}

Yes, it’s verbose. The Go structs mirror the YAML one for one, pointers and all. The upside is the compiler catches the typos that YAML would wave straight through.

Watching for changes

Kubernetes has a watch API that lets you react to changes as they happen:

func watchPods(clientset *kubernetes.Clientset) error {
    watcher, err := clientset.CoreV1().Pods("default").Watch(
        context.Background(),
        metav1.ListOptions{},
    )
    if err != nil {
        return err
    }
    defer watcher.Stop()

    for event := range watcher.ResultChan() {
        pod, ok := event.Object.(*corev1.Pod)
        if !ok {
            continue
        }
        fmt.Printf("Event: %s, Pod: %s\n", 
            event.Type, pod.Name)
    }
    return nil
}

This pattern is the foundation of Kubernetes operators: watch for changes, react accordingly. It’s also a good example of Go’s concurrency patterns in action, along with the ways they can go wrong.

Best practices

A few things worth flagging from working with client-go. Use contexts properly and set timeouts on anything long-running. Handle errors as though the network is unreliable, because it is. If you’re reading the same resources repeatedly, use informers; they cache locally and spare both you and the API server a lot of traffic. And test against a real cluster with kind or minikube, since fakes hide too much.

When you’re ready to build a proper operator, the ecosystem provides controller-runtime, which layers higher-level abstractions on top of client-go.

Wrapping up

Start small. List some pods, create a deployment, break something in a cluster you own. The watch pattern above scales all the way up to the controllers that run production platforms, and it’s the same library at every level. That’s the thing I find most satisfying about this ecosystem: the code you script with on a Tuesday afternoon is the same code the whole platform is built from.