AlexStocks opened a new issue, #3249:
URL: https://github.com/apache/dubbo-go/issues/3249
## Background
dubbo-go has grown organically over 10 years to 826 .go files and ~139K LOC
across 22+ top-level directories. Several structural issues have accumulated
that impact developer productivity, build times, and code quality. This issue
proposes a phased reorganization plan.
Related: #3246 (memory leaks rooted in URL god object), #3247 (data races
rooted in extension global maps), #3248 (general improvements).
---
## Current State Analysis
### Code Distribution
| Directory | Files | LOC | % of Total | Issue |
|-----------|-------|-----|------------|-------|
| `protocol/` | 198 | 46,221 | 33% | God package: 9 protocol
implementations, 10-level nesting |
| `cluster/` | 118 | 13,395 | 10% | Acceptable, good separation |
| `filter/` | 96 | 11,535 | 8% | 3rd-party integrations
(hystrix/sentinel/seata) bundled |
| `registry/` | 67 | 11,523 | 8% | Dual architecture (Registry +
ServiceDiscovery) unclear |
| `config/` | 68 | 8,641 | 6% | 65 files flat in one directory, no
sub-grouping |
| `remoting/` | 46 | 6,721 | 5% | Should be internal to registry, not
top-level |
| `common/extension/` | 22 | ~2,000 | 1% | 22 files with identical pattern,
root cause of #3247 |
| `global/` | 25 | 4,254 | 3% | Overlaps with `config/`, unclear boundary |
| Root `.go` | 8 | 2,727 | 2% | `compat.go` alone is 1,129 LOC |
| `tools/` | 80 | 9,456 | 7% | CLI + codegen + formatter mixed |
### Key Structural Problems
**Problem 1: `protocol/` is a 46K-LOC god package**
9 protocol implementations (dubbo, dubbo3, grpc, triple, rest, jsonrpc,
mock, invocation, result) plus protocolwrapper, all under one directory. The
triple sub-tree nests 10 levels deep, mixing generated and handwritten code:
```
protocol/triple/triple_protocol/internal/gen/proto/connect/collide/v1/*.pb.go
```
Any change to the shared `protocol/base/` interface forces rebuilding all 9
implementations. Code review scope is unpredictable.
**Problem 2: `config/` has 65 files flat in one directory**
```
config/
├── application_config.go
├── consumer_config.go
├── provider_config.go
├── service_config.go (602 LOC)
├── reference_config.go (544 LOC)
├── root_config.go (401 LOC)
├── config_loader.go
├── ... 58 more files ...
```
No sub-grouping by concern. New contributors can't find anything without
grep.
**Problem 3: `common/extension/` is 22 copy-pasted files**
Every file follows the exact same pattern — a global `map[string]func()`,
plus `Set*`/`Get*`/`Unregister*` functions with zero lock protection. This is
the root cause of 20+ data races (#3247). The pattern was duplicated 22 times
instead of being abstracted.
**Problem 4: `config/` vs `global/` boundary is unclear**
`global/` (25 files, 4,254 LOC) holds configuration struct definitions.
`config/` (68 files, 8,641 LOC) holds loading/initialization logic. But some
config structs live in `config/`, some in `global/`. Callers don't know which
package to import.
**Problem 5: `remoting/` is misplaced**
`remoting/` contains network clients for zookeeper, nacos, etcdv3, polaris,
getty. But these are implementation details of `registry/` and
`config_center/`, not a separate concern. Having `remoting/` at the top level
makes the dependency direction confusing: `registry/nacos/` imports
`remoting/nacos/`, which is essentially one package split across two
directories.
**Problem 6: Root `compat.go` is 1,129 LOC**
A backwards-compatibility shim layer sitting in the project root alongside
the core entry point `dubbo.go`. This mixes concerns and inflates the root
package.
**Problem 7: 67 direct dependencies**
Every protocol, registry backend, filter integration, and tool is bundled in
one `go.mod`. Users who only need triple+nacos still pull in zookeeper, etcd,
polaris, hystrix, sentinel, seata, and 60+ other dependencies.
---
## Reorganization Plan
### Phase 1: Zero-risk cleanups (no API changes, no import path changes)
These changes only move internal code and add new abstractions. No external
API breaks.
#### Step 1.1: Introduce `extension.Registry[T]` to replace 22 copy-pasted
files
**Before** (22 files, each ~80 LOC):
```
common/extension/
├── filter.go # var filters = make(map[string]...)
├── protocol.go # var protocols = make(map[string]...)
├── registry.go # var registries = make(map[string]...)
├── cluster.go # var clusters = make(map[string]...)
├── loadbalance.go # ...same pattern...
├── configurator.go
├── config_center.go
├── config_center_factory.go
├── config.go
├── metadata_report_factory.go
├── proxy_factory.go
├── tps_limit.go
├── auth.go
├── rest_client.go
├── rest_server.go
├── config_reader.go
├── config_post_processor.go
├── router_factory.go
├── registry_directory.go
├── service_discovery.go
├── service_instance_selector_factory.go
├── logger.go
└── otel_trace.go
```
**After** (3 files):
```
common/extension/
├── registry.go # Registry[T] generic container (NEW)
├── extensions.go # All extension variable declarations using
Registry[T]
└── graceful_shutdown.go # Shutdown callbacks (different pattern, keep
separate)
```
**Implementation**:
```go
// common/extension/registry.go
package extension
import "sync"
// Registry is a thread-safe, generic extension registry.
// It replaces the 22 hand-rolled global maps that previously had no lock
protection.
type Registry[T any] struct {
mu sync.RWMutex
items map[string]T
name string // for error messages
}
func NewRegistry[T any](name string) *Registry[T] {
return &Registry[T]{
items: make(map[string]T),
name: name,
}
}
func (r *Registry[T]) Register(name string, v T) {
r.mu.Lock()
defer r.mu.Unlock()
r.items[name] = v
}
func (r *Registry[T]) Get(name string) (T, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
v, ok := r.items[name]
return v, ok
}
func (r *Registry[T]) MustGet(name string) T {
r.mu.RLock()
defer r.mu.RUnlock()
v, ok := r.items[name]
if !ok {
panic(r.name + " for " + name + " is not registered")
}
return v
}
func (r *Registry[T]) Unregister(name string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.items, name)
}
// Snapshot returns a copy of all registered items. Safe to iterate.
func (r *Registry[T]) Snapshot() map[string]T {
r.mu.RLock()
defer r.mu.RUnlock()
m := make(map[string]T, len(r.items))
for k, v := range r.items {
m[k] = v
}
return m
}
// Names returns a copy of all registered names.
func (r *Registry[T]) Names() []string {
r.mu.RLock()
defer r.mu.RUnlock()
names := make([]string, 0, len(r.items))
for k := range r.items {
names = append(names, k)
}
return names
}
```
```go
// common/extension/extensions.go
package extension
// All extension registries, replacing 22 individual global maps.
var (
Filters = NewRegistry[func() filter.Filter]("filter")
Protocols = NewRegistry[func() base.Protocol]("protocol")
Registries = NewRegistry[func(*common.URL) (registry.Registry,
error)]("registry")
Clusters = NewRegistry[func() cluster.Cluster]("cluster")
LoadBalances = NewRegistry[func()
loadbalance.LoadBalance]("loadbalance")
// ... all others ...
)
// Backwards-compatible wrappers (deprecate over time):
func SetFilter(name string, v func() filter.Filter) {
Filters.Register(name, v) }
func GetFilter(name string) (filter.Filter, bool) { f, ok :=
Filters.Get(name); if !ok { return nil, false }; return f(), true }
// ... etc for all existing Set*/Get* functions ...
```
This single change fixes all 20+ data races in `common/extension/` (#3247
Category 1) while maintaining full backwards compatibility through wrapper
functions.
#### Step 1.2: Move `compat.go` to `compat/` sub-package
```bash
mkdir -p compat/
mv compat.go compat/compat.go
# Update package declaration and add re-export aliases in root if needed
```
Root directory after this change:
```
dubbo.go (350 LOC) — Instance API
loader.go (515 LOC) — Config loading
options.go (542 LOC) — InstanceOptions
compat/compat.go (1,129 LOC) — Backwards compatibility (moved)
```
#### Step 1.3: Centralize generated code under `gen/`
**Before**:
```
protocol/triple/triple_protocol/internal/gen/proto/connect/collide/v1/*.pb.go
(10 levels)
protocol/triple/triple_protocol/internal/gen/proto/connect/import/v1/*.pb.go
protocol/triple/triple_protocol/internal/gen/proto/connect/ping/v1/*.pb.go
```
**After**:
```
protocol/triple/
├── gen/ # All generated code (max 3 levels)
│ ├── collide_v1/
│ ├── import_v1/
│ └── ping_v1/
├── triple_protocol/ # Handwritten code only
│ ├── client.go
│ ├── server.go
│ └── ...
├── client.go
└── server.go
```
Add `gen/` to `.gitattributes` so GitHub collapses generated files in PRs:
```
protocol/triple/gen/** linguist-generated=true
```
---
### Phase 2: Config reorganization (internal refactor, deprecate old paths)
#### Step 2.1: Merge `global/` into `config/model/`
`global/` is purely data structures (no logic). It should be a sub-package
of `config/`:
**Before**:
```
global/
├── application_config.go # struct ApplicationConfig
├── consumer_config.go # struct ConsumerConfig
├── provider_config.go # struct ProviderConfig
├── registry_config.go # struct RegistryConfig
├── service_config.go # struct ServiceConfig
├── shutdown_config.go # struct ShutdownConfig
├── tls_config.go # struct TLSConfig
├── tracing_config.go # struct TracingConfig
├── method_config.go # struct MethodConfig
└── ... 16 more ...
```
**After**:
```
config/
├── model/ # Pure struct definitions (was global/)
│ ├── application.go
│ ├── consumer.go
│ ├── provider.go
│ ├── registry.go
│ └── ...
├── loader/ # Config loading & initialization
│ ├── loader.go # was config_loader.go
│ └── root.go # was root_config.go
└── ...
```
Migration path: keep `global/` as a thin re-export wrapper for one major
version:
```go
// global/application_config.go (deprecated)
package global
import "dubbo.apache.org/dubbo-go/v3/config/model"
type ApplicationConfig = model.ApplicationConfig // type alias, zero cost
```
#### Step 2.2: Group `config/` files by concern
**Before** (65 files flat):
```
config/
├── application_config.go
├── consumer_config.go
├── provider_config.go
├── service_config.go (602 LOC)
├── reference_config.go (544 LOC)
├── root_config.go (401 LOC)
├── config_loader.go
├── graceful_shutdown.go
├── metadata_report_config.go
├── ... 56 more files flat ...
```
**After** (grouped by domain):
```
config/
├── model/ # Pure structs (from global/)
├── loader/ # Root config + loading + env
│ ├── loader.go
│ ├── root.go
│ └── env.go
├── service/ # Provider-side config
│ ├── service_config.go
│ ├── method_config.go
│ └── export.go
├── reference/ # Consumer-side config
│ ├── reference_config.go
│ └── method_config.go
├── registry/ # Registry config
│ └── registry_config.go
├── metadata/ # Metadata config
│ └── metadata_config.go
├── validation/ # Config validation (extracted from Init()
methods)
│ └── validator.go
└── service.go # GetProviderService/GetConsumerService (keep at
root)
```
---
### Phase 3: Merge `remoting/` into its consumers
`remoting/` is not a standalone concern — it's network client code that only
exists to serve `registry/` and `config_center/`.
**Before**:
```
remoting/
├── getty/ → used by protocol/dubbo (keep as-is, it IS a standalone
network layer)
├── nacos/ → used ONLY by registry/nacos/ and config_center/nacos/
├── zookeeper/ → used ONLY by registry/zookeeper/ and
config_center/zookeeper/
├── etcdv3/ → used ONLY by registry/etcdv3/ and config_center/etcdv3/
└── polaris/ → used ONLY by registry/polaris/
```
**After**:
```
remoting/
└── getty/ # Keep: genuine standalone network layer
registry/
├── nacos/
│ ├── registry.go
│ └── client.go # Was remoting/nacos/ — internal network client
├── zookeeper/
│ ├── registry.go
│ └── client.go # Was remoting/zookeeper/
├── etcdv3/
│ ├── registry.go
│ └── client.go # Was remoting/etcdv3/
└── polaris/
├── registry.go
└── client.go # Was remoting/polaris/
config_center/
├── nacos/
│ └── client.go # Shared with registry/nacos/ via internal package
├── zookeeper/
│ └── client.go
└── etcdv3/
└── client.go
```
If `registry/nacos/` and `config_center/nacos/` share the same client code,
extract to a shared internal package:
```
internal/nacos/client.go # shared nacos client
registry/nacos/ # imports internal/nacos
config_center/nacos/ # imports internal/nacos
```
---
### Phase 4: Protocol modularization (major version, long-term)
Split each protocol into an independent Go module so users only pull in what
they need.
**Before** (single go.mod, 67 direct dependencies):
```
go.mod ← everything bundled
```
**After** (multi-module):
```
go.mod # Core framework (common, cluster,
filter, registry interfaces)
protocol/triple/go.mod # Triple protocol module
protocol/grpc/go.mod # gRPC protocol module
protocol/dubbo/go.mod # Dubbo protocol module (legacy)
protocol/rest/go.mod # REST protocol module
protocol/jsonrpc/go.mod # JSON-RPC protocol module
```
User imports only what they need:
```go
import (
"dubbo.apache.org/dubbo-go/v3"
_ "dubbo.apache.org/dubbo-go/v3/protocol/triple" // only triple
_ "dubbo.apache.org/dubbo-go/v3/registry/nacos" // only nacos
)
```
Benefits:
- Users who only use triple+nacos don't pull in zookeeper, etcd, polaris,
grpc dependencies
- Each protocol can have its own release cycle
- Build times decrease significantly
- `go.sum` shrinks dramatically
#### Step 4.2: Extract 3rd-party filter integrations
These filters depend on external SDKs and should not be in the core
repository:
```
# Move to separate repos:
filter/hystrix/ → github.com/apache/dubbo-go-filter-hystrix
filter/sentinel/ → github.com/apache/dubbo-go-filter-sentinel
filter/seata/ → github.com/apache/dubbo-go-filter-seata
# Keep in core (no external SDK dependency):
filter/active/
filter/accesslog/
filter/generic/
filter/token/
filter/echo/
filter/tps/
filter/exec_limit/
filter/graceful_shutdown/
filter/metrics/
filter/otel/
```
---
### Phase 5: Fix `dubbo.go` root package lock bug + cleanup
#### Step 5.1: Fix wrong lock (one-line, do immediately)
```go
// dubbo.go:300-306
func SetProviderService(svc common.RPCService) {
proLock.Lock() // FIX: was conLock.Lock()
defer proLock.Unlock() // FIX: was conLock.Unlock()
providerServices[common.GetReference(svc)] = &server.ServiceDefinition{
Handler: svc,
}
}
```
#### Step 5.2: Add missing lock to GetConsumerConnection
```go
// dubbo.go:308-311
func GetConsumerConnection(interfaceName string) (*client.Connection, error)
{
conLock.RLock()
defer conLock.RUnlock()
return consumerServices[interfaceName].GetConnection()
}
```
---
## Summary & Ordering
| Phase | Effort | Risk | Impact | Prerequisite |
|-------|--------|------|--------|-------------|
| 1.1 `extension.Registry[T]` | 1 week | Zero (backwards compatible
wrappers) | Fixes 20+ races | None |
| 1.2 Move `compat.go` | 1 hour | Zero | Root package clarity | None |
| 1.3 Centralize generated code | 1 day | Zero | Readability | None |
| 5.1 Fix wrong lock | 1 minute | Zero | Fixes confirmed crash | None |
| 5.2 Add missing lock | 1 minute | Zero | Fixes race | None |
| 2.1 Merge `global/` → `config/model/` | 1 week | Low (type aliases for
compat) | Clarity | None |
| 2.2 Group `config/` files | 2 weeks | Medium (internal import paths
change) | Navigability | 2.1 |
| 3 Merge `remoting/` | 2 weeks | Medium (internal import paths change) |
Clean boundaries | None |
| 4.1 Protocol multi-module | 1 month | High (module split) | Dependency
reduction | 1.1, 2.1 |
| 4.2 Extract 3rd-party filters | 2 weeks | Medium (new repos) | Dependency
reduction | 4.1 |
Phases 1 and 5 can start immediately with zero risk. Phases 2-3 can proceed
in parallel. Phase 4 requires a major version bump.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]