Table of contents

  1. Introduction
    1. Adding a new API to your service
  2. HTTP Content-Type
    1. JSON request, JSON response
    2. JSON request, Proto response
    3. Proto request, Proto response
    4. When to use proto content type for performance
  3. Returning HTTP status codes from gRPC APIs
    1. Overview
    2. gRPC status codes and HTTP status codes mapping
    3. Returning errors from RPC
    4. Returning additional error details
    5. Using ColdBrew errors package
  4. Customizing HTTP Error Responses
    1. Custom Error Handler
    2. Integrating with ColdBrew
    3. Using with runtime.NewServeMux
    4. Example Response
  5. Custom HTTP Routes
    1. Basic custom route
    2. Serving static files or a UI
    3. OAuth callback
    4. Path parameters
  6. In-Process Gateway with DoHTTPtoGRPC
    1. How to use
    2. When to use
    3. Payload size impact
      1. Codec: vtprotobuf vs standard proto
      2. Transport: TCP vs Unix socket vs in-process

Introduction

ColdBrew is gRPC first, which means that gRPC APIs are the primary APIs and HTTP/JSON APIs are generated from the gRPC APIs. This approach is different from other frameworks where HTTP/JSON APIs are independent from gRPC APIs.

ColdBrew uses grpc-gateway to generate HTTP/JSON APIs from gRPC APIs. It reads protobuf service definitions and generates a reverse-proxy server which translates a RESTful HTTP API into gRPC. This server is generated according to the google.api.http annotations in your service definitions.

To learn more about HTTP to gRPC API mapping please refer to gRPC Gateway mapping examples.

Adding a new API to your service

To add a new API endpoint, you need to add a new method to your service definition and annotate it with the google.api.http annotations. The following example shows how to add a new API endpoint to the [example service]:

syntax = "proto3";
package example.v1;

service MySvc {
  ....
  rpc Upper(UpperRequest) returns (UpperResponse) {
    option (google.api.http) = {
      post: "/api/v1/example/upper"
      body: "*"
    };
  }
  ...
}
message UpperRequest{
    string msg = 1;
}

message UpperResponse{
    string msg = 1;
}

The above example adds a new API endpoint to the service which converts the input string to upper case. The endpoint is available at /api/v1/example/upper on the HTTP port and example.v1.MySvc/Upper on the gRPC port.

Run make generate (for ColdBrew cookiecutter) or protoc/buf with grpc-gateway plugin for others to generate the gRPC and HTTP code.

In your service implement the gRPC server interface

// Upper returns the message in upper case
func (s *svc) Upper(_ context.Context, req *proto.UpperRequest) (*proto.UpperResponse, error) {
    return &proto.UpperResponse{
        Msg: strings.ToUpper(req.GetMsg()),
    }, nil
}

Run your server (make run for ColdBrew cookiecutter) and send a request to the HTTP endpoint:

$ curl -X POST -d '{"msg":"hello"}' -i http://localhost:9091/api/v1/example/upper
HTTP/1.1 200 OK
Content-Type: application/json
Grpc-Metadata-Content-Type: application/grpc
Vary: Accept-Encoding
Date: Sun, 23 Apr 2023 07:48:34 GMT
Content-Length: 15

{"msg":"HELLO"}%

or the gRPC endpoint:

$ grpcurl -plaintext -d '{"msg": "hello"}' localhost:9090 example.v1.MySvc/Upper
{
  "msg": "HELLO"
}

HTTP Content-Type

ColdBrew supports multiple content-types for requests and responses. The default content-type is application/json. The following content-types are supported by default:

  • application/json
  • application/proto
  • application/protobuf

Lets assume the following proto definition:

message EchoRequest{
  string msg = 1;
}
message EchoResponse{
  string msg = 1;
}

service MySvc {
  rpc Echo(EchoRequest) returns (EchoResponse) {
    option (google.api.http) = {
      post: "/api/v1/example/echo"
      body: "*"
    };
    option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
      summary: "Echo endpoint"
      description: "Provides an echo reply endpoint."
      tags: "echo"
    };
  }
}

and the following service implementation:

