最省事的grpc调试方式是用grpcurl直连服务端验证逻辑,需开启反射或指定proto文件,注意json字段名匹配、明文连接配置insecure.newcredentials(),结合日志、抓包和断点定位各层问题。

用 grpcurl 直连调试,绕过客户端代码
本地开发时最省事的调试方式,就是跳过自己写的 client 代码,用 grpcurl 直接发请求验证服务端逻辑是否正确。它不依赖 Go 环境,也不需要编译,只要 proto 文件或服务反射开启即可。
常见错误现象:执行 grpcurl -plaintext localhost:9000 list 报错 Failed to list services: server does not support the reflection API —— 这说明服务端没开反射,不是 client 写错了,而是服务启动时漏了配置。
- 确保
internal/server/grpc.go中创建*grpc.Server时传入了grpc.ReflectionProvider(reflection.GRPCServerReflector(srv))(Kratos v2.7+ 默认不启用) - 如果不想开反射,可改用
-proto指定本地.proto文件:grpcurl -plaintext -proto api/helloworld/v1/helloworld.proto localhost:9000 helloworld.v1.Greeter/SayHello - 发送带 body 的请求需用
-d,注意 JSON key 必须匹配 proto 中字段名(如name而非Name),且嵌套 message 要展开写:-d '{"name": "kratos"}'
在 client 代码里加日志和拦截器看调用链路
当 grpcurl 能通但业务 client 调不通,问题一定出在 client 初始化或上下文传递环节。这时候不能只看 error,得把每一步的连接状态、拦截器执行、ctx deadline 都打出来。
容易踩的坑:日志只打 err != nil,却忽略 ctx.Err() 导致误判为网络失败,实际是超时被 cancel。
- 在
internal/client/grpc.go的grpc.Dial后加grpc.WithUnaryInterceptor打印请求耗时:log.Printf("[grpc] %s → %s, cost: %v", method, target, time.Since(start)) - 务必检查
grpc.WithTransportCredentials是否设置:本地用明文必须显式传insecure.NewCredentials(),漏掉会 panic 或卡死在context deadline exceeded - 确认 client 初始化时传入的地址是服务发现地址(如
discovery:///helloworld.service)还是直连地址(如127.0.0.1:9000),两者行为完全不同;直连调试建议先用后者排除注册中心干扰
抓包看真实 gRPC 流量,确认协议层有没有问题
当拦截器日志显示“已发出请求”但服务端完全收不到,或者返回奇怪的 HTTP/2 错误码(如 HTTP/2 stream ID 13 is closed),就得跳出应用层,看 wire 上到底发生了什么。
gRPC 是基于 HTTP/2 的二进制协议,Wireshark 默认看不懂,必须配置解密或用专用工具。
- 用
mitmproxy或Charles无法解密 gRPC 流量(它们不支持 ALPN 和 TLS-ALPN 透传),别白费时间 - 推荐用
evans(比grpcurl更友好)或直接tcpdump -i lo port 9000 -w grpc.pcap+ Wireshark 加载grpc.dissector插件(需手动编译) - 重点观察:TLS 握手是否成功、SETTINGS 帧是否交换、HEADERS 帧里
:path是否为/helloworld.v1.Greeter/SayHello、DATA 帧 payload 是否被截断(可能因MaxRecvMsgSize太小)
服务端加断点看 request 解析和响应路径
客户端一切正常,但服务端返回空 response 或 panic,大概率是 proto 定义与生成代码不一致,或者中间件提前终止了流程。
最容易被忽略的是:Kratos 的 transport/http 和 transport/grpc 共享同一套 middleware 链,但 gRPC 请求不会触发 HTTP middleware 里的 Bind 或 Validate,除非你显式用了 transport.GRPC 标签。
- 在
internal/service/greeter_service.go的方法入口设断点,检查req字段是否为零值——如果是,说明 proto 生成时字段名/类型不匹配,或 client 发的 JSON key 写错了 - 检查
internal/middleware/xxx.go里是否有if tr, ok := transport.FromServerContext(ctx); ok && tr.Kind() == transport.GRPC这类判断,避免 HTTP 专用逻辑误入 gRPC 流程 - 流式接口(
stream HelloReply)必须在同一个 goroutine 调用srv.Send(),一旦起新 goroutine 并发 Send,会 panic:send on closed channel,这个 panic 不会打到日志,只能靠断点或 pprof goroutine dump 发现











