Chapter 04

Unary RPC 实战

实现完整的 Go gRPC 服务端与多语言客户端,掌握错误处理的正确姿势

Go 模块初始化

# 初始化 Go 模块
mkdir product-service && cd product-service
go mod init github.com/myapp/product-service

# 安装 gRPC 依赖
go get google.golang.org/grpc
go get google.golang.org/protobuf

定义商品服务 Proto

// proto/product/v1/product.proto
syntax = "proto3";
package product.v1;
option go_package = "github.com/myapp/product-service/gen/product/v1;productv1";

message Product {
  string id          = 1;
  string name        = 2;
  double price       = 3;
  int32  stock       = 4;
  string description = 5;
}

message GetProductRequest    { string id = 1; }
message CreateProductRequest {
  string name        = 1;
  double price       = 2;
  int32  stock       = 3;
  string description = 4;
}
message UpdateStockRequest   { string id = 1; int32 delta = 2; }
message ProductResponse      { Product product = 1; }
message DeleteProductRequest  { string id = 1; }

import "google/protobuf/empty.proto";

service ProductService {
  rpc GetProduct    (GetProductRequest)    returns (ProductResponse);
  rpc CreateProduct (CreateProductRequest) returns (ProductResponse);
  rpc UpdateStock   (UpdateStockRequest)   returns (ProductResponse);
  rpc DeleteProduct (DeleteProductRequest) returns (google.protobuf.Empty);
}

Go 服务端实现

// server/main.go
package main

import (
  "context"
  "fmt"
  "log"
  "net"
  "sync"

  "google.golang.org/grpc"
  "google.golang.org/grpc/codes"
  "google.golang.org/grpc/status"
  "google.golang.org/protobuf/types/known/emptypb"

  pb "github.com/myapp/product-service/gen/product/v1"
)

// productServer 实现 ProductServiceServer 接口
type productServer struct {
  pb.UnimplementedProductServiceServer  // 必须嵌入!
  mu       sync.RWMutex
  products map[string]*pb.Product
}

func NewProductServer() *productServer {
  return &productServer{
    products: make(map[string]*pb.Product),
  }
}

// GetProduct 实现 Unary RPC
func (s *productServer) GetProduct(
  ctx context.Context,
  req *pb.GetProductRequest,
) (*pb.ProductResponse, error) {
  s.mu.RLock()
  defer s.mu.RUnlock()

  p, ok := s.products[req.Id]
  if !ok {
    // 返回 gRPC 标准错误
    return nil, status.Errorf(codes.NotFound,
      "product %s not found", req.Id)
  }
  return &pb.ProductResponse{Product: p}, nil
}

func (s *productServer) CreateProduct(
  ctx context.Context,
  req *pb.CreateProductRequest,
) (*pb.ProductResponse, error) {
  if req.Name == "" {
    return nil, status.Error(codes.InvalidArgument, "name is required")
  }
  if req.Price <= 0 {
    return nil, status.Error(codes.InvalidArgument, "price must be positive")
  }

  id := fmt.Sprintf("prod-%d", time.Now().UnixNano())
  p := &pb.Product{
    Id: id, Name: req.Name,
    Price: req.Price, Stock: req.Stock,
    Description: req.Description,
  }

  s.mu.Lock()
  s.products[id] = p
  s.mu.Unlock()

  return &pb.ProductResponse{Product: p}, nil
}

func (s *productServer) DeleteProduct(
  ctx context.Context,
  req *pb.DeleteProductRequest,
) (*emptypb.Empty, error) {
  s.mu.Lock()
  defer s.mu.Unlock()

  if _, ok := s.products[req.Id]; !ok {
    return nil, status.Errorf(codes.NotFound,
      "product %s not found", req.Id)
  }
  delete(s.products, req.Id)
  return &emptypb.Empty{}, nil
}

func main() {
  lis, err := net.Listen("tcp", ":50051")
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }

  // 创建 gRPC Server
  s := grpc.NewServer()

  // 注册服务实现
  pb.RegisterProductServiceServer(s, NewProductServer())

  log.Printf("gRPC server listening on :50051")
  if err := s.Serve(lis); err != nil {
    log.Fatalf("failed to serve: %v", err)
  }
}

Go 客户端实现

// client/main.go
package main

