Go语言:使用ConnectRPC构建API

ConnectRPC 是 Buf 团队推出的 RPC 框架,兼容 gRPC 协议的同时支持 HTTP/1.1 和 JSON,可以直接用 curl 调试。本文介绍如何用 Go + ConnectRPC 从零构建一个 API 服务,并与前端 connect-web 对接。

为什么不直接用 gRPC

gRPC 基于 HTTP/2,在以下场景会碰到摩擦:

  1. 浏览器直调不行 — 浏览器不暴露 HTTP/2 帧控制,必须走 gRPC-Web 代理(Envoy 等)
  2. curl 调试困难 — 二进制 protobuf + HTTP/2 trailers,命令行调试需要 grpcurl
  3. 负载均衡 — 很多 L7 负载均衡器对 HTTP/2 长连接支持不完善

ConnectRPC 同时支持三种协议:

协议 传输 编码 适用场景
Connect HTTP/1.1 或 HTTP/2 JSON 或 Protobuf 通用,curl 友好
gRPC HTTP/2 Protobuf 与现有 gRPC 服务互通
gRPC-Web HTTP/1.1 Protobuf 浏览器(无需代理)

一个 ConnectRPC 服务端同时服务这三种协议,客户端自动协商。

Protobuf 定义

// proto/greet/v1/greet.proto
syntax = "proto3";

package greet.v1;

option go_package = "example.com/myapp/gen/greet/v1;greetv1";

message GreetRequest {
  string name = 1;
}

message GreetResponse {
  string greeting = 1;
}

service GreetService {
  rpc Greet(GreetRequest) returns (GreetResponse) {}
}

使用 buf CLI 生成代码:

# buf.gen.yaml
version: v2
plugins:
  - remote: buf.build/protocolbuffers/go
    out: gen
    opt: paths=source_relative
  - remote: buf.build/connectrpc/go
    out: gen
    opt: paths=source_relative

# 生成
buf generate

这会生成 gen/greet/v1/greet.pb.go(消息)和 gen/greet/v1/greetv1connect/greet.connect.go(服务接口)。

Go 服务端实现

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"

    "connectrpc.com/connect"
    greetv1 "example.com/myapp/gen/greet/v1"
    "example.com/myapp/gen/greet/v1/greetv1connect"
    "golang.org/x/net/http2"
    "golang.org/x/net/http2/h2c"
)

type GreetServer struct{}

func (s *GreetServer) Greet(
    ctx context.Context,
    req *connect.Request[greetv1.GreetRequest],
) (*connect.Response[greetv1.GreetResponse], error) {
    log.Printf("Headers: %v", req.Header())
    if req.Msg.Name == "" {
        return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("name is required"))
    }
    return connect.NewResponse(&greetv1.GreetResponse{
        Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name),
    }), nil
}

func main() {
    greeter := &GreetServer{}
    mux := http.NewServeMux()
    path, handler := greetv1connect.NewGreetServiceHandler(greeter)
    mux.Handle(path, handler)

    // h2c 支持无 TLS 的 HTTP/2(开发环境)
    addr := ":8080"
    log.Printf("Listening on %s", addr)
    log.Fatal(http.ListenAndServe(addr, h2c.NewHandler(mux, &http2.Server{})))
}

启动后可以直接用 curl 调用:

curl http://localhost:8080/greet.v1.GreetService/Greet \
  -H "Content-Type: application/json" \
  -d '{"name": "World"}'

# {"greeting": "Hello, World!"}

不需要任何代理,不需要 grpcurl,就是普通的 HTTP + JSON。

拦截器(Interceptor)

ConnectRPC 的拦截器类似 gRPC 的 UnaryInterceptor,但 API 更简洁:

func loggingInterceptor() connect.UnaryInterceptorFunc {
    return func(next connect.UnaryFunc) connect.UnaryFunc {
        return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
            log.Printf("[%s] %s", req.Spec().Procedure, req.Header().Get("User-Agent"))
            resp, err := next(ctx, req)
            if err != nil {
                log.Printf("  error: %v", err)
            }
            return resp, err
        }
    }
}

// 注册
path, handler := greetv1connect.NewGreetServiceHandler(
    greeter,
    connect.WithInterceptors(loggingInterceptor()),
)

前端:connect-web

import { createConnectTransport } from "@connectrpc/connect-web";
import { createClient } from "@connectrpc/connect";
import { GreetService } from "./gen/greet/v1/greet_pb";

const transport = createConnectTransport({
  baseUrl: "http://localhost:8080",
});

const client = createClient(GreetService, transport);

const res = await client.greet({ name: "Frontend" });
console.log(res.greeting); // "Hello, Frontend!"

无需 Envoy 代理,直接从浏览器调用后端。

与 gRPC-Gateway 对比

维度 ConnectRPC gRPC-Gateway
额外组件 需要反向代理进程
协议 Connect + gRPC + gRPC-Web 三合一 gRPC -> REST 转译
浏览器调用 原生支持 需要代理
性能开销 直接服务 多一层转译
代码生成 buf 插件 protoc-gen-grpc-gateway
兼容性 可与任何 gRPC 客户端/服务端互通 仅提供 REST 入口

ConnectRPC 的核心优势是零额外基础设施:同一个 handler 同时服务 gRPC 客户端和 HTTP+JSON 客户端。如果你正在开始一个新项目,ConnectRPC 是比 gRPC + gRPC-Gateway 更简洁的选择。