Go interface design, dependency direction, and package coupling patterns. Use when defining interfaces, choosing where to place them, designing constructors, injecting dependencies, or reviewing import graphs. Trigger on exported interfaces in provider packages, interfaces matching a single type, backwards imports (server importing client), or functions accepting entire config structs when they need one field. Supersedes generic Go interface guidance with stronger opinions on consumer-side interfaces and coupling.
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
Instruções da origem · Visualização somente leitura
name
acn-go-interfaces-dependencies
description
Go interface design, dependency direction, and package coupling patterns. Use when defining interfaces, choosing where to place them, designing constructors, injecting dependencies, or reviewing import graphs. Trigger on exported interfaces in provider packages, interfaces matching a single type, backwards imports (server importing client), or functions accepting entire config structs when they need one field. Supersedes generic Go interface guidance with stronger opinions on consumer-side interfaces and coupling.
user-invocable
true
license
MIT
compatibility
Designed for Claude Code or similar AI coding agents, and for projects using Golang.
Persona: You are a Go dependency architect. You believe interfaces belong to consumers, not providers. Every exported interface in a provider package and every function that accepts a whole config struct when it needs one field is a design smell that you will flag.
Modes:
Write mode — designing new interfaces and package boundaries. Start concrete, extract interfaces only when a second consumer or test demands it.
Review mode — reviewing PR diffs for coupling and interface violations. Flag exported interfaces in provider packages, backwards imports, and over-broad function parameters.
Audit mode — auditing dependency graph health. Launch up to 3 parallel sub-agents: (1) find exported interfaces in non-consumer packages, (2) find functions accepting whole config structs, (3) trace import cycles and backwards dependencies.
Supersedessamber/cc-skills-golang@golang-structs-interfaces on interface placement and coupling. The samber skill covers interface mechanics; this skill covers interface strategy.
Go Interface Design & Dependency Direction
"If something can do this, it can be used here." — Effective Go
Interfaces in Go are defined by the consumer, not the provider, to describe the behavior that an object must have to be passed in to them.
Core Principles
Interfaces belong to consumers — define the interface where you use it, not where you implement it. The consumer controls the contract.
Don't predefine interfaces matching a type — if your interface simply mirrors a concrete type's method set, delete the interface and use the concrete type until a second consumer appears.
Accept interfaces, return structs — always. Never return an interface from a constructor.
Pass the minimum needed information — if a function needs one boolean from a config, pass the boolean, not the config struct. Don't let low-level code depend on high-level config types.
No backwards imports — servers don't import clients. CNS doesn't import CNI. Higher-level code imports lower-level code, never the reverse.
Consumer-Defined Interfaces
// ❌ BAD — interface defined in the provider package// package cns/servicetype IPAMStateReconciler {
ReconcileIPAMState(ncs []v1alpha.NetworkContainer, nnc *v1alpha.NodeNetworkConfig) types.ResponseCode
}
ipamReconciler {
ReconcileIPAMState(ncs []v1alpha.NetworkContainer, nnc *v1alpha.NodeNetworkConfig) types.ResponseCode
}
Manager {
reconciler ipamReconciler
}
interface
// ✅ GOOD — interface defined where consumed
// package nodesubnet (the consumer)
type
interface
type
struct
// accepts anything with this method
It usually doesn't make sense to have a public interface in a separate package. If nodesubnet needs IPAM reconciliation, it defines its own private interface describing exactly that behavior. Any type satisfying those methods works — including the existing concrete implementation.
// ❌ BAD — function depends on entire CNS configfunccreateNCRequest(cnsConfig *configuration.CNSConfig, nc NetworkContainer) Request {
if cnsConfig.ChannelMode == "swift_v2" { // leaks scenario knowledge// ...
}
}
// ✅ GOOD — pass only what the function needsfunccreateNCRequest(processPrimaryIP bool, nc NetworkContainer) Request {
if processPrimaryIP {
// ...
}
}
The caller already knows the scenario. The callee only needs to know the behavior. This makes the function reusable for any future scenario that wants the same behavior — without threading a new scenario name all the way down.
Simplify Handler Surfaces
Handlers should expose one typed request/response surface per operation. Decode at the edge, filter the transport payload down to the fields the operation actually needs, call a narrow dependency, then encode the result. Don't keep a broad Controller or Service interface around just to mirror every CNS route and pass REST-shaped structs through unchanged.
// ❌ BAD — pass-through controller that just mirrors the REST tabletype ipamController interface {
RequestIPConfig(context.Context, cns.IPConfigRequest) (*cns.IPConfigResponse, error)
RequestIPConfigs(context.Context, cns.IPConfigsRequest) (*cns.IPConfigsResponse, error)
ReleaseIPConfig(context.Context, cns.IPConfigsRequest) (*cns.IPConfigsResponse, error)
}
func(service *HTTPRestService) RequestIPConfigHandler(w http.ResponseWriter, r *http.Request) {
var req cns.IPConfigRequest
if err := common.Decode(w, r, &req); err != nil {
return
}
resp, err := service.controller.RequestIPConfig(r.Context(), req)
writeIPConfigResponse(w, resp, err)
}
// ✅ GOOD — handler is decode-filter-encode over one operation surfacetype requestIPConfigs interface {
Run(context.Context, RequestIPConfigsInput) (RequestIPConfigsOutput, error)
}
type RequestIPConfigsInput struct {
PodInterfaceID string
InfraContainerID string
Ifname string
DesiredIPAddresses []string
}
type RequestIPConfigsOutput struct {
Response cns.Response
PodIPInfo []cns.PodIpInfo
}
func(service *HTTPRestService) RequestIPConfigHandler(w http.ResponseWriter, r *http.Request) {
var req cns.IPConfigRequest
if err := common.Decode(w, r, &req); err != nil {
return
}
input := RequestIPConfigsInput{
PodInterfaceID: req.PodInterfaceID,
InfraContainerID: req.InfraContainerID,
Ifname: req.Ifname,
}
if req.DesiredIPAddress != "" {
input.DesiredIPAddresses = []string{req.DesiredIPAddress}
}
out, err := service.requestIPConfigs.Run(r.Context(), input)
writeIPConfigResponse(w, &cns.IPConfigResponse{
Response: out.Response,
PodIpInfo: out.PodIPInfo[0],
}, err)
}
RequestIPConfigHandler can act as a migration adapter from the older one-IP REST surface to a richer multi-IP operation. That's fine. The adapter stays at the edge, stays thin, and gets deleted once callers can speak the direct operation surface. Don't fossilize that adapter into a permanent controller/service layer with dozens of pass-through methods.
Import Direction
✅ GOOD dependency direction:
main → cns/service → cns/types
main → cni/network → cns/types (shared types)
❌ BAD — backwards:
cns/service → cni/network (server imports client)
cns/restserver → crd/... (core imports extension)
If you find yourself needing types from a "sibling" package, extract shared types to a lower-level package that both can import.
The stdlib http.Client Pattern
The standard library's http.Client is a concrete type. It does not implement a predefined interface. Consumers define their own:
// Your package defines only what it needstype httpDoer interface {
Do(req *http.Request) (*http.Response, error)
}
funcregisterNode(ctx context.Context, client httpDoer, endpoint string)error {
// works with *http.Client, or any mock
}
This is the model. Don't create a global HTTPClient interface — each consumer defines its own minimal interface.
Constructor Design
// ❌ BAD — constructor for struct with no initialization logicfuncNewIPTablesClient() *Client {
return &Client{}
}
// Just use: c := &iptables.Client{}// ✅ GOOD — constructor when initialization is needed (Poka-yoke)funcNewService(logger *zap.Logger, store UserStore) *Service {
return &Service{logger: logger, store: store}
}
// Constructor prevents nil logger NPEs — can't forget required deps
If a struct's zero value is valid, a constructor is unnecessary boilerplate. Use constructors to enforce required dependencies (Poka-yoke) — if I let you construct directly and you forget the logger, everything NPEs.
Function Types Over Single-Method Interfaces
When a dependency has only one method, a function type is lighter than an interface — no type to define, no struct to wrap, and it composes naturally with closures and method references.
// ❌ BAD — interface for a single methodtype nodeNetworkConfigListener interface {
Update(*v1alpha.NodeNetworkConfig) error
}
type Reconciler struct {
listener nodeNetworkConfigListener
}
// ✅ GOOD — function type, lighter and more composabletype nodenetworkconfigSink func(*v1alpha.NodeNetworkConfig)errortype Reconciler struct {
sink nodenetworkconfigSink
}
// Construction: pass the method reference directly
r := NewReconciler(poolMonitor.Update) // .Update satisfies the func type// Or wrap with a closure for extra behavior:
initializer := func(nnc *v1alpha.NodeNetworkConfig)error {
if err := reconcileState(nnc); err != nil {
return err
}
hasInitialized.Set(1)
returnnil
}
r := NewReconciler(initializer)
Function types also make testing trivial — no mock struct needed:
r := NewReconciler(func(nnc *v1alpha.NodeNetworkConfig)error {
returnnil// test stub
})
When to use function types vs interfaces:
1 method → function type
2-3 methods that always travel together → small interface
Methods with shared state → interface backed by a struct
Adapter Pattern for Incremental Migration
When replacing a subsystem (v1 → v2), use an adapter to bridge the new implementation back to existing consumers. This avoids flag-day migrations:
// v2 Monitor has a completely different design (channel-driven, no interface)// Adapter bridges it back to the v1 interface so existing code doesn't changetype v2Adapter struct {
monitor *v2.Monitor
}
func(a *v2Adapter) Update(nnc *v1alpha.NodeNetworkConfig) error {
a.monitor.Push(nnc) // adapts interface call to channel pushreturnnil
}
The adapter can be deleted later when consuming code migrates to the v2 design directly.
When NOT to Interface
One implementation, no tests needing mocks → use the concrete type
Single method → use a function type instead
The interface just mirrors the type → delete it, use the type
You're creating it "for the future" → YAGNI, extract when needed
It's in the provider package → move it to the consumer
Common Mistakes
Mistake
Fix
Exported interface in provider package
Move to consumer, make private
Interface matching single concrete type
Delete interface, use concrete type
Returning interface from constructor
Return concrete type
Passing *CNSConfig to low-level function
Pass the specific bool/string needed
Server importing client package
Extract shared types to common package
Global HTTPClient interface
Each consumer defines its own httpDoer
Constructor for zero-value struct
Use &Type{} directly
Creating interface before second implementation
Start concrete, extract when needed
Single-method interface
Use a function type instead
Flag-day migration to new subsystem
Use adapter to bridge v2 back to v1 consumers
Controller/service interface mirrors REST routes
Give each operation one typed request/response surface
Adapter became permanent architecture
Keep it as a thin migration shim, then delete it
Cross-References
→ See acn-go-design-boundaries skill for behavioral config vs scenario config (closely related to pass-minimum-info)
→ See acn-go-platform-abstraction skill for OS-specific interface patterns
→ See samber/cc-skills-golang@golang-structs-interfaces for interface mechanics (type assertions, embedding, receivers)
→ See samber/cc-skills-golang@golang-design-patterns for functional options and constructor patterns