Go语言中gRPC服务无响应,如何实现与REST服务同端口运行?
问题描述
我是Go语言新手,正在编写服务端程序:认证模块用gRPC实现,核心功能计划用REST实现。已经完成gRPC与REST的代码编写,但启动服务后只有REST正常工作,gRPC完全无响应。试过让两者运行在同一端口或不同端口,都没解决问题;尝试把gRPC整合到REST里,目前难度较大。
提供的代码实现
grpc.go 应用
type App struct { log *slog.Logger gRPCServer *grpc.Server port int } // New creates new gRPC server app. func New( log *slog.Logger, authService authgrpc.Auth, port int, ) *App { recoveryOpts := []recovery.Option{ recovery.WithRecoveryHandler(func(p interface{}) (err error) { log.Error("Recovered from panic", slog.Any("panic", p)) return status.Errorf(codes.Internal, "internal error") }), } gRPCServer := grpc.NewServer(grpc.ChainUnaryInterceptor( recovery.UnaryServerInterceptor(recoveryOpts...), logging.UnaryServerInterceptor(InterceptorLogger(log), loggingOpts...), )) authgrpc.Register(gRPCServer, authService) return &App{ log: log, gRPCServer: gRPCServer, port: port, } } // MustRun runs gRPC server and panics if any error occurs. func (a *App) MustRun() { if err := a.Run(); err != nil { panic(err) } } // Run runs gRPC server. func (a *App) Run() error { const op = "grpcapp.Run" l, err := net.Listen("tcp", fmt.Sprintf(":%d", a.port)) if err != nil { return fmt.Errorf("%s: %w", op, err) } a.log.Info("grpc server started", slog.String("addr", l.Addr().String())) if err := a.gRPCServer.Serve(l); err != nil { return fmt.Errorf("%s: %w", op, err) } return nil } // Stop stops gRPC server. func (a *App) Stop() { const op = "grpcapp.Stop" a.log.With(slog.String("op", op)). Info("stopping gRPC server", slog.Int("port", a.port)) a.gRPCServer.GracefulStop() }
rest.go 应用
type App struct { log *slog.Logger httpServer *http.Server port int coreService *core.Core } func New( log *slog.Logger, coreService *core.Core, port int, ) *App { return &App{ log: log, port: port, coreService: coreService, } } // MustRun runs the HTTP server and panics if any error occurs. func (a *App) MustRun() { if err := a.Run(); err != nil { panic(err) } } // Run runs the HTTP server. func (a *App) Run() error { const op = "restapp.Run" router := mux.NewRouter() router.HandleFunc("/author", a.coreService.GetAuthorHandler).Methods("GET") a.httpServer = &http.Server{ Addr: fmt.Sprintf(":%d", a.port), Handler: router, } if err := a.httpServer.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) { return fmt.Errorf("%s: %w", op, err) } return nil }
rest 与 grpc 构建器
type App struct { log *slog.Logger GRPCServer *grpcapp.App RestServer *restapp.App port int } func NewGrpc( log *slog.Logger, port int, storagePath string, tokenTTL time.Duration, ) *App { storage, err := sqlite.New(storagePath) if err != nil { panic(err) } authService := auth.New(log, storage, storage, storage, tokenTTL) grpcApp := grpcapp.New(log, authService, port) return &App{ log: log, GRPCServer: grpcApp, port: port, } } func NewRest( log *slog.Logger, port int, storagePath string, tokenTTL time.Duration, ) *App { storage, err := sqlite.New(storagePath) if err != nil { panic(err) } coreService := core.New(log, storage, tokenTTL) restApp := restapp.New(log, coreService, port) return &App{ log: log, RestServer: restApp, port: port, } }
rest 处理器
type AuthorProvider interface { Author(ctx context.Context) ([]models.Author, error) } type Core struct { log *slog.Logger authorProvider AuthorProvider tokenTTL time.Duration } func New( log *slog.Logger, authorProvider AuthorProvider, tokenTTL time.Duration, ) *Core { return &Core{ log: log, authorProvider: authorProvider, tokenTTL: tokenTTL, } } func (c *Core) GetAuthorHandler(w http.ResponseWriter, r *http.Request) { const op = "core.GetAuthorHandler" // Retrieve author data using AuthorProvider authors, err := c.authorProvider.Author(r.Context()) if err != nil { http.Error(w, fmt.Sprintf("%s: %v", op, err), http.StatusInternalServerError) return } // Encode authors data as JSON response w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(authors); err != nil { http.Error(w, fmt.Sprintf("%s: %v", op, err), http.StatusInternalServerError) return } }
问题排查与解决方案
一、不同端口运行时gRPC无响应的修复
你的代码中,gRPC和REST的Run方法都是阻塞调用(grpc.Server.Serve和http.Server.ListenAndServe会一直占用当前goroutine)。如果启动逻辑是串行调用两者的MustRun,比如先启动REST再启动gRPC,那么REST的阻塞会导致gRPC服务根本没机会启动,自然无响应。
修复方法:用goroutine并行启动服务
修改main函数的启动逻辑,将其中一个服务的启动放到goroutine中:
func main() { log := slog.Default() // 启动gRPC服务(放入goroutine,避免阻塞主线程) grpcApp := builder.NewGrpc(log, 50051, "./storage.db", time.Hour) go func() { grpcApp.GRPCServer.MustRun() }() // 启动REST服务(主线程阻塞,保持程序运行) restApp := builder.NewRest(log, 8080, "./storage.db", time.Hour) restApp.RestServer.MustRun() }
注意:必须给两个服务分配不同端口(比如gRPC用50051,REST用8080),避免端口冲突。
二、同一端口运行gRPC与REST的实现方案
利用gRPC基于HTTP/2的特性,通过请求头的Content-Type区分请求类型,让HTTP Server同时处理两种请求:
1. 给gRPC的App添加HTTP handler方法
在grpc.go的App结构体中新增方法,让gRPC服务可以处理HTTP请求:
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { a.gRPCServer.ServeHTTP(w, r) }
2. 修改REST的Run方法,添加请求路由逻辑
修正原REST代码中的错误(原错误判断会导致正常关闭的错误被当成异常返回),同时添加gRPC请求的转发逻辑:
import "strings" // 修改rest.go的Run方法,新增grpcHandler参数 func (a *App) Run(grpcHandler http.Handler) error { const op = "restapp.Run" router := mux.NewRouter() router.HandleFunc("/author", a.coreService.GetAuthorHandler).Methods("GET") // 根handler:区分gRPC和HTTP请求 rootHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // gRPC请求的Content-Type以application/grpc开头 if strings.HasPrefix(r.Header.Get("Content-Type"), "application/grpc") { grpcHandler.ServeHTTP(w, r) return } // 其余请求交给REST路由处理 router.ServeHTTP(w, r) }) a.httpServer = &http.Server{ Addr: fmt.Sprintf(":%d", a.port), Handler: rootHandler, } // 修正错误判断逻辑:只有非关闭错误才返回异常 if err := a.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { return fmt.Errorf("%s: %w", op, err) } return nil }
3. 修改main函数,实现单端口启动
同时初始化两个服务,让它们共用一个端口,并且处理优雅关闭:
import ( "context" "os" "os/signal" "syscall" ) func main() { log := slog.Default() port := 8080 storagePath := "./storage.db" tokenTTL := time.Hour // 初始化数据库(只创建一次,避免重复连接) storage, err := sqlite.New(storagePath) if err != nil { panic(err) } // 初始化gRPC服务 authService := auth.New(log, storage, storage, storage, tokenTTL) grpcApp := grpcapp.New(log, authService, port) // 初始化REST服务 coreService := core.New(log, storage, tokenTTL) restApp := restapp.New(log, coreService, port) // 启动REST服务(传入gRPC handler) go func() { if err := restApp.Run(grpcApp); err != nil { log.Error("rest server failed", slog.String("err", err.Error())) } }() // 监听退出信号,实现优雅关闭 sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) <-sigChan // 关闭服务 restApp.httpServer.Shutdown(context.Background()) grpcApp.Stop() }
额外检查点
- 确认gRPC客户端连接的端口与服务启动端口一致
- 检查
authgrpc.Register(gRPCServer, authService)调用是否正确,确保proto生成的注册逻辑无误 - 查看服务启动日志,确认gRPC服务的启动日志是否打印,未打印则说明服务未启动
内容的提问来源于stack exchange,提问作者Denis Popkov
相关产品推荐
相关产品推荐

