Updated Jul 20, 2026

05 - Kubernetes Client-Go

📋 Jump to Takeaways

🎁 What if you could build your own kubectl, Helm, or ArgoCD? They all share one library, and once you learn it, you can build anything they can.

Imports in code snippets are trimmed for brevity. See the full example linked at the end for complete, compilable source.

Client-Go Library Overview

client-go is the official Go client library for Kubernetes. Every tool that talks to a K8s cluster (kubectl, Helm, ArgoCD, custom controllers) uses it internally. The library provides typed clients for all built-in resources, dynamic clients for CRDs, and an informer framework for efficient event-driven watching.

go get k8s.io/client-go@latest
go get k8s.io/apimachinery@latest

Key packages:

Package Purpose
kubernetes Typed clientset for built-in resources
rest REST client config and transport
tools/clientcmd Load kubeconfig files
informers Cached, event-driven resource watching
dynamic Untyped client for any resource (CRDs)

Loading Kubeconfig

Your tool needs to authenticate to the API server. There are two modes:

Out-of-Cluster Configuration

For CLI tools, local development, or anything running outside the cluster:

package main

import (
	"path/filepath"

	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
)

func outOfClusterClient() (*kubernetes.Clientset, error) {
	kubeconfig := filepath.Join(homedir.HomeDir(), ".kube", "config")

	config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
	if err != nil {
		return nil, err
	}

	return kubernetes.NewForConfig(config)
}

In-Cluster Configuration

For pods running inside the cluster (controllers, operators, jobs):

import "k8s.io/client-go/rest"

func inClusterClient() (*kubernetes.Clientset, error) {
	config, err := rest.InClusterConfig()
	if err != nil {
		return nil, err
	}

	return kubernetes.NewForConfig(config)
}

Unified Pattern

Most tools combine both approaches:

func getClient() (*kubernetes.Clientset, error) {
	// Try in-cluster first
	config, err := rest.InClusterConfig()
	if err != nil {
		// Fall back to kubeconfig
		kubeconfig := filepath.Join(homedir.HomeDir(), ".kube", "config")
		config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
		if err != nil {
			return nil, err
		}
	}

	return kubernetes.NewForConfig(config)
}

Creating Clientsets

The Clientset groups typed clients by API group and version. You access resources through a hierarchy:

clientset, _ := kubernetes.NewForConfig(config)

// Core v1 resources
clientset.CoreV1().Pods("default")
clientset.CoreV1().Services("default")
clientset.CoreV1().ConfigMaps("my-namespace")

// Apps v1 resources
clientset.AppsV1().Deployments("default")
clientset.AppsV1().StatefulSets("default")

// All namespaces
clientset.CoreV1().Pods("")  // empty string = all namespaces

Listing Pods, Deployments, and Services

Each resource client exposes standard CRUD operations:

import (
	"context"
	"fmt"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func listResources(clientset *kubernetes.Clientset) error {
	ctx := context.Background()

	// List pods with label selector
	pods, err := clientset.CoreV1().Pods("default").List(ctx, metav1.ListOptions{
		LabelSelector: "app=web",
	})
	if err != nil {
		return err
	}

	for _, pod := range pods.Items {
		fmt.Printf("Pod: %s | Phase: %s | IP: %s\n",
			pod.Name, pod.Status.Phase, pod.Status.PodIP)
		// Output: Pod: web-abc123 | Phase: Running | IP: 10.244.0.12
	}

	// List deployments
	deployments, err := clientset.AppsV1().Deployments("").List(ctx, metav1.ListOptions{})
	if err != nil {
		return err
	}

	for _, d := range deployments.Items {
		fmt.Printf("Deployment: %s/%s | Replicas: %d/%d\n",
			d.Namespace, d.Name, d.Status.ReadyReplicas, *d.Spec.Replicas)
		// Output: Deployment: default/web-app | Replicas: 3/3
	}

	return nil
}

Watch and Informers for Real-Time Updates

Polling the API server is wasteful. Informers maintain a local cache and deliver events when resources change:

import (
	"time"

	"k8s.io/client-go/informers"
	"k8s.io/client-go/tools/cache"
)

func watchPods(clientset *kubernetes.Clientset) {
	// Create a shared informer factory (resync every 30s)
	factory := informers.NewSharedInformerFactory(clientset, 30*time.Second)

	podInformer := factory.Core().V1().Pods().Informer()

	podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
			pod := obj.(*corev1.Pod)
			fmt.Printf("[ADD] %s/%s\n", pod.Namespace, pod.Name)
			// Output: [ADD] default/web-abc123
		},
		UpdateFunc: func(oldObj, newObj interface{}) {
			pod := newObj.(*corev1.Pod)
			fmt.Printf("[UPDATE] %s/%s phase=%s\n",
				pod.Namespace, pod.Name, pod.Status.Phase)
			// Output: [UPDATE] default/web-abc123 phase=Running
		},
		DeleteFunc: func(obj interface{}) {
			pod := obj.(*corev1.Pod)
			fmt.Printf("[DELETE] %s/%s\n", pod.Namespace, pod.Name)
			// Output: [DELETE] default/web-abc123
		},
	})

	// Start all informers
	stopCh := make(chan struct{})
	factory.Start(stopCh)
	factory.WaitForCacheSync(stopCh)

	// Block forever (or until signal)
	<-stopCh
}

