Skip to content

[Proposal] Kratos v3: Clean up historical design issues and reduce external dependencies #3820

@tonybase

Description

@tonybase

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

  1. Implement the core SRE circuit breaker algorithm in internal/circuitbreaker (about 200 lines of code).
  2. Make middleware/circuitbreaker use the internal implementation by default, while keeping an option for users to inject a custom implementation.
  3. Move the default rate limiter implementation into internal packages as well, while keeping the interface replaceable.
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions