| name | Writing dubbo-go extensions |
| description | 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. |
Writing dubbo-go Extensions
dubbo-go exposes nearly every runtime behavior through a uniform SPI pattern:
- Implement an interface
- Call
extension.SetXxx(name, factory) in an init()
- Blank-import the package so
init() runs
- Enable the extension by name at the call-site (
client.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 |
Filter (the 90% case)
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"
)
pb.RegisterGreetServiceHandler(srv, impl, server.WithFilter("timing"))
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.
LoadBalance
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]
}
Activate per reference:
svc, _ := pb.NewGreetService(cli, client.WithLoadBalance("tenant-affinity"))
Or set as client default:
cli, _ := client.NewClient(client.WithClientLoadBalance("tenant-affinity"))
Registry (new service-discovery backend)
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("..."))
Protocol (new wire protocol)
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.
Router (traffic-shaping)
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.
Troubleshooting
| 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 |
Key rule of thumb
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.