开启Prefork的Go-Fiber应用RabbitMQ连接共享空指针异常排查
我有一个基于Go-Fiber的应用,尝试向RabbitMQ发布消息,简化后的代码如下:
main.go
// main.go type custom_handler struct { RMQConn *rabbitmq.Connection } func newHandler(conn *rabbitmq.Connection) custom_handler { return custom_handler{ RMQConn: conn, } } func main() { var hndlr custom_handler if !fiber.IsChild() { messageq.ConnectMQ() hndlr = newHandler(messageq.RMQConn) log.Println(messageq.RMQConn) // not nil } engine := html.New("./static", ".html") app := fiber.New(fiber.Config{ Views: engine, Prefork: true, // ATTENTION!!! }) app.Use(cors.New()) app.Get("/", func(c *fiber.Ctx) error { return c.Render("index", fiber.Map{}) }) app.Post("/", hndlr.handleFileupload) log.Fatal(app.Listen(":8080")) } func (hndlr custom_handler) handleFileupload(c *fiber.Ctx) error { log.Println(hndlr.RMQConn) // nil RMQChan, err := hndlr.RMQConn.Channel() if err != nil { return err } defer RMQChan.Close() }
messageq包代码
// messageq.go var RMQConn *rabbitmq.Connection func ConnectMQ() { var err error amqpServerURL := os.Getenv("RABBITMQ_SERVER_URL") RMQConn, err = rabbitmq.Dial(amqpServerURL) if err != nil { panic(err) } }
运行应用时出现段错误,错误日志如下:
producer_1 | panic: runtime error: invalid memory address or nil pointer dereference producer_1 | [signal SIGSEGV: segmentation violation code=0x1 addr=0x14 pc=0x8ef6db] imgress-producer_1 | producer_1 | goroutine 7 [running]: producer_1 | github.com/rabbitmq/amqp091-go.(*Connection).allocateChannel(0x0) producer_1 | /go/pkg/mod/github.com/rabbitmq/amqp091-go@v1.5.0/connection.go:679 +0x5b producer_1 | github.com/rabbitmq/amqp091-go.(*Connection).openChannel(0x4f46c8?) producer_1 | /go/pkg/mod/github.com/rabbitmq/amqp091-go@v1.5.0/connection.go:709 +0x25 producer_1 | github.com/rabbitmq/amqp091-go.(*Connection).Channel(...) producer_1 | /go/pkg/mod/github.com/rabbitmq/amqp091-go@v1.5.0/connection.go:736 producer_1 | main.ValidateAndPublish({0xc0000120c8, 0x1, 0xb7aeff?}, 0x1?, {0xc0000e7f38, 0x8}, 0xffffffffffffffff?) producer_1 | /app/validate.go:14 +0x8a producer_1 | main.custom_handler.handleFileupload({0x0?, 0x0?}, 0xc00015f080?) producer_1 | /app/main.go:94 +0x4bb producer_1 | github.com/gofiber/fiber/v2.(*App).next(0xc000221b80, 0xc00009e000) producer_1 | /go/pkg/mod/github.com/gofiber/fiber/v2@v2.39.0/router.go:132 +0x1c2 producer_1 | github.com/gofiber/fiber/v2.(*Ctx).Next(0xc0001bcf18?) producer_1 | /go/pkg/mod/github.com/gofiber/fiber/v2@v2.39.0/ctx.go:945 +0x53 producer_1 | github.com/gofiber/fiber/v2/middleware/cors.New.func1(0xc00009e000) producer_1 | /go/pkg/mod/github.com/gofiber/fiber/v2@v2.39.0/middleware/cors/cors.go:141 +0x286 producer_1 | github.com/gofiber/fiber/v2.(*App).next(0xc000221b80, 0xc00009e000) producer_1 | /go/pkg/mod/github.com/gofiber/fiber/v2@v2.39.0/router.go:132 +0x1c2 producer_1 | github.com/gofiber/fiber/v2.(*App).handler(0xc000221b80, 0x485df7?) producer_1 | /go/pkg/mod/github.com/gofiber/fiber/v2@v2.39.0/router.go:159 +0x45 producer_1 | github.com/valyala/fasthttp.(*Server).serveConn(0xc0001478c0, {0xc8dd90?, 0xc000012070}) producer_1 | /go/pkg/mod/github.com/valyala/fasthttp@v1.40.0/server.go:2311 +0x1268 producer_1 | github.com/valyala/fasthttp.(*workerPool).workerFunc(0xc0000000a0, 0xc0000400a0) producer_1 | /go/pkg/mod/github.com/valyala/fasthttp@v1.40.0/workerpool.go:224 +0xa9 producer_1 | github.com/valyala/fasthttp.(*workerPool).getCh.func1() producer_1 | /go/pkg/mod/github.com/valyala/fasthttp@v1.40.0/workerpool.go:196 +0x38 producer_1 | created by github.com/valyala/fasthttp.(*workerPool).getCh producer_1 | /go/pkg/mod/github.com/valyala/fasthttp@v1.40.0/workerpool.go:195 +0x1b0
我推测是Prefork: true导致每个请求由独立goroutine处理,handler无法访问连接,但不确定具体忽略了什么,请问如何让RabbitMQ连接对handler可用?
你的核心问题是对Fiber的Prefork模式理解有误:Prefork是多进程模式,不是多goroutine模式。当开启Prefork: true时,Fiber会先启动一个父进程,然后fork出多个子进程来处理请求,每个子进程都是独立的操作系统进程,进程间的内存空间完全隔离,父进程中初始化的RabbitMQ连接无法被子进程共享。
你当前的代码只在父进程(!fiber.IsChild())中初始化了hndlr和RabbitMQ连接,但子进程启动时不会执行这段初始化逻辑,导致子进程里的hndlr.RMQConn是nil,调用Channel()时触发空指针引用错误。
有两种常见的解决方式,根据你的业务需求选择:
方案1:关闭Prefork模式
如果你的应用不需要利用多核CPU的极致性能,或者可以通过Fiber的默认goroutine池处理并发,可以直接关闭Prefork:
app := fiber.New(fiber.Config{ Views: engine, Prefork: false, // 关闭Prefork })
此时应用运行在单进程多goroutine模式下,RabbitMQ连接可以安全地在goroutine间共享(RabbitMQ的Connection本身是线程安全的,多个goroutine可以共用一个Connection创建Channel)。
方案2:在每个子进程中初始化RabbitMQ连接
如果必须保留Prefork模式,需要让每个子进程都建立自己的RabbitMQ连接,修改初始化逻辑:
func main() { // 去掉fiber.IsChild()判断,让所有进程(父+子)都初始化连接 messageq.ConnectMQ() hndlr := newHandler(messageq.RMQConn) log.Println(messageq.RMQConn) engine := html.New("./static", ".html") app := fiber.New(fiber.Config{ Views: engine, Prefork: true, }) // ... 后续路由注册逻辑不变 }
注意:每个子进程都会创建独立的RabbitMQ连接,这种方式会消耗更多的RabbitMQ连接资源,建议配合RabbitMQ的连接配置(如最大连接数)进行调整,同时确保进程退出时正确关闭连接。
另外,也可以优化messageq包,增加连接关闭的逻辑:
// messageq.go var RMQConn *rabbitmq.Connection func ConnectMQ() { var err error amqpServerURL := os.Getenv("RABBITMQ_SERVER_URL") RMQConn, err = rabbitmq.Dial(amqpServerURL) if err != nil { panic(err) } } func CloseMQ() { if RMQConn != nil { _ = RMQConn.Close() } }
然后在main函数中添加退出回调:
func main() { messageq.ConnectMQ() defer messageq.CloseMQ() // 进程退出时关闭连接 // ... 其他逻辑 }
内容的提问来源于stack exchange,提问作者Eziz Durdyyev

