Proposal Description
Kratos v3 is intended to clean up historical design issues accumulated in v2, reduce coupling between the framework and specific external dependencies, and align with newer standards in the Go ecosystem. This proposal contains several v3 cleanup improvements, including a small number of breaking changes.
TODOs
V3 Integration Branch
1. Split encoding/json Into Separate json and protojson Packages
Problem
The current encoding/json package mixes standard JSON encoding/decoding and Protobuf JSON encoding/decoding in the same codec, both registered under the "json" name:
// encoding/json/json.go
func (codec) Marshal(v any) ([]byte, error) {
switch m := v.(type) {
case json.Marshaler: // Standard JSON
return m.MarshalJSON()
case proto.Message: // Protobuf JSON
return MarshalOptions.Marshal(m)
default: // Standard JSON
return json.Marshal(m)
}
}
This design causes several issues:
- Users cannot explicitly choose the encoding strategy they want: standard JSON or proto JSON.
- Protojson dependencies (
google.golang.org/protobuf/encoding/protojson) are always pulled in, even when a project does not use protobuf.
- The same
"json" name cannot register two separate codecs at the same time.
Implementation
Split the current encoding/json package into two independent packages:
encoding/json: uses only the standard-library encoding/json package and registers as "json".
encoding/protojson: uses only protojson and registers as "protojson".
Example
import (
_ "github.com/go-kratos/kratos/v3/encoding/json" // Register the "json" codec
_ "github.com/go-kratos/kratos/v3/encoding/protojson" // Register the "protojson" codec
)
Breaking Change
- Code that previously relied on
encoding/json to automatically handle proto.Message values must explicitly import encoding/protojson.
- Migration: add
_ "github.com/go-kratos/kratos/v3/encoding/protojson" to imports where protojson support is required.
2. Replace the Log Package With a Lightweight slog Facade and Compose OTEL Through Extensions
Problem
The old log.Logger interface, helper/value/std logger APIs, trace/service handlers, and related APIs do not align with the standard-library log/slog package. They also make the core log package responsible for too many concerns.
Implementation
- Use
*slog.Logger consistently in core.
- Provide
log.NewLogger(handler slog.Handler, opts ...Option) to compose handlers, filters, context attributes, and related capabilities.
- Use the standard-library
logger.With(...) API for static fields.
- Move OTEL support into
contrib/otel/log, exposing only a slog.Handler.
Example
import (
"log/slog"
"os"
"github.com/go-kratos/kratos/v3/log"
)
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
logger := log.NewLogger(
handler,
log.WithFilter(log.FilterKey("password")),
).With(
slog.String("service.id", id),
slog.String("service.name", name),
slog.String("service.version", version),
)
log.SetDefault(logger)
OpenTelemetry only provides a slog.Handler:
import (
"log/slog"
otel "github.com/go-kratos/kratos/contrib/otel/v3/log"
"github.com/go-kratos/kratos/v3/log"
)
handler := otel.NewHandler("name")
logger := log.NewLogger(
handler,
log.WithFilter(log.FilterKey("password")),
).With(
slog.String("service.id", id),
slog.String("service.name", name),
slog.String("service.version", version),
)
Breaking Change
- Code that depends on
log.Logger, log.Helper, log.Valuer, log.NewStdLogger, or trace/service helpers must migrate to *slog.Logger, log.NewLogger(handler, opts...), and logger.With(...).
kratos.Logger and logging middleware configuration now accept *slog.Logger.
contrib/log/* adapter packages are no longer kept; third-party logging libraries can provide slog.Handler directly.
3. Move middleware/auth/jwt to Contrib
Problem
middleware/auth/jwt directly depends on github.com/golang-jwt/jwt/v5, which means:
- Every project using the framework indirectly pulls in
golang-jwt/jwt, regardless of whether it uses JWT.
- Framework core is tightly coupled to a specific JWT library version.
- Authentication has many possible implementations, and JWT is only one of them, so it does not belong in core.
Implementation
- Move the entire
middleware/auth/jwt package to contrib/middleware/jwt.
- Remove the
golang-jwt/jwt dependency from the core middleware package.
- Preserve existing behavior and only change the import path.
Example
// v2
import "github.com/go-kratos/kratos/v2/middleware/auth/jwt"
// v3
import "github.com/go-kratos/kratos/contrib/middleware/jwt/v3"
Breaking Change
- The import path changes, and the core framework no longer pulls
golang-jwt/jwt into go.mod.
- Migration: update the import path and run
go mod tidy.
4. Remove the aegis Dependency and Move the Default Circuit Breaker Into Internal Packages
Problem
middleware/circuitbreaker depends on github.com/go-kratos/aegis by default. Since aegis is maintained as a separate repository:
- It adds version coordination cost and external dependency uncertainty.
- The SRE circuit breaker implementation in
aegis is small enough to live directly in an internal framework package.
- Users who need custom circuit breaker strategies do not need the
aegis interfaces by default.
Current dependency:
// go.mod
github.com/go-kratos/aegis v0.2.0 // Direct dependency
Referenced from:
middleware/circuitbreaker/circuitbreaker.go
middleware/ratelimit/ratelimit.go
transport/grpc/resolver/discovery/resolver.go
transport/http/resolver.go
contrib/polaris/ratelimit.go
contrib/polaris/limiter.go
Implementation
- Implement the core SRE circuit breaker algorithm in
internal/circuitbreaker (about 200 lines of code).
- Make
middleware/circuitbreaker use the internal implementation by default, while keeping an option for users to inject a custom implementation.
- Move the default rate limiter implementation into internal packages as well, while keeping the interface replaceable.
- Keep compatibility for
aegis interface usage in the short to medium term where practical, but stop depending on the external repository for the default implementation.
Example
// v3: default behavior remains the same and requires no extra configuration.
import "github.com/go-kratos/kratos/v3/middleware/circuitbreaker"
handler := circuitbreaker.Client()(next)
// Custom circuit breakers are still supported.
handler := circuitbreaker.Client(
circuitbreaker.WithCircuitBreaker(myCustomBreaker),
)(next)
Breaking Change
- Aegis-related types are no longer exported as the default implementation.
- If code injects a custom aegis breaker with
WithCircuitBreaker(myAegisBreaker), it must add aegis as an explicit dependency.
5. HTTP Router and Proto Gen HTTP Refactor Conclusion
Conclusion
v3 will not switch to the Go 1.22 standard-library HTTP router for now, and will continue to keep github.com/gorilla/mux. The generated routes currently rely on RESTful routing with {name:regex} and regular-expression matching semantics. Switching directly to the standard-library router would require an additional compatibility layer and migration work. The main benefit would be removing one third-party dependency, which is not enough to offset the compatibility cost in this round.
This round focuses on cleaning up historical coupling between proto gen http and the binding package:
- Remove the exported
transport/http/binding package and move query/form binding logic into transport/http internals.
- Add
http.BuildPath to construct request paths from a path template and request message, replacing generated code's direct dependency on binding.EncodeURL.
- Stop generated
_http.pb.go files from importing transport/http/binding.
- Make generated proto HTTP clients control response encoding through
Accept: application/protojson by default. Requests with a body also set Content-Type: application/protojson. No additional codec marker API is introduced.
- Continue supporting AIP-style path templates, where
* matches a single path segment and ** matches multiple segments.
Breaking Change
- The
transport/http/binding package is removed. Generated code does not require manual migration. Hand-written code that directly uses binding.EncodeURL should migrate to http.BuildPath. Code that directly uses binding.BindQuery / binding.BindForm should use the binding capabilities on transport/http.Context or the lower-level encoding/form codec instead.
6. HTTP Streaming, SSE, and WebSocket Proto Examples
PR #3829 adds HTTP streaming support to protoc-gen-go-http and transport/http. The generated mapping is:
- Server-streaming RPCs (
returns (stream T)) are exposed as Server-Sent Events (SSE).
- Client-streaming and bidirectional-streaming RPCs (
stream T requests) are exposed through WebSocket upgrade endpoints.
- Streaming clients use
application/protojson by default for message payloads, with Accept / Content-Type based codec negotiation.
Server-Streaming SSE
syntax = "proto3";
package routeguide.v1;
import "google/api/annotations.proto";
service RouteGuide {
// Generated HTTP client uses Accept: text/event-stream and receives
// each Feature as one SSE event.
rpc ListFeatures(Rectangle) returns (stream Feature) {
option (google.api.http) = {
get: "/v1/routeguide/features"
};
}
}
message Rectangle {
Point lo = 1;
Point hi = 2;
}
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
message Feature {
string name = 1;
Point location = 2;
}
Client-Streaming WebSocket
syntax = "proto3";
package routeguide.v1;
import "google/api/annotations.proto";
service RouteGuide {
// Generated HTTP transport opens a WebSocket stream at this path.
// The annotated method describes the HTTP rule, while the generated
// server upgrades WebSocket streaming calls through a GET endpoint.
rpc RecordRoute(stream Point) returns (RouteSummary) {
option (google.api.http) = {
post: "/v1/routeguide/record-route"
body: "*"
};
}
}
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
message RouteSummary {
int32 point_count = 1;
int32 feature_count = 2;
int32 distance = 3;
int32 elapsed_time = 4;
}
Bidirectional WebSocket
syntax = "proto3";
package routeguide.v1;
import "google/api/annotations.proto";
service RouteGuide {
// Both client and server exchange RouteNote messages on the same
// WebSocket stream.
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {
option (google.api.http) = {
post: "/v1/routeguide/route-chat"
body: "*"
};
}
}
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
message RouteNote {
Point location = 1;
string message = 2;
}
Streaming Notes
- SSE is response-streaming only and is suitable for server push over HTTP.
- WebSocket is used when the request side is streaming, including client-streaming and bidirectional-streaming RPCs.
- For WebSocket streaming examples, prefer fixed paths or query parameters over path variables unless the service explicitly handles path state outside individual stream messages.
Overall Migration Path
Developers migrating from v2 to v3 need the following changes:
| Change |
Migration Action |
Impact |
| Encoding split |
Add an encoding/protojson import |
Users of proto.Message |
slog log facade |
Replace old Logger/Helper/Valuer/trace helpers with *slog.Logger, log.NewLogger(handler, opts...), and logger.With(...) |
Users of the Kratos log package and logging middleware |
| JWT migration |
Update the import path |
Users of the JWT middleware |
| Aegis removal |
Add aegis explicitly when using a custom aegis circuit breaker |
A small number of advanced users |
| HTTP binding package removal |
Migrate binding.EncodeURL to http.BuildPath; use Context binding capabilities or encoding/form for direct query/form binding |
Users of transport/http/binding |
References
Proposal Description
Kratos v3 is intended to clean up historical design issues accumulated in v2, reduce coupling between the framework and specific external dependencies, and align with newer standards in the Go ecosystem. This proposal contains several v3 cleanup improvements, including a small number of breaking changes.
TODOs
encoding/jsonintoencoding/jsonandencoding/protojson(PR feat(encoding): split json and protojson codecs #3823)slogfacade and compose OTEL through contrib (PR refactor(log)!: use slog logger facade #3821)middleware/auth/jwtto contrib (PR refactor(middleware): move jwt to contrib #3824)aegisdependency and move the default circuit breaker/rate limiter implementations into internal packages (PR refactor: internalize aegis defaults #3826)apipackage (PR refactor(api)!: remove metadata api package #3825)github.com/gorilla/mux, remove thetransport/http/bindingpackage, switch generated code tohttp.BuildPath, and useapplication/protojsonby default throughAccept/Content-Type(PR refactor(http)!: remove binding package #3827)dario.cat/mergoand keepgithub.com/fsnotify/fsnotifybecause the file source still requires it (PR refactor(config): remove mergo dependency #3828)V3 Integration Branch
v3->main)v3branch, then be collected into the main line through break: update kratos modules to v3 #3822.1. Split
encoding/jsonInto SeparatejsonandprotojsonPackagesProblem
The current
encoding/jsonpackage mixes standard JSON encoding/decoding and Protobuf JSON encoding/decoding in the same codec, both registered under the"json"name:This design causes several issues:
google.golang.org/protobuf/encoding/protojson) are always pulled in, even when a project does not use protobuf."json"name cannot register two separate codecs at the same time.Implementation
Split the current
encoding/jsonpackage into two independent packages:encoding/json: uses only the standard-libraryencoding/jsonpackage and registers as"json".encoding/protojson: uses onlyprotojsonand registers as"protojson".Example
Breaking Change
encoding/jsonto automatically handleproto.Messagevalues must explicitly importencoding/protojson._ "github.com/go-kratos/kratos/v3/encoding/protojson"to imports where protojson support is required.2. Replace the Log Package With a Lightweight
slogFacade and Compose OTEL Through ExtensionsProblem
The old
log.Loggerinterface, helper/value/std logger APIs, trace/service handlers, and related APIs do not align with the standard-librarylog/slogpackage. They also make the core log package responsible for too many concerns.Implementation
*slog.Loggerconsistently in core.log.NewLogger(handler slog.Handler, opts ...Option)to compose handlers, filters, context attributes, and related capabilities.logger.With(...)API for static fields.contrib/otel/log, exposing only aslog.Handler.Example
OpenTelemetry only provides a
slog.Handler:Breaking Change
log.Logger,log.Helper,log.Valuer,log.NewStdLogger, or trace/service helpers must migrate to*slog.Logger,log.NewLogger(handler, opts...), andlogger.With(...).kratos.Loggerand logging middleware configuration now accept*slog.Logger.contrib/log/*adapter packages are no longer kept; third-party logging libraries can provideslog.Handlerdirectly.3. Move
middleware/auth/jwtto ContribProblem
middleware/auth/jwtdirectly depends ongithub.com/golang-jwt/jwt/v5, which means:golang-jwt/jwt, regardless of whether it uses JWT.Implementation
middleware/auth/jwtpackage tocontrib/middleware/jwt.golang-jwt/jwtdependency from the coremiddlewarepackage.Example
Breaking Change
golang-jwt/jwtintogo.mod.go mod tidy.4. Remove the
aegisDependency and Move the Default Circuit Breaker Into Internal PackagesProblem
middleware/circuitbreakerdepends ongithub.com/go-kratos/aegisby default. Sinceaegisis maintained as a separate repository:aegisis small enough to live directly in an internal framework package.aegisinterfaces by default.Current dependency:
Referenced from:
middleware/circuitbreaker/circuitbreaker.gomiddleware/ratelimit/ratelimit.gotransport/grpc/resolver/discovery/resolver.gotransport/http/resolver.gocontrib/polaris/ratelimit.gocontrib/polaris/limiter.goImplementation
internal/circuitbreaker(about 200 lines of code).middleware/circuitbreakeruse the internal implementation by default, while keeping an option for users to inject a custom implementation.aegisinterface usage in the short to medium term where practical, but stop depending on the external repository for the default implementation.Example
Breaking Change
WithCircuitBreaker(myAegisBreaker), it must addaegisas an explicit dependency.5. HTTP Router and Proto Gen HTTP Refactor Conclusion
Conclusion
v3 will not switch to the Go 1.22 standard-library HTTP router for now, and will continue to keep
github.com/gorilla/mux. The generated routes currently rely on RESTful routing with{name:regex}and regular-expression matching semantics. Switching directly to the standard-library router would require an additional compatibility layer and migration work. The main benefit would be removing one third-party dependency, which is not enough to offset the compatibility cost in this round.This round focuses on cleaning up historical coupling between proto gen http and the binding package:
transport/http/bindingpackage and move query/form binding logic intotransport/httpinternals.http.BuildPathto construct request paths from a path template and request message, replacing generated code's direct dependency onbinding.EncodeURL._http.pb.gofiles from importingtransport/http/binding.Accept: application/protojsonby default. Requests with a body also setContent-Type: application/protojson. No additional codec marker API is introduced.*matches a single path segment and**matches multiple segments.Breaking Change
transport/http/bindingpackage is removed. Generated code does not require manual migration. Hand-written code that directly usesbinding.EncodeURLshould migrate tohttp.BuildPath. Code that directly usesbinding.BindQuery/binding.BindFormshould use the binding capabilities ontransport/http.Contextor the lower-levelencoding/formcodec instead.6. HTTP Streaming, SSE, and WebSocket Proto Examples
PR #3829 adds HTTP streaming support to
protoc-gen-go-httpandtransport/http. The generated mapping is:returns (stream T)) are exposed as Server-Sent Events (SSE).stream Trequests) are exposed through WebSocket upgrade endpoints.application/protojsonby default for message payloads, withAccept/Content-Typebased codec negotiation.Server-Streaming SSE
Client-Streaming WebSocket
Bidirectional WebSocket
Streaming Notes
Overall Migration Path
Developers migrating from v2 to v3 need the following changes:
encoding/protojsonimportproto.Messagesloglog facade*slog.Logger,log.NewLogger(handler, opts...), andlogger.With(...)aegisexplicitly when using a custom aegis circuit breakerbinding.EncodeURLtohttp.BuildPath; useContextbinding capabilities orencoding/formfor direct query/form bindingtransport/http/bindingReferences