// Echo returns the message with the prefix added
func (s *svc) Echo(_ context.Context, req *proto.EchoRequest) (*proto.EchoResponse, error) {
	return &proto.EchoResponse{
		Msg: fmt.Sprintf("%s: %s", "echo", req.GetMsg()),
	}, nil
}

when Content-Type or Accept is not specified in the request header, the default content-type of application/json is used.

JSON request, JSON response

When we send a curl call to the endpoint, we get the following response:

 $ curl -X POST -d '{"msg":"hello"}' -i http://127.0.0.1:9091/api/v1/example/echo
HTTP/1.1 200 OK
Content-Type: application/json
Grpc-Metadata-Content-Type: application/grpc
Vary: Accept-Encoding
Date: Sun, 23 Apr 2023 13:42:37 GMT
Content-Length: 20

{"msg":"echo: hello"}%

JSON request, Proto response

We can send a proto request and get a proto response by specifying the Accept header:

curl -X POST -H 'Accept: application/proto' -d '{"msg":"hello"}' -i http://127.0.0.1:9091/api/v1/example/echo
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Grpc-Metadata-Content-Type: application/grpc
Vary: Accept-Encoding
Date: Sun, 23 Apr 2023 13:46:47 GMT
Content-Length: 12


echo: hello%

Proto request, Proto response

We can send a proto request and get a JSON response by specifying the Content-Type header:

$ echo 'msg: "proto message"' | protoc --encode=EchoRequest proto/app.proto | curl -sS -X POST --data-binary @- -H 'Content-Type: application/proto' -i http://127.0.0.1:9091/api/v1/example/echo
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Grpc-Metadata-Content-Type: application/grpc
Vary: Accept-Encoding
Date: Sun, 23 Apr 2023 14:07:38 GMT
Content-Length: 20


echo: proto message%

When to use proto content type for performance

JSON is the default and best choice for browser clients and debugging. But for service-to-service HTTP calls (or mobile clients that can handle binary), application/proto can significantly reduce latency:

Aspect JSON Proto binary
Serialization speed ~3-5x slower (reflection-based, text encoding) Native proto marshal (or vtprotobuf for up to ~4x faster marshal)
Payload size ~2-3x larger (field names, base64 for bytes, text numbers) Compact binary (varint, field tags only)
CPU cost Higher (string parsing, escaping, number formatting) Lower (direct binary read/write)
Human-readable Yes No (use grpcurl or Swagger UI for debugging)

To use proto format, set the Content-Type and Accept headers:

# Send proto, receive proto
curl -X POST \
  -H 'Content-Type: application/proto' \
  -H 'Accept: application/proto' \
  --data-binary @request.bin \
  http://localhost:9091/api/v1/example/echo

For Go service-to-service calls, use the gRPC client directly instead of HTTP — it already uses proto binary encoding. The HTTP proto content type is most useful for non-gRPC clients (mobile apps, polyglot services) that want binary performance without the gRPC protocol overhead.

ColdBrew supports application/proto and application/protobuf out of the box — no configuration needed. Both map to the same proto binary marshaller.

Returning HTTP status codes from gRPC APIs

Overview

gRPC provides a set of standard response messages that can be used to return errors from gRPC APIs. These messages are defined in the google/rpc/status.proto.

// The `Status` type defines a logical error model that is suitable for
// different programming environments, including REST APIs and RPC APIs. It is
// used by [gRPC](https://github.com/grpc). Each `Status` message contains
// three pieces of data: error code, error message, and error details.
//
// You can find out more about this error model and how to work with it in the
// [API Design Guide](https://cloud.google.com/apis/design/errors).
message Status {
  // The status code, which should be an enum value of
  // [google.rpc.Code][google.rpc.Code].
  int32 code = 1;

  // A developer-facing error message, which should be in English. Any
  // user-facing error message should be localized and sent in the
  // [google.rpc.Status.details][google.rpc.Status.details] field, or localized
  // by the client.
  string message = 2;

  // A list of messages that carry the error details.  There is a common set of
  // message types for APIs to use.
  repeated google.protobuf.Any details = 3;
}

gRPC status codes and HTTP status codes mapping

