From dubbo-go
Guides writing custom dubbo-go v3 extensions — Filter, LoadBalance, Registry, Protocol — through the SPI pattern. Use when user asks how to write a filter, custom interceptor, custom load balancer, plug in a new registry, or hook into dubbo-go's extension points.
How this skill is triggered — by the user, by Claude, or both
Slash command
/dubbo-go:extensionsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
dubbo-go exposes nearly every runtime behavior through a uniform SPI pattern:
dubbo-go exposes nearly every runtime behavior through a uniform SPI pattern:
extension.SetXxx(name, factory) in an init()init() runsclient.WithFilter, client.WithLoadBalance, etc.)The extension point you pick depends on what you want to intercept:
| You want to... | Extension point | extension.Set* fn |
|---|---|---|
| Intercept every RPC call on server or client (auth, logging, metrics) | Filter | SetFilter |
| Choose which provider instance to call | LoadBalance | SetLoadbalance |
| Prune the provider list before load balancing (canary, A/B) | Router | pushed via config center (see note below) |
| Plug in a new service discovery backend | Registry | SetRegistry |
| Add a new wire protocol | Protocol | SetProtocol |
| Replace the logger backend | Logger | SetLogger |
Filters run on every RPC. They're the right extension point for auth, token injection, metrics, tracing, rate limiting, and request/response logging.
Package layout:
yourapp/
└── filter/
└── myfilter/
└── myfilter.go
myfilter.go:
package myfilter
import (
"context"
"time"
)
import (
"dubbo.apache.org/dubbo-go/v3/common/extension"
"dubbo.apache.org/dubbo-go/v3/filter"
"dubbo.apache.org/dubbo-go/v3/protocol/base"
"dubbo.apache.org/dubbo-go/v3/protocol/result"
"github.com/dubbogo/gost/log/logger"
)
func init() {
extension.SetFilter("timing", func() filter.Filter { return &timingFilter{} })
}
type timingFilter struct{}
func (f *timingFilter) Invoke(ctx context.Context, invoker base.Invoker, inv base.Invocation) result.Result {
start := time.Now()
res := invoker.Invoke(ctx, inv)
logger.Infof("call %s took %s", inv.MethodName(), time.Since(start))
return res
}
func (f *timingFilter) OnResponse(ctx context.Context, res result.Result, invoker base.Invoker, inv base.Invocation) result.Result {
return res
}
Activate in main.go:
import (
_ "github.com/yourorg/yourapp/filter/myfilter" // triggers init()
)
// Server side — apply per service
pb.RegisterGreetServiceHandler(srv, impl, server.WithFilter("timing"))
// Client side — apply per reference
svc, _ := pb.NewGreetService(cli, client.WithFilter("timing"))
Chain multiple filters by comma-separating: server.WithFilter("timing,auth"). Filters run in order on the way in, reverse order on OnResponse.
Modify attachments (e.g. add a request ID) in Invoke via inv.SetAttachment(key, val); read on the other side with inv.Attachment(key).
Short-circuit the call by returning a result without calling invoker.Invoke:
func (f *authFilter) Invoke(ctx context.Context, invoker base.Invoker, inv base.Invocation) result.Result {
if !isAuthorized(inv) {
return &result.RPCResult{Err: errors.New("unauthorized")}
}
return invoker.Invoke(ctx, inv)
}
See filter/custom for the canonical end-to-end example.
Choose one provider out of a list. Built-in: Random (default), RoundRobin, LeastActive, ConsistentHash, P2C.
Write a custom one only if the built-ins don't cover your selection rule (e.g. pick by request header, pick by tenant ID).
package affinityLB
import (
"dubbo.apache.org/dubbo-go/v3/cluster/loadbalance"
"dubbo.apache.org/dubbo-go/v3/common/extension"
"dubbo.apache.org/dubbo-go/v3/protocol/base"
)
func init() {
extension.SetLoadbalance("tenant-affinity", func() loadbalance.LoadBalance {
return &tenantAffinityLB{}
})
}
type tenantAffinityLB struct{}
func (lb *tenantAffinityLB) Select(invokers []base.Invoker, inv base.Invocation) base.Invoker {
tenant := inv.Attachment("tenant-id")
for _, iv := range invokers {
if iv.GetURL().GetParam("tenant", "") == tenant {
return iv
}
}
return invokers[0] // fallback
}
Activate per reference:
svc, _ := pb.NewGreetService(cli, client.WithLoadBalance("tenant-affinity"))
Or set as client default:
cli, _ := client.NewClient(client.WithClientLoadBalance("tenant-affinity"))
Only needed if you want to plug in something the built-in set (Nacos, ZooKeeper, etcd, Polaris, Kubernetes) doesn't cover.
import (
"dubbo.apache.org/dubbo-go/v3/common"
"dubbo.apache.org/dubbo-go/v3/common/extension"
"dubbo.apache.org/dubbo-go/v3/registry"
)
func init() {
extension.SetRegistry("myregistry", func(url *common.URL) (registry.Registry, error) {
return newMyRegistry(url)
})
}
Implement the full registry.Registry interface (see dubbo-go/registry/registry.go): Register, UnRegister, Subscribe, UnSubscribe, LoadSubscribeInstances. This is a non-trivial undertaking — expect to look at how registry/nacos or registry/zookeeper implement it.
Enable:
dubbo.WithRegistry(registry.WithRegistry("myregistry"), registry.WithAddress("..."))
Rarely needed. Implement base.Protocol: Export(invoker), Refer(url), Destroy(). Register with extension.SetProtocol(name, factory). Most users should not touch this — pick Triple, Dubbo, or gRPC from the built-ins.
Routers prune the provider list before the load balancer runs. dubbo-go ships tag, condition, and script routers out of the box.
Custom routers are uncommon. The built-in ones read rules from the config center (Nacos, ZK, Apollo) at runtime — so what users usually want is rule authoring, not a new router. See:
If you genuinely need a new routing algorithm, implement router.PriorityRouter and register via extension.SetRouterFactory.
| Symptom | Likely cause |
|---|---|
filter not found at startup | Blank-import missing, or string in WithFilter("x") doesn't match SetFilter("x", ...) |
| Filter registered but never called | init() in a file that's never imported — check blank-import path |
| LB not used | Built-in LB took precedence — ensure WithLoadBalance("name") matches your registered name |
| Filter runs but modified attachment not visible on the other side | Triple attachments go over HTTP/2 headers; reserved header names may be dropped |
The name string is the contract. Whatever you pass to extension.SetFilter("timing", ...) must be the exact same string passed to server.WithFilter("timing"). Typos here produce silent failures — the extension simply doesn't run.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub tsukikage7/dubbo-go-skills --plugin dubbo-go