我在之前的文章《go里面的异常处理》简单地说了下go的异常处理机制, 在web中, 一般可以通过框架层面提供的过滤器/拦截器统一地处理这种异常, 避免main函数被带崩.
grpc-go的异常处理比较简单, 需要注意的点其实就是需要针对Unary和Stream两种模式都添加拦截器
下面实现一个简单的异常处理拦截器并将其应用到Server中
interceptors.go
package grpc_recovery
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// RecoveryHandlerFunc is a function that recovers from the panic `p` by returning an `error`.
type RecoveryHandlerFunc func(p interface{}) (err error)
// RecoveryHandlerFuncContext is a function that recovers from the panic `p` by returning an `error`.
// The context can be used to extract request scoped metadata and context values.
type RecoveryHandlerFuncContext func(ctx context.Context, p interface{}) (err error)
// UnaryServerInterceptor returns a new unary server interceptor for panic recovery.
func UnaryServerInterceptor(opts ...Option) grpc.UnaryServerInterceptor {
o := evaluateOptions(opts)
fmt.Println("拦截器")
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (_ interface{}, err error) {
panicked := true
defer func() {
if r := recover(); r != nil || panicked {
err = recoverFrom(ctx, r, o.recoveryHandlerFunc)
}
}()
resp, err := handler(ctx, req)
panicked = false
return resp, err
}
}
// StreamServerInterceptor returns a new streaming server interceptor for panic recovery.
func StreamServerInterceptor(opts ...Option) grpc.StreamServerInterceptor {
o := evaluateOptions(opts)
fmt.Println("拦截器")
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) (err error) {
panicked := true
defer func() {
if r := recover(); r != nil || panicked {
err = recoverFrom(stream.Context(), r, o.recoveryHandlerFunc)
}
}()
err = handler(srv, stream)
panicked = false
return err
}
}
func recoverFrom(ctx context.Context, p interface{}, r RecoveryHandlerFuncContext) error {
if r == nil {
return status.Errorf(codes.Internal, "%v", p)
}
return r(ctx, p)
}
options.go
package grpc_recovery
import "context"
var (
defaultOptions = &options{
recoveryHandlerFunc: nil,
}
)
type options struct {
recoveryHandlerFunc RecoveryHandlerFuncContext
}
func evaluateOptions(opts []Option) *options {
optCopy := &options{}
*optCopy = *defaultOptions
for _, o := range opts {
o(optCopy)
}
return optCopy
}
type Option func(*options)
// WithRecoveryHandler customizes the function for recovering from a panic.
func WithRecoveryHandler(f RecoveryHandlerFunc) Option {
return func(o *options) {
o.recoveryHandlerFunc = RecoveryHandlerFuncContext(func(ctx context.Context, p interface{}) error {
return f(p)
})
}
}
// WithRecoveryHandlerContext customizes the function for recovering from a panic.
func WithRecoveryHandlerContext(f RecoveryHandlerFuncContext) Option {
return func(o *options) {
o.recoveryHandlerFunc = f
}
}
main.go
package main
import (
"crypto/tls"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/examples/data"
"google.golang.org/grpc/status"
"grpc-demo/helloworld/grpc_recovery"
"grpc-demo/helloworld/pb"
"log"
"net"
)
const (
port = ":50051"
)
var (
customFunc grpc_recovery.RecoveryHandlerFunc
)
func main() {
cert, err := tls.LoadX509KeyPair(
data.Path("/Users/guirong/go/src/grpc-demo/helloworld/server/server.crt"),
data.Path("/Users/guirong/go/src/grpc-demo/helloworld/server/server.key"))
if err != nil {
log.Fatalf("failed to load key pair: %s", err)
}
// Define customfunc to handle panic
customFunc = func(p interface{}) (err error) {
fmt.Printf("panic triggered: %v\n", p)
return status.Errorf(codes.Unknown, "panic triggered: %v", p)
}
// Shared options for the logger, with a custom gRPC code to log level function.
opts := []grpc_recovery.Option{
grpc_recovery.WithRecoveryHandler(customFunc),
}
s := grpc.NewServer(
grpc.Creds(credentials.NewServerTLSFromCert(&cert)),
grpc.ChainUnaryInterceptor(grpc_recovery.UnaryServerInterceptor(opts...), myEnsureValidToken),
grpc.ChainStreamInterceptor(grpc_recovery.StreamServerInterceptor(opts...), streamInterceptorAuth),
)
pb.RegisterGreeterServer(s, &GreeterServer{})
// 玩家连续进行了多次战斗请求,服务器将操作结果响应给玩家
pb.RegisterBattleServiceServer(s, &BattleServer{})
listen, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
log.Println("Serving gRPC on 0.0.0.0" + port)
if err := s.Serve(listen); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
⚠️需要注意的是, 异常处理的拦截器应该在整个chain的顶端, 这样才能避免异常传递到主函数导致服务崩溃.
我们需要手动在服务里添加一个painc, 然后对比有异常处理和没有的区别.
可以看到, 一旦触发异常, 那么服务器main函数也会被拉垮.
有异常处理, 只是输出了一个painc信息, main函数不受影响.
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。