Go Interface Kapselung von Kubernetes lernen
Olivia Novak
Dev Intern · Leapcell

Verbergen von Details zu Eingabeparametern mithilfe von Schnittstellen
Wenn der Eingabeparameter einer Methode eine Struktur ist, geben die internen Aufrufe zu viele Details der Eingabe preis. In solchen Fällen können Sie die Eingabe implizit in eine Schnittstelle konvertieren, sodass die interne Implementierung nur die Methoden sieht, die sie benötigt.
type Kubelet struct{} func (kl *Kubelet) HandlePodAdditions(pods []*Pod) { for _, pod := range pods { fmt.Printf("create pods : %s\n", pod.Status) } } func (kl *Kubelet) Run(updates <-chan Pod) { fmt.Println(" run kubelet") go kl.syncLoop(updates, kl) } func (kl *Kubelet) syncLoop(updates <-chan Pod, handler SyncHandler) { for { select { case pod := <-updates: handler.HandlePodAdditions([]*Pod{&pod}) } } } type SyncHandler interface { HandlePodAdditions(pods []*Pod) }
Hier sehen wir, dass das Kubelet selbst mehrere Methoden hat:
syncLoop
: eine Schleife zur Synchronisierung des ZustandsRun
: wird verwendet, um die Listening-Schleife zu startenHandlePodAdditions
: Logik zur Behandlung von Pod-Ergänzungen
Da syncLoop
die anderen Methoden auf Kubelet nicht wirklich kennen muss, definieren wir eine SyncHandler
-Schnittstelle, lassen Kubelet diese Schnittstelle implementieren und übergeben Kubelet als Argument an syncLoop
als SyncHandler
. Dies führt dazu, dass Kubelet als SyncHandler
typisiert wird.
Nach dieser Konvertierung sind andere Methoden auf Kubelet in den Eingabeparametern nicht mehr sichtbar, sodass Sie sich beim Programmieren stärker auf die Logik in syncLoop
konzentrieren können.
Dieser Ansatz kann jedoch auch einige Probleme verursachen. Die anfängliche Abstraktion ist möglicherweise für die ersten Anforderungen ausreichend, aber wenn die Anforderungen wachsen und iterieren und wir andere Methoden auf Kubelet verwenden müssen, die nicht in der Schnittstelle enthalten sind, müssen wir entweder Kubelet explizit übergeben oder die Schnittstelle erweitern, was den Programmieraufwand erhöht und die ursprüngliche Kapselung aufbricht.
Schichtweise Kapselung und Ausblendung ist unser oberstes Ziel beim Design – sodass sich jeder Teil des Codes nur auf das konzentrieren kann, worum er sich kümmern muss.
Schnittstellenkapselung für einfachere Mock-Tests
Durch Abstraktion mit Schnittstellen können wir direkt eine Mock-Struktur für die Teile instanziieren, die uns während des Testens nicht interessieren.
type OrderAPI interface { GetOrderId() string } type realOrderImpl struct{} func (r *realOrderImpl) GetOrderId() string { return "" } type mockOrderImpl struct{} func (m *mockOrderImpl) GetOrderId() string { return "mock" }
Wenn wir uns während des Testens nicht darum kümmern, ob GetOrderId
korrekt funktioniert, können wir OrderAPI
hier direkt mit mockOrderImpl
initialisieren, und die Logik im Mock kann so komplex sein, wie nötig.
func TestGetOrderId(t *testing.T) { orderAPI := &mockOrderImpl{} // Wenn wir die Bestell-ID abrufen müssen, sie aber nicht im Fokus des Tests steht, initialisieren Sie sie einfach mit der Mock-Struktur fmt.Println(orderAPI.GetOrderId()) }
gomonkey
kann auch für Testinjektionen verwendet werden. Wenn der vorhandene Code also nicht durch Schnittstellen gekapselt wurde, können wir trotzdem Mocking erreichen, und diese Methode ist sogar noch leistungsfähiger.
patches := gomonkey.ApplyFunc(GetOrder, func(orderId string) Order { return Order{ OrderId: orderId, OrderState: delivering, } }) return func() { patches.Reset() }
Die Verwendung von gomonkey
ermöglicht ein flexibleres Mocking, da es den Rückgabewert einer Funktion direkt festlegen kann, während die Schnittstellenabstraktion nur Inhalte verarbeiten kann, die aus Strukturen instanziiert wurden.
Schnittstellenkapselung für mehrere zugrunde liegende Implementierungen
Implementierungen wie iptables
und ipvs
werden durch Schnittstellenabstraktion erreicht, da alle Netzwerkeinstellungen sowohl Service als auch Endpoint verarbeiten müssen. Daher haben sie ServiceHandler
und EndpointSliceHandler
abstrahiert:
// ServiceHandler ist eine abstrakte Schnittstelle, die zum Empfangen von Benachrichtigungen über Änderungen an Serviceobjekten verwendet wird. type ServiceHandler interface { // OnServiceAdd wird aufgerufen, wenn beobachtet wird, dass ein neues Serviceobjekt erstellt wird. OnServiceAdd(service *v1.Service) // OnServiceUpdate wird aufgerufen, wenn beobachtet wird, dass ein vorhandenes Serviceobjekt geändert wird. OnServiceUpdate(oldService, service *v1.Service) // OnServiceDelete wird aufgerufen, wenn beobachtet wird, dass ein vorhandenes Serviceobjekt gelöscht wird. OnServiceDelete(service *v1.Service) // OnServiceSynced wird aufgerufen, sobald alle anfänglichen Ereignisbehandler aufgerufen wurden und der Status vollständig in den lokalen Cache übertragen wurde. OnServiceSynced() } // EndpointSliceHandler ist eine abstrakte Schnittstelle, die zum Empfangen von Benachrichtigungen über Änderungen an Endpoint-Slice-Objekten verwendet wird. type EndpointSliceHandler interface { // OnEndpointSliceAdd wird aufgerufen, wenn beobachtet wird, dass ein neues Endpoint-Slice-Objekt erstellt wird. OnEndpointSliceAdd(endpointSlice *discoveryv1.EndpointSlice) // OnEndpointSliceUpdate wird aufgerufen, wenn beobachtet wird, dass ein vorhandenes Endpoint-Slice-Objekt geändert wird. OnEndpointSliceUpdate(oldEndpointSlice, newEndpointSlice *discoveryv1.EndpointSlice) // OnEndpointSliceDelete wird aufgerufen, wenn beobachtet wird, dass ein vorhandenes Endpoint-Slice-Objekt gelöscht wird. OnEndpointSliceDelete(endpointSlice *discoveryv1.EndpointSlice) // OnEndpointSlicesSynced wird aufgerufen, sobald alle anfänglichen Ereignisbehandler aufgerufen wurden und der Status vollständig in den lokalen Cache übertragen wurde. OnEndpointSlicesSynced() }
Dann können sie über einen Provider
injiziert werden:
type Provider interface { config.EndpointSliceHandler config.ServiceHandler }
Dies ist auch die Codierungstechnik, die ich am häufigsten bei der Arbeit an Komponenten verwende: Durch die Abstraktion ähnlicher Operationen muss der Code der oberen Ebene nach dem Ersetzen der zugrunde liegenden Implementierung nicht geändert werden.
Kapselung der Ausnahmebehandlung
Wenn wir Ausnahmen nach dem Starten von Goroutinen nicht abfangen, führt eine Ausnahme dazu, dass die Goroutine direkt in Panik gerät. Aber jedes Mal eine globale Wiederherstellungslogik zu schreiben, ist nicht sehr elegant, daher können wir eine gekapselte HandleCrash
-Methode verwenden:
package runtime var ( ReallyCrash = true ) // Standardmäßiger globaler Panikbehandler var PanicHandlers = []func(interface{}){logPanic} // Ermöglicht das Übergeben zusätzlicher benutzerdefinierter Panikbehandler von außerhalb func HandleCrash(additionalHandlers ...func(interface{})) { if r := recover(); r != nil { for _, fn := range PanicHandlers { fn(r) } for _, fn := range additionalHandlers { fn(r) } if ReallyCrash { panic(r) } } }
Dies unterstützt sowohl die interne Ausnahmebehandlung als auch die externe Injektion zusätzlicher Handler. Wenn Sie keinen Absturz wünschen, können Sie die Logik nach Bedarf ändern.
package runtime func Go(fn func()) { go func() { defer HandleCrash() fn() }() }
Beim Starten einer Goroutine können Sie die Go
-Methode verwenden, die auch verhindert, dass Sie vergessen, die Panikbehandlung hinzuzufügen.
Kapselung von WaitGroup
import "sync" type Group struct { wg sync.WaitGroup } func (g *Group) Wait() { g.wg.Wait() } func (g *Group) Start(f func()) { g.wg.Add(1) go func() { defer g.wg.Done() f() }() }
Der wichtigste Teil hier ist die Start
-Methode, die Add
und Done
intern kapselt. Obwohl es nur ein paar Codezeilen sind, stellt es sicher, dass wir beim Verwenden einer Waitgroup nicht vergessen, den Zähler zu erhöhen oder zu vervollständigen.
Kapselung der durch Semaphore ausgelösten Logik
type BoundedFrequencyRunner struct { sync.Mutex // Aktiv ausgelöst run chan struct{} // Timer-Limit timer *time.Timer // Die tatsächliche auszuführende Logik fn func() } func NewBoundedFrequencyRunner(fn func()) *BoundedFrequencyRunner { return &BoundedFrequencyRunner{ run: make(chan struct{}, 1), fn: fn, timer: time.NewTimer(0), } } // Run löst die Ausführung aus; hier kann nur ein Signal geschrieben werden, zusätzliche Signale werden verworfen, ohne zu blockieren. Sie können die Warteschlangengröße nach Bedarf erhöhen. func (b *BoundedFrequencyRunner) Run() { select { case b.run <- struct{}{}: fmt.Println("Signal written successfully") default: fmt.Println("Signal already triggered once, discarding") } } func (b *BoundedFrequencyRunner) Loop() { b.timer.Reset(time.Second * 1) for { select { case <-b.run: fmt.Println("Run signal triggered") b.tryRun() case <-b.timer.C: fmt.Println("Timer triggered execution") b.tryRun() } } } func (b *BoundedFrequencyRunner) tryRun() { b.Lock() defer b.Unlock() // Hier können Sie Logik wie Ratenbegrenzung hinzufügen b.timer.Reset(time.Second * 1) b.fn() }
Wir sind Leapcell, Ihre erste Wahl für das Hosten von Go-Projekten.
Leapcell ist die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis:
Multi-Sprachen-Unterstützung
- 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 Antwortzeit 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 Betriebsaufwand – konzentrieren Sie sich einfach auf das Bauen.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