Microservices & gRPC
Microservices: Switching to gRPC (and Connect)
REST is fine for public APIs. But for internal communication between microservices, JSON over HTTP/1.1 is inefficient (text parsing, no types, high overhead).
gRPC (Google Remote Procedure Call) is the standard for high-performance internal traffic.
The Protocol Buffers (Protobuf)
You define your data and service in a .proto file.
syntax = "proto3";
service UserService {
rpc GetUser (UserRequest) returns (UserResponse);
}
message UserRequest {
string id = 1;
}
message UserResponse {
string name = 1;
int32 age = 2;
}Then, you compile this using protoc to generate Go code. * Strict Types: The generated code guarantees the wire format matches the struct. * Binary: Messages are binary (smaller, faster to parse than JSON).
The gRPC Complexity (and the Solution: ConnectRPC)
Classic gRPC requires special load balancers (because it breaks HTTP/1.1 framing) and is hard to debug with curl.
In 2026, we prefer ConnectRPC (by Buf). * It’s just HTTP: It supports gRPC but also standard HTTP/JSON. * You can curl a Connect service with JSON, but talk to it via gRPC from other Go services.
Connect Example
package main
import (
"context"
"net/http"
"connectrpc.com/connect"
user "example/gen/user/v1" // Generated code
"example/gen/user/v1/userv1connect"
)
type UserServer struct {}
func (s *UserServer) GetUser(
ctx context.Context,
req *connect.Request[user.UserRequest],
) (*connect.Response[user.UserResponse], error) {
return connect.NewResponse(&user.UserResponse{
Name: "Alice",
Age: 30,
}), nil
}
func main() {
mux := http.NewServeMux()
path, handler := userv1connect.NewUserServiceHandler(&UserServer{})
mux.Handle(path, handler)
http.ListenAndServe(":8080", mux)
}When to use Microservices?
Default to Monolith. Microservices introduce: 1. Network Latency. 2. Distributed Tracing requirements. 3. Deployment complexity.
Only switch to Microservices (and gRPC) when: * You have distinct teams who need to deploy independently. * You have distinct scaling requirements (e.g., the Video Encoder needs 100 GPUs, but the Login server needs 1 CPU).
Worked example
Unary JSON-RPC client/server with typed request envelopes.
Save as main.go. Then:
go mod init example
go run .package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
)
type Req[T any] struct {
Payload T `json:"payload"`
}
type Resp[T any] struct {
Payload T `json:"payload"`
}
type AddIn struct {
A, B int `json:"a"`
}
type AddOut struct {
Sum int `json:"sum"`
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /calc.v1/Add", func(w http.ResponseWriter, r *http.Request) {
var in Req[AddIn]
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "bad json", 400)
return
}
_ = json.NewEncoder(w).Encode(Resp[AddOut]{Payload: AddOut{Sum: in.Payload.A + in.Payload.B}})
})
srv := httptest.NewServer(mux)
defer srv.Close()
body, _ := json.Marshal(Req[AddIn]{Payload: AddIn{A: 2, B: 40}})
res, err := http.Post(srv.URL+"/calc.v1/Add", "application/json", bytes.NewReader(body))
if err != nil {
panic(err)
}
defer res.Body.Close()
var out Resp[AddOut]
_ = json.NewDecoder(res.Body).Decode(&out)
fmt.Println("sum:", out.Payload.Sum)
}Expected output:
sum: 42
More examples
RPC error envelope + status mapping.
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /user.v1/Get", func(w http.ResponseWriter, r *http.Request) {
var req struct {
ID string `json:"id"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
if req.ID == "" {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "id required"})
return
}
_ = json.NewEncoder(w).Encode(map[string]string{"name": "Ada"})
})
srv := httptest.NewServer(mux)
defer srv.Close()
res, _ := http.Post(srv.URL+"/user.v1/Get", "application/json", bytes.NewReader([]byte(`{}`)))
fmt.Println("status:", res.StatusCode)
res.Body.Close()
}Expected output:
status: 400
Runnable example
Note: Production internal APIs often use gRPC or ConnectRPC with protobuf. This stdlib stand-in is a typed JSON “RPC over HTTP” service—same request/response shape idea, no codegen.
Save as main.go. Then:
go mod init example
go run .package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
)
type UserRequest struct {
ID string `json:"id"`
}
type UserResponse struct {
Name string `json:"name"`
Age int `json:"age"`
}
type ErrorBody struct {
Error string `json:"error"`
}
func getUser(id string) (UserResponse, bool) {
db := map[string]UserResponse{
"u1": {Name: "Alice", Age: 30},
"u2": {Name: "Bob", Age: 25},
}
u, ok := db[id]
return u, ok
}
func main() {
mux := http.NewServeMux()
// Connect/gRPC-style unary RPC as JSON POST
mux.HandleFunc("POST /user.v1.UserService/GetUser", func(w http.ResponseWriter, r *http.Request) {
var req UserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(ErrorBody{Error: "invalid json"})
return
}
user, ok := getUser(req.ID)
if !ok {
w.WriteHeader(http.StatusNotFound)
_ = json.NewEncoder(w).Encode(ErrorBody{Error: "user not found"})
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(user)
})
srv := httptest.NewServer(mux)
defer srv.Close()
call := func(id string) {
body, _ := json.Marshal(UserRequest{ID: id})
resp, err := http.Post(srv.URL+"/user.v1.UserService/GetUser", "application/json", bytes.NewReader(body))
if err != nil {
panic(err)
}
defer resp.Body.Close()
var raw map[string]any
_ = json.NewDecoder(resp.Body).Decode(&raw)
fmt.Printf("id=%s status=%d body=%v\n", id, resp.StatusCode, raw)
}
call("u1")
call("missing")
}Expected output:
id=u1 status=200 body=map[age:30 name:Alice]
id=missing status=404 body=map[error:user not found]
What to notice: Method path + typed request/response mirrors an RPC surface even over JSON/HTTP. httptest keeps the demo offline-friendly. Real gRPC adds binary protobuf, streaming, and code-generated stubs.
Try next: Add a ListUsers method. Wrap the handler with a middleware that logs RPC name and status code.