You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 03:19:50