gRPC status codes can be easily translated to HTTP status codes. The following table shows the mapping between the canonical error codes and HTTP status codes:

gRPC status code HTTP status code
OK 200
INVALID_ARGUMENT 400
OUT_OF_RANGE 400
FAILED_PRECONDITION 400
PERMISSION_DENIED 403
NOT_FOUND 404
ABORTED 409
ALREADY_EXISTS 409
RESOURCE_EXHAUSTED 429
CANCELLED 499
UNKNOWN 500
UNIMPLEMENTED 501
DEADLINE_EXCEEDED 504

Full list of gRPC status codes can be found in the google/rpc/code.proto file.

Returning errors from RPC

When the service returns an error from the rpc its mapped to http status code 500 by default. To return a different http status code, the service can return a google.rpc.Status message with the appropriate error code. The following example shows how to return a google.rpc.Status message with the INVALID_ARGUMENT error code:

    message GetBookRequest {
      string name = 1;
    }

    message GetBookResponse {
      Book book = 1;
    }

    service BookService {
      rpc GetBook(GetBookRequest) returns (GetBookResponse) {
        option (google.api.http) = {
          get: "/v1/{name=books/*}"
        };
      }
    }

import (
  "google.golang.org/grpc/codes"
  "google.golang.org/grpc/status"
)

func (s *server) GetBook(ctx context.Context, req *pb.GetBookRequest) (*pb.Book, error) {
  if req.Name == "" {
    return nil, status.Errorf(codes.InvalidArgument, "Name argument is required")
  }
  ...
}

This will return a google.rpc.Status message with the INVALID_ARGUMENT error code in HTTP and gRPC:

$ grpcurl -plaintext -d '{"name": ""}' localhost:8080 BookService.GetBook
{
  "code": 3,
  "message": "Name argument is required"
}
$ curl -X GET -i localhost:8080/v1/books/
HTTP/1.1 400 Bad Request
Content-Type: application/json
Vary: Accept-Encoding
Date: Sun, 23 Apr 2023 06:23:43 GMT
Content-Length: 61

{"code":3,"message":"Name argument is required","details":[]}%

Returning additional error details

The google.rpc.Status message can also be used to return additional error details. The following example shows how to return a google.rpc.Status message with the INVALID_ARGUMENT error code and additional error details:

    message GetBookRequest {
      string name = 1;
    }

    message GetBookResponse {
      Book book = 1;
    }

    service BookService {
      rpc GetBook(GetBookRequest) returns (GetBookResponse) {
        option (google.api.http) = {
          get: "/v1/{name=books/*}"
        };
      }
    }
import (
  "google.golang.org/grpc/codes"
  "google.golang.org/grpc/status"
  "google.golang.org/genproto/googleapis/rpc/errdetails"
)

func (s *server) GetBook(ctx context.Context, req *pb.GetBookRequest) (*pb.Book, error) {
  if req.Name == "" {
    st := status.New(codes.InvalidArgument, "Name argument is required")
    st, _ = st.WithDetails(&errdetails.BadRequest_FieldViolation{
      Field:       "name",
      Description: "Name argument is required",
    })
    return nil, st.Err()
  }
  ...
}

This will output

$ grpcurl -plaintext -d '{"name": ""}' localhost:8080 BookService.GetBook
{
  "code": 3,
  "message": "Name argument is required",
  "details": [
    {
      "@type": "type.googleapis.com/google.rpc.BadRequest",
      "fieldViolations": [
        {
          "field": "name",
          "description": "Name argument is required"
        }
      ]
    }
  ]
}
$ curl -X GET localhost:8080/v1/books/
{
  "code": 3,
  "message": "Name argument is required",
  "details": [
    {
      "@type": "type.googleapis.com/google.rpc.BadRequest",
      "fieldViolations": [
        {
          "field": "name",
          "description": "Name argument is required"
        }
      ]
    }
  ]
}

Using ColdBrew errors package

All the above examples can be used with the ColdBrew errors package by using the functions NewWithStatus/WrapWithStatus