import (
  "context"
  "log"
  "time"

  "google.golang.org/grpc"
  "google.golang.org/grpc/credentials/insecure"

  pb "github.com/myapp/product-service/gen/product/v1"
)

func main() {
  // 创建连接(开发环境用 insecure,生产环境需要 TLS)
  conn, err := grpc.NewClient(
    "localhost:50051",
    grpc.WithTransportCredentials(insecure.NewCredentials()),
  )
  if err != nil {
    log.Fatalf("failed to connect: %v", err)
  }
  defer conn.Close()

  // 创建客户端存根
  client := pb.NewProductServiceClient(conn)

  // 带超时的 context
  ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  defer cancel()

  // 创建商品
  resp, err := client.CreateProduct(ctx, &pb.CreateProductRequest{
    Name:  "iPhone 16 Pro",
    Price: 9999.0,
    Stock: 100,
  })
  if err != nil {
    log.Fatalf("CreateProduct failed: %v", err)
  }
  log.Printf("Created: %+v", resp.Product)

  // 查询商品
  getResp, err := client.GetProduct(ctx, &pb.GetProductRequest{
    Id: resp.Product.Id,
  })
  if err != nil {
    log.Fatalf("GetProduct failed: %v", err)
  }
  log.Printf("Got: %+v", getResp.Product)
}

错误处理:status 与 codes

gRPC 有标准的错误状态码体系,类似 HTTP 状态码但更面向 RPC 场景。正确使用状态码是 API 设计的重要规范。

codes 常量HTTP 对应含义使用场景
OK200成功正常返回
InvalidArgument400参数无效必填字段为空、格式错误
NotFound404资源不存在查询不到指定 ID 的记录
AlreadyExists409资源已存在创建重复记录
PermissionDenied403权限不足已认证但无操作权限
Unauthenticated401未认证缺少或无效的 Token
ResourceExhausted429资源耗尽限流、配额超限
Internal500内部错误服务端意外错误
Unavailable503服务不可用服务过载、正在重启
DeadlineExceeded504超时请求超过 deadline
Unimplemented501未实现方法未实现
// 服务端:返回带详情的错误
import (
  "google.golang.org/grpc/codes"
  "google.golang.org/grpc/status"
)

// 简单错误
return nil, status.Error(codes.NotFound, "product not found")

// 格式化错误消息
return nil, status.Errorf(codes.InvalidArgument,
  "price must be positive, got %f", req.Price)

// 客户端:解析错误
resp, err := client.GetProduct(ctx, req)
if err != nil {
  st, ok := status.FromError(err)
  if ok {
    switch st.Code() {
    case codes.NotFound:
      log.Printf("商品不存在: %s", st.Message())
    case codes.InvalidArgument:
      log.Printf("参数错误: %s", st.Message())
    default:
      log.Printf("未知错误 [%s]: %s", st.Code(), st.Message())
    }
  }
  return
}

Python 客户端调用同一服务

# client.py — Python 客户端调用 Go gRPC 服务端
import grpc
import product_pb2
import product_pb2_grpc

def run():
    # 创建 channel(不安全,仅开发用)
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = product_pb2_grpc.ProductServiceStub(channel)

        # 调用 CreateProduct
        create_req = product_pb2.CreateProductRequest(
            name='MacBook Pro M4',
            price=19999.0,
            stock=50
        )
        try:
            resp = stub.CreateProduct(create_req)
            print(f'Created: {resp.product.id} - {resp.product.name}')

            # 调用 GetProduct
            get_resp = stub.GetProduct(
                product_pb2.GetProductRequest(id=resp.product.id)
            )
            print(f'Got: {get_resp.product.name}, Price: {get_resp.product.price}')

        except grpc.RpcError as e:
            # 解析 gRPC 错误
            print(f'RPC Error: code={e.code()}, msg={e.details()}')

if __name__ == '__main__':
    run()

跨语言互通:这是 gRPC 最强大的特性之一。Go 服务端和 Python 客户端可以无缝通信,因为双方都基于同一份 .proto 文件生成代码,数据格式完全一致。企业中常见的场景是:Go 微服务提供高性能后端,Python/Java 等其他语言的服务调用它。

本章小结:Unary RPC 是 gRPC 中最常用的模式,掌握它就能覆盖绝大多数 CRUD 场景。注意始终使用标准错误码,便于客户端区分处理;生产环境务必为 context 设置超时;创建 Channel 代价较大,应在应用启动时创建并全局复用。