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 对应 | 含义 | 使用场景 |
|---|---|---|---|
OK | 200 | 成功 | 正常返回 |
InvalidArgument | 400 | 参数无效 | 必填字段为空、格式错误 |
NotFound | 404 | 资源不存在 | 查询不到指定 ID 的记录 |
AlreadyExists | 409 | 资源已存在 | 创建重复记录 |
PermissionDenied | 403 | 权限不足 | 已认证但无操作权限 |
Unauthenticated | 401 | 未认证 | 缺少或无效的 Token |
ResourceExhausted | 429 | 资源耗尽 | 限流、配额超限 |
Internal | 500 | 内部错误 | 服务端意外错误 |
Unavailable | 503 | 服务不可用 | 服务过载、正在重启 |
DeadlineExceeded | 504 | 超时 | 请求超过 deadline |
Unimplemented | 501 | 未实现 | 方法未实现 |
// 服务端:返回带详情的错误
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 代价较大,应在应用启动时创建并全局复用。