import (
  "github.com/go-coldbrew/errors"
  "google.golang.org/grpc/codes"
  "google.golang.org/grpc/status"
  "google.golang.org/genproto/googleapis/rpc/errdetails"
)

func (s *server) GetBook(ctx context.Context, req *pb.GetBookRequest) (*pb.Book, error) {
  if req.Name == "" {
    st := status.New(codes.InvalidArgument, "Name argument is required")
    st, _ = st.WithDetails(&errdetails.BadRequest_FieldViolation{
      Field:       "name",
      Description: "Name argument is required",
    })
    return nil, errors.NewWithStatus("Name argument is required", st)
  }
  ...
}

Using the errors.WrapWithStatus function has the same effect as errors.Wrap but it also sets the status code of the error to the status code of the google.rpc.Status message. Similarly, the errors.NewWithStatus function has the same effect as errors.New but it also sets the status code of the error to the status code of the google.rpc.Status message.

ColdBrew errors package also provides stack trace support for errors, which can make debugging easier. For more information see ColdBrew errors package.

Customizing HTTP Error Responses

By default, grpc-gateway returns errors in the following JSON format:

{
  "code": 3,
  "message": "Name argument is required",
  "details": []
}

You may want to customize this error response structure to match your API conventions, support legacy clients, or provide additional context. grpc-gateway provides the WithErrorHandler option to achieve this.

Custom Error Handler

To customize the error response format, create a custom error handler and pass it to the runtime.NewServeMux():

package main

import (
	"context"
	"encoding/json"
	"net/http"

	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// CustomError defines your desired error response structure
type CustomError struct {
	Error ErrorDetail `json:"error"`
}

type ErrorDetail struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

// CustomErrorHandler handles gRPC errors and writes custom JSON response
func CustomErrorHandler(
	ctx context.Context,
	mux *runtime.ServeMux,
	marshaler runtime.Marshaler,
	w http.ResponseWriter,
	r *http.Request,
	err error,
) {
	// Extract gRPC status from the error
	st, ok := status.FromError(err)
	if !ok {
		st = status.New(codes.Unknown, err.Error())
	}

	// Build custom error response
	customErr := CustomError{
		Error: ErrorDetail{
			Code:    st.Code().String(),
			Message: st.Message(),
		},
	}

	// Set content type and HTTP status code
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(runtime.HTTPStatusFromCode(st.Code()))

	// Write the custom JSON response
	json.NewEncoder(w).Encode(customErr)
}

Integrating with ColdBrew

In your InitHTTP function, apply the custom error handler to the existing mux before registering your service handlers:

func (s *cbSvc) InitHTTP(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) error {
	// Apply custom error handler to the existing mux
	runtime.WithErrorHandler(CustomErrorHandler)(mux)

	return proto.RegisterMyServiceHandlerFromEndpoint(ctx, mux, endpoint, opts)
}

This works because runtime.ServeMuxOption is defined as func(*ServeMux), allowing you to apply options to an existing mux by calling the option function directly.

Using with runtime.NewServeMux

If you’re managing your own gateway setup (without ColdBrew core), pass the option when creating the ServeMux:

mux := runtime.NewServeMux(
	runtime.WithErrorHandler(CustomErrorHandler),
)

Example Response

With the custom error handler above, when your gRPC service returns an InvalidArgument error:

return nil, status.Errorf(codes.InvalidArgument, "Name argument is required")

The HTTP response will be:

$ curl -X GET localhost:8080/v1/books/
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": {
    "code": "InvalidArgument",
    "message": "Name argument is required"
  }
}

Instead of the default format:

{"code":3,"message":"Name argument is required","details":[]}

For more advanced customization options, refer to the grpc-gateway customization guide.

Custom HTTP Routes

ColdBrew is gRPC-first, but sometimes you need HTTP endpoints that don’t map to a gRPC method — webhooks, file uploads, OAuth callbacks, static file serving, or custom REST endpoints.

The grpc-gateway runtime.ServeMux passed to InitHTTP supports custom routes via HandlePath. You can register any HTTP handler alongside your gateway routes:

Basic custom route

func (s *svc) InitHTTP(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) error {
    // Register gateway routes (proto-generated)
    if err := proto.RegisterMyServiceHandlerFromEndpoint(ctx, mux, endpoint, opts); err != nil {
        return err
    }

    // Custom HTTP routes
    if err := mux.HandlePath("POST", "/webhooks/stripe", func(w http.ResponseWriter, r *http.Request, _ map[string]string) {
        // Handle Stripe webhook — raw HTTP, no proto marshalling
        body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 1MB limit
        if err != nil {
            http.Error(w, "bad request", http.StatusBadRequest)
            return
        }
        if !verifyStripeSignature(r.Header.Get("Stripe-Signature"), body) {
            http.Error(w, "invalid signature", http.StatusForbidden)
            return
        }
        processWebhookEvent(body)
        w.WriteHeader(http.StatusOK)
    }); err != nil {
        return err
    }

    return nil
}

Serving static files or a UI

Use the {path=**} wildcard to catch all sub-paths:

func (s *svc) InitHTTP(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) error {
    if err := proto.RegisterMyServiceHandlerFromEndpoint(ctx, mux, endpoint, opts); err != nil {
        return err
    }

    // Serve a React/Vue frontend from embedded files
    uiHandler := http.FileServer(http.FS(uiFiles))
    if err := mux.HandlePath("GET", "/ui/{path=**}", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
        // Strip the /ui prefix
        r.URL.Path = "/" + pathParams["path"]
        uiHandler.ServeHTTP(w, r)
    }); err != nil {
        return err
    }

    return nil
}

OAuth callback

if err := mux.HandlePath("GET", "/auth/callback", func(w http.ResponseWriter, r *http.Request, _ map[string]string) {
    code := r.URL.Query().Get("code")
    token, err := exchangeCodeForToken(code)
    if err != nil {
        http.Error(w, "auth failed", http.StatusUnauthorized)
        return
    }
    setSessionCookie(w, token)
    http.Redirect(w, r, "/", http.StatusFound)
}); err != nil {
    return err
}

Path parameters

HandlePath supports path parameters using {name} syntax. Parameters are passed in the pathParams map:

if err := mux.HandlePath("GET", "/files/{id}", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
    fileID := pathParams["id"]
    data, err := s.storage.GetFile(r.Context(), fileID)
    if err != nil {
        http.Error(w, "not found", http.StatusNotFound)
        return
    }
    w.Header().Set("Content-Type", "application/octet-stream")
    w.Write(data)
}); err != nil {
    return err
}

Custom routes registered via HandlePath go through ColdBrew’s HTTP middleware stack (compression, tracing, New Relic) just like gateway routes. They benefit from the same observability without any extra configuration.

For routes that need to bypass the grpc-gateway marshalling entirely (e.g., streaming file uploads), HandlePath gives you raw http.ResponseWriter and *http.Request — no proto encoding/decoding involved.

In-Process Gateway with DoHTTPtoGRPC

By default, ColdBrew’s HTTP gateway connects to the gRPC server via a network hop (TCP or Unix socket). For maximum performance, you can use RegisterHandlerServer instead of RegisterHandlerFromEndpoint to handle HTTP requests in-process — eliminating all network overhead.

The challenge is that RegisterHandlerServer bypasses the gRPC interceptor chain (no logging, tracing, metrics, or panic recovery). ColdBrew’s interceptors.DoHTTPtoGRPC() solves this by wrapping each method call through the full interceptor chain.

How to use

  1. In InitHTTP, use RegisterHandlerServer and pass the gRPC server directly:
func (s *svc) InitHTTP(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) error {
    return proto.RegisterMyServiceHandlerServer(ctx, mux, s)
}
  1. Wrap each public gRPC method with DoHTTPtoGRPC:
func (s *svc) Echo(ctx context.Context, req *proto.EchoRequest) (*proto.EchoResponse, error) {
    handler := func(ctx context.Context, req interface{}) (interface{}, error) {
        return s.echo(ctx, req.(*proto.EchoRequest))
    }
    r, err := interceptors.DoHTTPtoGRPC(ctx, s, handler, req)
    if err != nil {
        return nil, err
    }
    return r.(*proto.EchoResponse), nil
}

func (s *svc) echo(ctx context.Context, req *proto.EchoRequest) (*proto.EchoResponse, error) {
    // ... actual implementation ...
}

The public method (Echo) is the wrapper that grpc-gateway and gRPC clients both call. The private method (echo) contains the actual business logic. DoHTTPtoGRPC detects whether the call came via the HTTP gateway (using runtime.RPCMethod) and applies the interceptor chain accordingly.

The interceptor chain is cached on first invocation. All interceptor configuration (AddUnaryServerInterceptor, SetFilterFunc, etc.) must be finalized before the first call to DoHTTPtoGRPC.

When to use

Approach Latency Setup
RegisterHandlerFromEndpoint (default) ~67µs (TCP) or ~36µs (Unix socket) Zero code changes
RegisterHandlerServer + DoHTTPtoGRPC ~19µs (in-process) Per-method wrapper

Use DoHTTPtoGRPC when HTTP gateway latency is critical and you’re willing to add per-method wrappers. For most services, enabling Unix sockets (DISABLE_UNIX_GATEWAY=false) provides a good balance of performance and simplicity.

Payload size impact

How does payload size affect gateway performance? We benchmarked with realistic proto messages containing nested structs with strings, numbers, maps, repeated fields, and timestamps — at three sizes: 1 item (~200B), 50 items (~10KB), and 500 items (~100KB).

Codec: vtprotobuf vs standard proto

Pure serialization cost, no network (Apple M1 Pro):

Payload Proto Marshal VTProto Marshal Speedup
1 item (~200B) 1.1µs / 17 allocs 0.28µs / 1 alloc 4x faster
50 items (~10KB) 53µs / 801 allocs 12µs / 1 alloc 4.3x faster
500 items (~100KB) 529µs / 8,001 allocs 122µs / 1 alloc 4.3x faster
Payload Proto Unmarshal VTProto Unmarshal Speedup
1 item (~200B) 1.4µs / 40 allocs 0.58µs / 23 allocs 2.5x faster
50 items (~10KB) 69µs / 1,908 allocs 29µs / 1,107 allocs 2.4x faster
500 items (~100KB) 684µs / 19,011 allocs 290µs / 11,010 allocs 2.4x faster

vtprotobuf marshal produces a single allocation regardless of payload size, and is up to ~4x faster. Unmarshal is ~2.4x faster with 40-70% fewer allocations.

Transport: TCP vs Unix socket vs in-process

End-to-end gRPC unary call latency with default proto codec (Apple M1 Pro):

Payload TCP Unix socket Bufconn (theoretical)*
1 item (~200B) 85µs 48µs 30µs
50 items (~10KB) 393µs 356µs 295µs
500 items (~100KB) 2,834µs 3,061µs 2,628µs

*Bufconn is a test utility (google.golang.org/grpc/test/bufconn) — no keepalive, backpressure, or connection lifecycle. These numbers represent a theoretical in-process lower bound, not a production transport. For production in-process calls, use DoHTTPtoGRPC which skips serialization entirely and is even faster.

Key takeaways:

  • Transport matters most for small payloads. At 1 item, Unix socket (48µs) vs TCP (85µs) is a 1.8x improvement — the fixed transport overhead dominates when serialization is cheap.
  • Serialization dominates at scale. At 500 items, TCP and Unix socket converge because proto marshal/unmarshal (1.2ms total) dwarfs the ~40µs transport difference. This is where vtprotobuf and in-process calls (DoHTTPtoGRPC) have the most impact.
  • vtprotobuf is ColdBrew’s default and provides the biggest single improvement. If you’re not already using generated vtproto files, see the vtprotobuf howto.
  • Both optimizations compound. For maximum throughput: use vtprotobuf (default) + Unix socket for easy wins, or vtprotobuf + DoHTTPtoGRPC for latency-critical paths.

These benchmarks measure the gRPC layer only — the HTTP JSON↔proto conversion at the boundary is identical for all approaches. For large responses, consider using application/proto content type to avoid JSON marshalling overhead entirely.

Benchmark source: benchmarks/ — run with cd benchmarks && go test -bench=. -benchmem ./...