Skip to main content
  1. Posts/

Lightweight gRPC on the Go Standard Library

·2592 words·13 mins·
Author
Agent IO
Sidecar is a tiny Go package that can be used to make clients and servers that use the gRPC wire protocol.

Why I Built Sidecar

I wrote Sidecar to use with IO after first trying the official grpc-go library and then using connect-go, a gRPC-compatible library created by Buf.

IO is a network proxy that builds on Envoy. We might say that Envoy is an implementation detail of IO, but it's an important one. Envoy handles all of the network traffic that IO manages. IO runs Envoy as a local subprocess and configures this child Envoy using Envoy's gRPC APIs. For these configuration APIs, Envoy sends gRPC requests to IO, which means that IO is a gRPC server that serves APIs to its child Envoy. But unlike typical gRPC servers, IO just serves this one client that is running on the same machine and, in managed environments, in the same pod. For simplicity and efficiency, the IO gRPC APIs listen on a Linux abstract socket.

The libraries released by the gRPC project are not built for applications like IO. Originally created for use in data centers, they contain lots of powerful features that IO doesn't use. Here's a list of some of them that was presented at the 2025 gRPConf:

  • Name Resolution (Service Discovery, Pluggable)
  • Load Balancer (Manage subchannels, seamless HTTP/2)
  • Interceptor (Powerful middleware)
  • Deadlines/Timeouts, Cancellation (Safeguard against network latency or server issues, Optimize resource usage)
  • Retry (Fault-tolerant and resilient)
  • Termination (Resource cleanup)

Some of these features add dependencies that IO doesn't need, or worse, that might conflict with things that IO does need. For example, grpc-go depends on OpenTelemetry libraries that contain Protocol Buffer-based generated code, which means that the protocol buffer runtime used by gRPC must be kept in sync with the one used by anything else in IO. We also have to be careful to not include any protobuf-generated code more than once, as this triggers a fatal error on startup in the Go protobuf runtime. If IO used any of these features, it might be worth this extra coordination, but it doesn't, so it seems better to not carry this baggage.

Connect is better, but still isn't a great fit for IO. As described in the connect-go README, Connect "is a simple protocol that works over HTTP/1.1 or HTTP/2. It takes the best portions of gRPC and gRPC-Web, including streaming, and packages them into a protocol that works equally well in browsers, monoliths, and microservices." The Connect developers did a great job of minimizing dependencies, but Connect still adds things that IO doesn't need: IO doesn't use HTTP/1.1 or gRPC-web and IO doesn't need JSON framing. But these are built into Connect, and their support makes Connect code layered and complicated.

So why use either of these libraries? One of them might make sense if the gRPC messaging protocol was fundamentally too hard to implement ourselves. But as we'll see, it's not.

Software Complexity and Where to Put It

As you might guess, the complexity of software is a major concern to me. Let's digress briefly to think about it.

Three Laws

In Three Laws of Software Complexity, software researcher and former professor Mahesh Balakrishnan makes three claims:

  • A well-designed system will degrade into a badly designed system over time.
  • Complexity is a moat.
  • There is no fundamental upper limit on software complexity.

If you didn't click through my link above, go read his discussion now.

What about gRPC?

It's not different to imagine how Mahesh's three claims might apply to gRPC implementations.

Here's a memorable observation that I heard from John Ousterhout, the creator of TCL, Magic, Raft (and more) and the author of A Philosophy of Software Design. He was talking about the C implementation of gRPC, but I think this is more broadly applicable to the project:

I've been programming for a little bit more than 50 years now... and I think probably the gRPC codebase is the most challenging one that I've ever worked in... it's very complex with a very large number of classes, many layers, and very deep call chains... this makes it pretty difficult to see the structure of the code... trying to figure out exactly which three lines of code I had to write took several days of work... and unfortunately there's almost no internal documentation in the gRPC code base... there's essentially zero comments." Integrating gRPC with the Homa Transport Protocol - John Ousterhout, Stanford University