Informers use a single watch connection per resource type and share the cache across your application. Always prefer informers over raw Watch calls.

Creating and Patching Resources

Create resources by constructing typed objects:

import (
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func createConfigMap(clientset *kubernetes.Clientset) error {
	cm := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "app-config",
			Namespace: "default",
			Labels: map[string]string{
				"managed-by": "go-devops-tool",
			},
		},
		Data: map[string]string{
			"database_url": "postgres://db:5432/app",
			"log_level":    "info",
			"feature_flags": `{"new_ui": true, "beta_api": false}`,
		},
	}

	created, err := clientset.CoreV1().ConfigMaps("default").Create(
		context.Background(), cm, metav1.CreateOptions{},
	)
	if err != nil {
		return err
	}

	fmt.Printf("Created ConfigMap: %s (uid: %s)\n", created.Name, created.UID)
	// Output: Created ConfigMap: app-config (uid: 7a3f2c91-4e8b-11ee-be56-0242ac120002)
	return nil
}

For partial updates, use strategic merge patches:

import (
	"encoding/json"
	"k8s.io/apimachinery/pkg/types"
)

func patchDeploymentReplicas(clientset *kubernetes.Clientset, name string, replicas int32) error {
	patch := map[string]interface{}{
		"spec": map[string]interface{}{
			"replicas": replicas,
		},
	}

	patchBytes, _ := json.Marshal(patch)

	_, err := clientset.AppsV1().Deployments("default").Patch(
		context.Background(),
		name,
		types.StrategicMergePatchType,
		patchBytes,
		metav1.PatchOptions{},
	)
	return err
}

Dynamic Client for CRDs

The typed clientset only knows built-in resources. For Custom Resource Definitions, use the dynamic client:

import (
	"k8s.io/client-go/dynamic"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

func listCustomResources(config *rest.Config) error {
	dynClient, err := dynamic.NewForConfig(config)
	if err != nil {
		return err
	}

	// Define the GVR (Group/Version/Resource) for your CRD
	gvr := schema.GroupVersionResource{
		Group:    "apps.example.com",
		Version:  "v1",
		Resource: "webapps",
	}

	// List all instances
	list, err := dynClient.Resource(gvr).Namespace("default").List(
		context.Background(), metav1.ListOptions{},
	)
	if err != nil {
		return err
	}

	for _, item := range list.Items {
		name := item.GetName()
		spec, _, _ := unstructured.NestedMap(item.Object, "spec")
		fmt.Printf("WebApp: %s | Spec: %v\n", name, spec)
		// Output: WebApp: my-service | Spec: map[image:nginx:1.25 port:8080 replicas:3]
	}

	return nil
}

Cluster Resource Watcher

A Kubernetes client tool demonstrating three core client-go patterns: listing pods across all namespaces, creating a ConfigMap, and watching for real-time pod changes using a shared informer. It auto-detects in-cluster vs local kubeconfig and handles graceful shutdown on Ctrl+C.

Input: A valid kubeconfig at ~/.kube/config (or in-cluster service account when running inside a pod)

Output: A list of all pods with their status, ConfigMap creation confirmation, and a live stream of pod add/delete events

Full source: examples/cluster-resource-watcher

Key Takeaways

  • client-go is the foundation for all Go-based Kubernetes tooling. Learn it and you can build anything from CLI tools to operators
  • Always use the unified in-cluster/out-of-cluster config pattern so your tool works everywhere
  • Informers are far more efficient than polling; they maintain a local cache and deliver granular add/update/delete events
  • The dynamic client handles any resource type including CRDs without compile-time type information
  • Strategic merge patches allow surgical updates without fetching and resending the entire resource
  • All operations are context-aware. Pass cancellable contexts for proper shutdown handling

🎁 What if your code could watch for changes and automatically reconcile the cluster to match a desired state, no human intervention needed?

💻 Examples

Complete examples for this lesson. Copy and run locally.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy