The transport leak problem
You start a service to manage user discounts. It begins as a single HTTP handler. You parse the JSON body, check the database, apply a percentage, and return a 200 OK. It works fine for a week. Then product asks for a gRPC endpoint so the checkout service can call it directly. Then operations wants a CLI tool to run batch updates. You copy the discount logic into three different files. Each copy handles its own input parsing, status codes, and error formatting. Testing becomes painful because you have to spin up a server or mock a gRPC client just to verify the math. The business logic has leaked into the transport layer.
Go Kit was built to stop that leak. It forces a strict boundary between what your service does and how clients talk to it. You define a pure Go interface for the business logic. You wrap that interface in transport-agnostic endpoints. You stack middleware on top to handle logging, tracing, and timeouts. The core logic never sees HTTP headers or gRPC metadata. It only sees a context and a request struct.
How Go Kit draws the line
Think of a hotel front desk. Guests arrive through different doors: the main entrance, the service elevator, the staff back door. The concierge at the front desk doesn't care which door they used. They only care about the reservation number and the guest name. The concierge checks availability, assigns a room, and hands over a key. The doors are transports. The reservation system is the business logic. The concierge is the endpoint layer that translates door-specific requests into system calls.
Go Kit applies this exact pattern. Your service interface defines the recipes. Endpoints translate incoming requests into those recipes. Transports listen on ports and decode bytes into endpoint calls. Middleware sits between the transport and the endpoint, stamping requests with timestamps, routing traces, and enforcing deadlines. The architecture is explicit by design. You cannot accidentally mix a JSON parser with a discount calculator.
The three layers in practice
Start with the service interface. Keep it in a package that imports nothing related to HTTP, gRPC, or databases. Define the methods your service needs. Implement them behind a struct. The receiver name should be short and match the type, like (g greeter) or (s service). Go convention favors one or two letters. It keeps signatures readable and matches the standard library style.
// service.go
// Greeter defines the business logic contract.
type Greeter interface {
// Greet returns a formatted greeting for the given name.
Greet(ctx context.Context, name string) (string, error)
}
// greeter implements Greeter with pure logic.
type greeter struct{}
// Greet formats the string and returns it.
func (g greeter) Greet(ctx context.Context, name string) (string, error) {
// context.Context is always the first parameter by convention.
// It carries deadlines, cancellation signals, and request-scoped values.
if name == "" {
return "", fmt.Errorf("name is required")
}
return fmt.Sprintf("Hello, %s", name), nil
}
The endpoint layer sits between the transport and the service. Every endpoint in Go Kit shares the same signature: it takes a context and an interface{} request, and returns an interface{} response and an error. This uniform signature lets the middleware stack treat every call identically, regardless of whether it came from HTTP, gRPC, or Thrift. The endpoint function performs a type assertion to convert the interface{} into a concrete request struct, calls the service method, and wraps the result in a response struct.
// endpoint.go
// GreetRequest holds the decoded payload for the greet operation.
type GreetRequest struct {
Name string
}
// GreetResponse holds the encoded result for the greet operation.
type GreetResponse struct {
Message string
}
// MakeGreetEndpoint builds an endpoint that delegates to the Greeter service.
func MakeGreetEndpoint(svc Greeter) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
// Type assert the generic interface{} into the concrete request struct.
req := request.(GreetRequest)
// Call the pure service method with the extracted fields.
resp, err := svc.Greet(ctx, req.Name)
if err != nil {
// Return the error directly. Go Kit's transport layer will map it.
return nil, err
}
// Wrap the result in the response struct for the transport encoder.
return GreetResponse{Message: resp}, nil
}
}
Accept interfaces, return structs. That is the standard Go style mantra, and Go Kit follows it strictly. Your service interface is accepted by the endpoint constructor. The endpoint returns a concrete struct. This keeps dependencies flowing inward and makes unit testing trivial. You can pass a mock implementation to MakeGreetEndpoint without touching the transport layer.
Walking through a request
When a client sends a POST request to /greet, the HTTP transport decodes the JSON body into a GreetRequest struct. It passes that struct as the interface{} argument to the endpoint function. The endpoint asserts the type, extracts the name, and calls svc.Greet. The service returns a string or an error. The endpoint wraps the string in GreetResponse and returns it. The HTTP transport encodes the response struct back into JSON and writes the status code.
If the type assertion fails, the program panics at runtime. The compiler cannot verify interface{} conversions. You will see a runtime panic like panic: interface conversion: interface {} is main.OtherRequest, not main.GreetRequest. This is the tradeoff for the uniform endpoint signature. You must ensure your transport decoders and endpoint assertions stay in sync. Go Kit provides helper functions in its transport packages to reduce this friction, but the responsibility remains yours.
Middleware wraps the endpoint function before the transport ever sees it. Each middleware is a higher-order function that takes an endpoint and returns a new endpoint. You compose them by chaining assignments. The outermost middleware runs first on the request path and last on the response path. This onion model lets you add logging, tracing, rate limiting, and timeouts without modifying the endpoint or the service.
Wiring the server and stacking middleware
The main function is where the layers meet. You instantiate the service, build the endpoint, wrap it in middleware, and hand it to the transport server. Go Kit's HTTP transport implements the standard http.Handler interface, so it plugs directly into http.ListenAndServe. You do not need a custom server loop.
// main.go
func main() {
// Instantiate the pure service implementation.
svc := greeter{}
// Build the base endpoint from the service.
ep := MakeGreetEndpoint(svc)
// Wrap with logging middleware to print request/response details.
ep = log.Middleware(log.NewStdlibLogger(os.Stdout))(ep)
// Wrap with timeout middleware to enforce a 5-second deadline.
ep = endpoint.Timeout(5 * time.Second)(ep)
// Create the HTTP server with JSON request/response encoders.
// The nil arguments use default JSON encoding for request and response.
httpHandler := http.NewServer(ep, nil, nil)
// Start the standard library HTTP server on port 8080.
log.Fatal(http.ListenAndServe(":8080", httpHandler))
}
The middleware stack executes in reverse order of wrapping. The timeout middleware sets a deadline on the context. The logging middleware records the start time. The endpoint runs. The logging middleware calculates the duration and prints it. If the service takes longer than five seconds, the context cancels, the endpoint returns a deadline-exceeded error, and the transport maps it to a 504 response.
Error handling in Go is verbose by design. You will write if err != nil { return err } repeatedly. The community accepts this boilerplate because it makes the unhappy path visible. Do not swallow errors. Do not use panic for control flow. Return them through the endpoint signature and let the transport layer decide the HTTP status code. Go Kit's error package provides Error types that carry HTTP status codes and gRPC status codes, so you can return a single error value that works across transports.
Trust the middleware stack. It handles the cross-cutting concerns so your endpoints stay thin.
The maintenance reality and runtime traps
Go Kit is in maintenance mode. The core team is not adding new features or transports. The codebase is stable, well-tested, and widely deployed in production, but it will not evolve with newer Go features like generics or workspaces. For existing services, it remains a solid choice. For greenfield projects, the ecosystem has shifted toward lighter patterns.
The interface{} endpoint signature is the most common source of confusion. Beginners expect type safety across the transport boundary. Go Kit sacrifices compile-time checking for runtime flexibility. You must keep request/response structs in sync with your transport decoders. A mismatch causes a runtime panic, not a compile error. Write integration tests that hit the actual HTTP or gRPC server to catch these mismatches early.
Another trap is over-engineering the middleware stack. Each middleware adds latency and complexity. Stack too many layers and debugging becomes a guessing game. Start with logging and timeouts. Add tracing only when you need distributed request tracking. Add rate limiting only when you hit capacity. Keep the stack shallow until metrics prove otherwise.
Convention matters in Go Kit projects. Run gofmt on every file. Do not argue about indentation or brace placement. Let the tool decide. Most editors run it on save. Name your context variables ctx. Keep them as the first parameter in every function that crosses a package boundary. Use the underscore _ to discard values you intentionally ignore, like a second return value from a channel receive. Public names start with a capital letter. Private names start lowercase. There are no public or private keywords. Visibility is controlled by capitalization alone.
The worst goroutine bug is the one that never logs. Go Kit does not manage goroutines for you. If you spawn background workers inside a service method, ensure they respect context cancellation. A leaked goroutine will hold onto memory and file descriptors until the process restarts. Always pass ctx to long-running operations and check ctx.Err() in loops.
Go Kit is a pattern enforcer, not a framework. It gives you structure, not magic.
When to reach for Go Kit versus other tools
Use Go Kit when you need a strict transport-agnostic boundary and want battle-tested middleware for logging, tracing, and timeouts. Use the standard library with clean architecture when your service only needs HTTP and you want zero external dependencies. Use gRPC with a gateway when you prioritize strong typing, protobuf contracts, and automatic HTTP/JSON translation. Use a full framework like Gin or Echo when you need rapid routing, built-in validation, and a conventional MVC layout. Use a service mesh when cross-cutting concerns like retries, circuit breaking, and mTLS should live outside your application code.
Pick the tool that matches your operational maturity. Do not add middleware layers to solve problems that routing or configuration can handle.