Ousterhout is as much of a hero to me as anyone, and I was at this conference and made a point to be in this session. I nearly jumped out of my chair when I heard this. If even he had this impression, then surely I could too.

The video link below takes you directly to his full quote.

The day that I first learned of gRPC

I first learned of gRPC on January 11, 2016, my first day at Google, when my new tech lead and future manager Louis Ryan picked me up at orientation. "This can't be serious," I thought to myself as Louis described Google's new RPC protocol, because I had come to his team through the unlikely path of iOS app development.

Before Google, I wrote iPhone apps, and although I'd interviewed with Google as a potential iOS developer, I was ready for a change and found a director who was willing to hire me into the "One Platform" team, the team that managed all of Google's public APIs (or at least the ones that conformed with the top-down mandates). So why was gRPC distasteful to me as an iOS developer? With gRPC, Google was expecting me to bundle Google-developed code into my applications with the expectation that Google's developers had the skill, experience, and integrity to put code into a product that Apple's app store reviewers and customers would hold me responsible.

Just a few years earlier, I produced a conference for iOS developers. One of our speakers was Domingo Guerra, who spoke about security and privacy problems that his auditing company had found in mobile applications. Number one in his list of "top five failures" was "Using Risky SDKs". The link below takes you directly to this discussion:

The leading "Risky SDKs" that Domingo discussed were ad network SDKs, which were notoriously closed and misbehaving. As Domingo described, some of the SDKs that he observed scraped all kinds of private information about users that app developers didn't even know they were taking. This was leading to real-world app store rejections and even legal liabilities for the app developers. But even if the SDK that you use is open source and isn't stealing private information, it could still cause problems in your app by requiring weird build systems (remember Cocoapods?), by linking obsolete dependencies, by colliding with other libraries that your app uses, by failing to follow current idioms and best practices, or even by just not working.

Where to put complexity?

When complexity is necessary, where should we put it? One answer is "someplace where I don't have to see it". gRPC hides complexity beneath its API, which is helpful, but because that underlying code is still linked into our applications, gRPC is only superficially hiding only its complexity. Complexity still appears in build times, surprise symbol conflicts, unknown liabilities, and a regular drumbeat of dependency updates driven by vulnerability reports and package scanners.

We don't need to worry about any of this for code that is not in our apps, so "outside of my application" is another, better, answer to this question.

gRPC and Sidecar

With all of these concerns, why do I still think it is good to use gRPC?

gRPC abstractions simplify our projects

gRPC simplifies projects through the discipline of clear patterns. As gRPC users know, gRPC defines four "modes" based on whether clients and servers are streaming or not, and these modes are all layered on an underlying reusable connection. Also, by encouraging messaging using Protocol Buffers, gRPC promotes fast and type-safe message serialization that requires applications to formally define their interfaces. I think all of this is good and, by the way, a big reason that I want to use gRPC is that gRPC is used to make APIs that I myself want to use, like Envoy's control APIs.

What gRPC Is

Early in its life, gRPC was described by an IETF Draft that described a wire protocol based on HTTP/2 that used a very simple message framing. Each message was preceeded by five bytes: a one byte compression flag followed by a four byte big-endian message length. Messages were multiplexed over a shared HTTP/2 connection. The gRPC team has written a lot of custom code for this but in many cases, such as using the Go standard library, this multiplexing is hidden below the standard library's networking API and just works.

That IETF draft was abandoned by the gRPC team. Instead of pursuing protocol standardization, the project has focused on the details of an ever-growing set of implementations. To see many of the things this includes, visit the gRPC Proposals repository. Many of these proposals are responses to Envoy's popularity in service meshes. To reduce certain costs, the gRPC project would like to eliminate the need for Envoy, essentially by building support for many of Envoy's features and configurability directly into gRPC. That might sound great at first, but have you counted the number of programming languages that gRPC supports? Do you expect that all of those features will be consistently supported in all of those languages? Do you really want to link all of that into all of your applications? Do you have a big enough CI system to rebuild, retest, and redeploy all of your applications each time one of these libraries has a version bump?

What Sidecar Is

Sidecar is a tiny Go library that I wrote to help me write gRPC client and servers. It's mostly just a super-thin layer that handles message framing and transmission, but it also weaves in use of Go generics so that gRPC clients and servers can be written without any generated code. If your APIs use Protocol Buffers, you will probably still have generated code for your messages, but that's all the generated code that you need. Overall, the failure surface of networking code becomes vastly smaller. With less networking code, we test and rebuild less, and our apps work better.

Sidecar is written exclusively in Go and is completely in the agentio/sidecar repo. Tests there demonstrate how Sidecar can be used with Protocol Buffer-based messages and messages of raw bytes. A separate repo, agentio/echo-sidecar, contains a CLI that implements a client and server for a Protocol Buffer-based service that demonstrates all four gRPC streaming modes.

Examples

Here's the code for that echo-sidecar server that demonstrates all four gRPC streaming modes:

func Cmd() *cobra.Command {
	var port int
	var socket string
	var verbose bool
	cmd := &cobra.Command{
		Use:  "serve",
		Args: cobra.NoArgs,
		RunE: func(cmd *cobra.Command, args []string) error {
			mux := http.NewServeMux()
			mux.HandleFunc(constants.EchoGetProcedure, sidecar.HandleUnary(get))
			mux.HandleFunc(constants.EchoExpandProcedure, sidecar.HandleServerStreaming(expand))
			mux.HandleFunc(constants.EchoCollectProcedure, sidecar.HandleClientStreaming(collect))
			mux.HandleFunc(constants.EchoUpdateProcedure, sidecar.HandleBidiStreaming(update))
			server := sidecar.NewServer(mux)
			var err error
			var listener net.Listener
			if port == 0 {
				listener, err = net.Listen("unix", socket)
			} else {
				listener, err = net.Listen("tcp", fmt.Sprintf(":%d", port))
			}
			if err != nil {
				return err
			}
			return server.Serve(listener)
		},
	}
	cmd.Flags().IntVarP(&port, "port", "p", 0, "server port")
	cmd.Flags().StringVarP(&socket, "socket", "s", "@echo", "server socket")
	cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "verbose")
	return cmd
}

This is just using the Go standard library to set up an HTTP server with handler functions for each of the gRPC methods that we want to serve. That server needs to serve HTTP/2, but that also is configurable using the Go standard library. Here that is in a helper function in the Sidecar implementation:

// NewServer creates an http.Server instance that is configured for h2c communication.
//
// With appropriate handlers, this can be used to run gRPC services.
func NewServer(handler http.Handler) *http.Server {
	// Configure protocols for h2c-only support (HTTP/2 cleartext)
	protocols := new(http.Protocols)
	protocols.SetUnencryptedHTTP2(true) // Enable h2c (HTTP/2 cleartext)
	protocols.SetHTTP1(false)           // Explicitly disable HTTP/1.1
	protocols.SetHTTP2(false)           // Explicitly disable encrypted HTTP/2 (HTTPS)
	return &http.Server{
		Handler:   handler,
		Protocols: protocols,
	}
}

The server handlers are further down in the first file that we sampled. Here's the unary method handler:

func get(ctx context.Context, req *sidecar.Request[echopb.EchoRequest]) (*sidecar.Response[echopb.EchoResponse], error) {
	return sidecar.NewResponse(&echopb.EchoResponse{
		Text: "Go echo get: " + req.Msg.Text,
	}), nil
}

When it installed in the HTTP server, it gets wrapped in this generic function that is defined in Sidecar:

// HandleUnary wraps a unary function in an HTTP handler.
func HandleUnary[Req any, Res any](fn UnaryFunction[Req, Res]) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/grpc")
		var request Req
		var response *Response[Res]
		err := Receive(r.Body, &request)
		if err != nil {
			goto end
		}
		response, err = fn(r.Context(), &Request[Req]{Msg: &request})
		if err != nil {
			goto end
		}
		err = Send(w, response.Msg)
	end:
		WriteTrailer(w, err)
	}
}

This function uses Receive, another Sidecar helper function, to get the request message from the incoming stream and later calls Send to send its reply. These functions work directly on the io.Reader and io.Writer that are created by the Go standard library for the inbound request to the server.

If you sit with the code for a bit, or maybe fork it and add a few print statements, I think you'll find it all is quite clear for servers and clients of unary and all of the streaming methods. All of the messaging code that's not in the Go standard library is in the sidecar directory. Including comments and excluding tests, its less than 750 lines.

For more discussion of how this all works, see Kevin McDonald's gRPC From Scratch series.

Comparisons

One way to look at complexity is to compare the go.sum files of sidecar, grpc-go, and connect-go. First, the grpc-go go.sum is currently 95 lines.

cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spiffe/go-spiffe/v2 v2.7.0 h1:uXe1MflJoHw58wAUvxVlcM7WpKtijWG7I1UidcGh6g4=
github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

That's a lot of dependencies! In contrast, Sidecar's go.sum is much smaller.

github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=

The winner of our go.sum golf game, however, is connect-go, with an even tinier go.sum:

github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=

"How did this happen?" I wondered, and traced the difference to Sidecar's use of the x/net package to get HTTP/2 error codes, which Akshay Shah, the creator of connect-go, avoided with a workaround that I am still processing.

Just counting files, we note that the grpc-go project contains 1037 Go files, connect-go contains 85 Go files, and sidecar contains 21.

So you have a choice. You can either trust a large organization to properly build and test a challenging-to-understand but widely-used implementation, or you can trust a small but respectably-sized open source team to take care of an implementation that you probably don't need to think about but might not want to read, or you can trust no one1 and just read the 750 lines of code in the sidecar implementation and see what you think. Depending on your situation, any of these might be the right choice for you.

What I'm really doing with IO

The Sidecar project is part of a bigger agenda for me. I'm not interested in replacing grpc-go or connect-go, but I am interested in having a simple alternative that I can fully grok and use to build IO and the applications that use IO.

IO is where networking complexity goes to be managed

IO handles all of the complex things that I don't want in my gRPC libraries. It does this out-of-process in a binary that can be easily upgraded and shared among many applications. If you're a developer, this means that you have less code in your apps and more time to think about the problems that you're solving. If you're into platform governance, this means that you only need to pay attention to the version of IO that your applications use and how it's configured. You don't have to continuously monitor the build processes and vulnerability scans of dozens of applications and somehow ensure that those applications link in the correct versions of their dependencies and also correctly configure and use them. IO exists to make applications more manageable and secure while also making life more pleasant for developers.

Applications serve and call APIs through IO with Sidecar

If you're writing an application that uses a gRPC API, Sidecar and IO make that a lot easier. IO handles authentication and retry, so you don't have any of that in your application code. If your IO is local, you can also make these calls over a socket2, and TCP ports also work fine. IO keeps your secrets safe: using them when you want them to be used, logging and monitoring their usage, and because IO keeps them out of your applications, those applications can't lose, steal, or accidentally leak secrets. Your could make its calls to IO with a standard gRPC library, but why? Sidecar works great for that and has far less complexity.

I have code in another repo that uses Sidecar and IO to call the Google Cloud Translate API. I'll write more about that in another post. Until then, it's here:

🦋 Comment with ATProto


  1. Really, you don't even have to trust me. Just make your own copy of the Sidecar repo or copy its code into your project. ↩︎

  2. I love Linux abstract sockets! For one reason why, see https://github.com/mschwager/socket-speed ↩︎