RabbitMQ示例

用户创建

在 Linux 虚拟机中,用 sudo 重新创建并授权 test 用户

复制代码
# 1. 以root权限创建test用户(密码123456)
sudo rabbitmqctl add_user test 123456

# 2. 授予test用户管理员权限(关键:否则无法远程访问)
sudo rabbitmqctl set_user_tags test administrator

# 3. 授予test用户所有虚拟主机的全部权限(关键:否则无法操作队列/交换机)
sudo rabbitmqctl set_permissions -p / test ".*" ".*" ".*"

# 4. 验证用户是否创建成功(可选,确认结果)
sudo rabbitmqctl list_users
  • 执行成功会提示:Adding user "test"/Setting tags for user "test" to [administrator]
  • list_users输出中能看到test,标签是[administrator],说明创建授权成功。

重启 RabbitMQ 服务,使用户配置生效

复制代码
sudo systemctl restart rabbitmq-server

# 验证服务是否启动(可选)
sudo systemctl status rabbitmq-server

环境准备

安装AMQP 0-9-1客户端库 go get github.com/rabbitmq/amqp091-go

"hello,world!"

生产者代码(send.go)

生产者的作用是:连接 RabbitMQ → 创建通道 → 声明队列 → 发送一条 "Hello World!" 消息 → 关闭连接 / 通道。

复制代码
package main

import (
	"context"
	amqp "github.com/rabbitmq/amqp091-go"
	"log"
	"time"
)

func failOnError(err error, msg string) {
	if err != nil {
		log.Panicf("%s: %s", msg, err)
	}
}

func main() {
	// 1. 连接RabbitMQ服务器
	conn, err := amqp.Dial("amqp://test:123456@192.168.100.128:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

	// 2. 创建通道(Channel)
	ch, err := conn.Channel()
	failOnError(err, "Failed to open a channel")
	defer ch.Close()

	// 3. 声明队列(QueueDeclare)
	// 声明队列是"幂等的"------队列不存在就创建,存在就复用(不会重复创建)
	q, err := ch.QueueDeclare(
		"hello", // 队列名:hello
		false,   // durable:是否持久化(重启RabbitMQ后队列是否还在)
		false,   // delete when unused:不再使用时是否自动删除
		false,   // exclusive:是否排他(仅当前连接可用,其他连接不能访问)
		false,   // no-wait:是否非阻塞(不等待服务器确认)
		nil,     // arguments:队列的额外参数(入门用不到,传nil)
	)
	failOnError(err, "Failed to declare a queue")

	// 4. 设置上下文超时(5秒)
	// 确保发布消息的操作不会无限等待,超时会终止
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel() // 函数退出前取消上下文,释放资源

	// 5. 准备要发送的消息内容
	body := "Hello World!"

	// 6. 发布消息到队列
	err = ch.PublishWithContext(
		ctx,    // 超时上下文
		"",     // exchange:交换机(入门用默认交换机,传空字符串)
		q.Name, // routing key:路由键(默认交换机下等于队列名)
		false,  // mandatory:消息无法路由时是否返回给生产者
		false,  // immediate:消息无消费者时是否立即返回
		// 消息体:指定内容类型和字节数组形式的消息内容
		amqp.Publishing{
			ContentType: "text/plain", // 消息内容类型:纯文本
			Body:        []byte(body), // 消息内容(字节数组,支持任意格式)
		},
	)
	failOnError(err, "Failed to publish a message")
	log.Printf(" [x] Sent %s\n", body) // 打印发送成功的日志
}

消费者代码(receive.go)

消费者的作用是:连接 RabbitMQ → 创建通道 → 声明队列(确保队列存在) → 监听队列 → 接收并打印消息 → 持续运行(直到按 Ctrl+C 停止)。

复制代码
package main

import (
  "log"

  amqp "github.com/rabbitmq/amqp091-go"
)

// 和生产者一样的错误处理辅助函数
func failOnError(err error, msg string) {
  if err != nil {
    log.Panicf("%s: %s", msg, err)
  }
}

func main() {
  // 1. 连接RabbitMQ服务器(和生产者逻辑一致)
  conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
  failOnError(err, "Failed to connect to RabbitMQ")
  defer conn.Close()

  // 2. 创建通道(和生产者逻辑一致)
  ch, err := conn.Channel()
  failOnError(err, "Failed to open a channel")
  defer ch.Close()

  // 3. 声明队列(必须和生产者的队列名一致)
  // 为什么重复声明?因为消费者可能比生产者先启动,要确保队列存在
  q, err := ch.QueueDeclare(
    "hello", // 队列名:和生产者一致
    false,   // 以下参数和生产者保持一致
    false,
    false,
    false,
    nil,
  )
  failOnError(err, "Failed to declare a queue")

  // 4. 注册消费者,监听队列消息
  // Consume方法会返回一个通道(msgs),RabbitMQ会把消息推送到这个通道
  msgs, err := ch.Consume(
    q.Name, // 要消费的队列名
    "",     // consumer:消费者标签(标识消费者,空字符串自动生成)
    true,   // auto-ack:自动确认(收到消息后立即告诉RabbitMQ已处理,入门推荐)
    false,  // exclusive:是否排他
    false,  // no-local:不接收当前连接发布的消息
    false,  // no-wait:非阻塞
    nil,    // args:额外参数
  )
  failOnError(err, "Failed to register a consumer")

  // 5. 创建一个阻塞通道,让程序持续运行
  // 避免main函数执行完退出,消费者需要一直监听消息
  var forever chan struct{}

  // 6. 启动goroutine(Go的轻量级线程)处理消息
  // 异步读取msgs通道里的消息,不阻塞主线程
  go func() {
    // 循环读取msgs通道,有消息就处理
    for d := range msgs {
      log.Printf("Received a message: %s", d.Body) // 打印收到的消息
    }
  }()

  // 7. 打印提示信息,然后阻塞程序
  log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
  <-forever // 读取forever通道(永远读不到数据),程序会一直阻塞在这里
}

Work Queues

生产者代码(send.go)

这个程序的作用是从命令行接收任务内容,将其封装成消息发送到task_queue队列,核心是消息持久化保证任务不丢。

复制代码
package main

import (
	"context"
	"log"
	"os"
	"strings"
	"time"

	amqp "github.com/rabbitmq/amqp091-go"
)

// 错误处理工具函数:出错时打印信息并终止程序
func failOnError(err error, msg string) {
	if err != nil {
		log.Panicf("%s: %s", msg, err) // Panic会终止程序,适合演示;生产环境可改为返回错误
	}
}

func main() {
	// 1. 连接RabbitMQ服务器
	conn, err := amqp.Dial("amqp://test:123456@192.168.100.128:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close() // 程序退出前关闭连接

	// 2. 创建信道(Channel):所有操作都通过信道完成,而非直接用连接
	ch, err := conn.Channel()
	failOnError(err, "Failed to open a channel")
	defer ch.Close() // 退出前关闭信道

	// 3. 声明队列(关键:durable=true 让队列持久化)
	q, err := ch.QueueDeclare(
		"task_queue", // 队列名:改为task_queue,避免和旧的非持久化队列冲突
		true,         // durable:持久化(RabbitMQ重启后队列不丢失)
		false,        // autoDelete:队列未被使用时是否自动删除
		false,        // exclusive:是否排他(仅当前连接可用)
		false,        // noWait:是否非阻塞(不等待服务器确认)
		nil,          // arguments:额外参数
	)
	failOnError(err, "Failed to declare a queue")

	// 4. 创建超时上下文:保证发布消息有超时限制(5秒)
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel() // 函数结束时取消上下文

	// 5. 从命令行获取任务内容(无参数时默认"hello")
	body := bodyFrom(os.Args)

	// 6. 发布消息到队列(关键:DeliveryMode=Persistent 让消息持久化)
	err = ch.PublishWithContext(ctx,
		"",     // exchange:默认交换机(空字符串),直接路由到指定队列
		q.Name, // routing key:队列名(默认交换机通过这个匹配队列)
		false,  // mandatory:消息无法路由时是否返回给生产者
		false,  // immediate:已废弃,忽略
		amqp.Publishing{
			DeliveryMode: amqp.Persistent, // 消息持久化(写入磁盘)
			ContentType:  "text/plain",    // 消息内容类型
			Body:         []byte(body),    // 消息体
		})
	failOnError(err, "Failed to publish a message")
	log.Printf(" [x] Sent %s", body)
}

// bodyFrom:处理命令行参数,提取要发送的消息内容
func bodyFrom(args []string) string {
	var s string
	if (len(args) < 2) || os.Args[1] == "" {
		s = "hello" // 无参数时默认发送"hello"
	} else {
		s = strings.Join(args[1:], " ") // 拼接多个参数为一个字符串
	}
	return s
}
  • 队列持久化durable=true,必须保证生产者和消费者声明的队列参数一致,否则 RabbitMQ 会报错;
  • 消息持久化DeliveryMode: amqp.Persistent,RabbitMQ 会把消息写入磁盘,但注意:这不是 100% 不丢(有极短的窗口可能只存在于缓存);
  • 默认交换机 :空字符串的交换机是 RabbitMQ 的默认交换机,它会把消息路由到和routing key同名的队列。

消费者代码(receive.go)

这个程序的作用是从task_queue队列消费消息,模拟耗时处理(每个.代表 1 秒),核心是手动消息确认公平分发

复制代码
package main

import (
	"bytes"
	"log"
	"time"

	amqp "github.com/rabbitmq/amqp091-go"
)

func failOnError(err error, msg string) {
	if err != nil {
		log.Panicf("%s: %s", msg, err)
	}
}

func main() {
	// 1. 连接RabbitMQ(和生产者逻辑一致)
	conn, err := amqp.Dial("amqp://test:123456@192.168.100.128:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

	// 2. 创建信道
	ch, err := conn.Channel()
	failOnError(err, "Failed to open a channel")
	defer ch.Close()

	// 3. 声明队列(必须和生产者一致,包括durable=true)
	q, err := ch.QueueDeclare(
		"task_queue", // 队列名和生产者保持一致
		true,         // 持久化
		false,
		false,
		false,
		nil,
	)
	failOnError(err, "Failed to declare a queue")

	// 4. 设置QoS(公平分发关键:prefetch=1)
	err = ch.Qos(
		1,     // prefetch count:每个消费者最多同时处理1个未确认的消息
		0,     // prefetch size:每个消费者最多接收的字节数(0=无限制)
		false, // global:是否应用到整个连接(false=仅当前信道)
	)
	failOnError(err, "Failed to set QoS")

	// 5. 消费队列消息(关键:auto-ack=false 手动确认)
	msgs, err := ch.Consume(
		q.Name, // 要消费的队列名
		"",     // consumer tag:消费者标识(空字符串由RabbitMQ自动生成)
		false,  // auto-ack:是否自动确认(false=手动确认)
		false,  // exclusive:是否排他消费
		false,  // no-local:是否接收当前连接发布的消息(已废弃)
		false,  // no-wait:是否非阻塞
		nil,    // args:额外参数
	)
	failOnError(err, "Failed to register a consumer")

	// 6. 定义一个永不关闭的通道,让程序一直运行
	var forever chan struct{}

	// 7. 启动goroutine处理消息(异步消费,不阻塞主线程)
	go func() {
		for d := range msgs { // 循环读取消息通道中的消息
			// 打印收到的消息
			log.Printf("Received a message: %s", d.Body)
			// 统计消息中的点数量,模拟耗时(每个点=1秒)
			dotCount := bytes.Count(d.Body, []byte("."))
			t := time.Duration(dotCount)
			time.Sleep(t * time.Second) // 模拟耗时处理
			log.Printf("Done")          // 处理完成
			d.Ack(false)                // 手动确认:告诉RabbitMQ消息已处理完成,可以删除
		}
	}()

	// 8. 提示信息,阻塞程序退出
	log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
	<-forever // 阻塞主线程,直到收到关闭信号
}

消费者核心要点

(1)手动消息确认(auto-ack=false)

  • 为什么需要手动确认?如果开启auto-ack=true(自动确认),RabbitMQ 一旦把消息发给消费者,就会立刻删除消息。如果此时消费者崩溃(比如按 Ctrl+C),这个消息就永久丢失了。手动确认(d.Ack(false))的逻辑是:消费者处理完消息后,主动告诉 RabbitMQ"我处理完了,你可以删了"。如果消费者崩溃,RabbitMQ 会把未确认的消息重新发给其他消费者。
  • 注意事项:Ack必须在接收消息的同一个信道调用,否则会报错;不要忘记写Ack,否则 RabbitMQ 会堆积大量未确认消息,耗尽内存。

(2)公平分发(QoS prefetch=1)

  • 默认的轮询分发问题:RabbitMQ 会把消息 "平均" 分给消费者,不管消费者是否繁忙。比如:
    • 消费者 1 拿到 "重任务(5 个点,5 秒)",消费者 2 拿到 "轻任务(1 个点,1 秒)";
    • 消费者 2 处理完轻任务后,RabbitMQ 又给它发下一个轻任务,而消费者 1 还在处理重任务;
    • 最终导致 "一个忙死、一个闲死" 的不均衡。
  • prefetch=1的作用:告诉 RabbitMQ "不要给一个消费者发超过 1 个未确认的消息",只有当消费者确认了上一个消息,才会给它发下一个。这样 RabbitMQ 会把消息发给空闲的消费者,实现公平分发。

运行演示

步骤 1:启动多个消费者

打开 2 个终端,分别运行:

终端1(消费者1)

go run receive.go

输出:[*] Waiting for messages. To exit press CTRL+C

终端2(消费者2)

go run receive.go

输出:[*] Waiting for messages. To exit press CTRL+C

步骤 2:发送多个任务

go run .\send.go "Task 1 (1s) ."

go run .\send.go "Task 2 (2s) .."

go run .\send.go "Task 3 (3s) ..."

go run .\send.go "Task 4 (4s) ...."

go run .\send.go "Task 5 (5s) ....."

步骤 3:观察消费结果

  • 消费者 1 和消费者 2 会公平地分担任务:不会出现一个处理重任务、一个处理轻任务的情况;
  • 如果此时杀掉其中一个消费者(比如按 Ctrl+C 终止消费者 1),它未处理完的任务会自动重新发给消费者 2;
  • 即使重启 RabbitMQ,队列和未处理的消息也不会丢失(因为开启了持久化)。

Publish/Subscribe

发布 / 订阅模式的核心思想

首先要明确这个模式和之前 "工作队列" 的区别:

  • 工作队列:一个任务只分给一个消费者(比如多个工人抢一个任务);
  • 发布 / 订阅:一个消息会发给所有消费者(比如广播,所有接收端都能收到同一份消息)。

教程里用 "日志系统" 举例:

  • 生产者:发送日志消息;
  • 多个消费者:一个把日志存到文件,一个把日志打印到控制台,两个消费者都能收到所有日志

核心概念

1. 交换机(Exchange):消息的 "路由器"

之前的教程里,生产者直接把消息发到队列,但 RabbitMQ 的核心设计是:生产者永远不直接发消息到队列,而是发到「交换机」,由交换机决定把消息转发到哪些队列。

交换机的核心属性:

  • 类型 :决定消息转发规则,教程中用fanout(扇出 / 广播),这是最简单的类型 ------ 把收到的所有消息广播给所有绑定的队列;
  • 声明交换机:必须先声明交换机才能使用(发布到不存在的交换机会报错)。

声明 fanout 交换机的代码解析:

复制代码
err = ch.ExchangeDeclare(
  "logs",   // 交换机名称(自定义,比如叫logs)
  "fanout", // 类型:扇出,广播给所有绑定的队列
  true,     // durable:是否持久化(重启RabbitMQ后交换机还在)
  false,    // auto-deleted:是否自动删除(没有队列绑定时自动删)
  false,    // internal:是否内部使用(仅RabbitMQ自身使用,用户不能发消息)
  false,    // no-wait:是否非阻塞(不等待服务器确认)
  nil,      // arguments:额外参数(默认nil)
)

2. 临时队列(Temporary Queues):用完即删的队列

日志系统不需要固定名称的队列,原因:

  • 每个消费者需要独立的队列(否则会变成工作队列模式);
  • 消费者断开连接后,队列应该自动删除(不需要保留历史日志)。

声明临时队列的代码解析:

复制代码
q, err := ch.QueueDeclare(
  "",    // 队列名称:空字符串 → 让RabbitMQ自动生成随机名称(比如amq.gen-xxxx)
  false, // durable:不持久化(队列不会存到磁盘)
  false, // delete when unused:未使用时删除(无消费者时)
  true,  // exclusive:排他(仅当前连接可用,连接断开则队列删除)
  false, // no-wait:非阻塞
  nil,   // 额外参数
)

这个队列的特点:

  • 名称随机(每个消费者启动都会生成新队列);
  • 消费者断开后自动删除,完全 "临时"。

3. 绑定(Bindings):交换机→队列的 "桥梁"

声明了交换机和队列后,需要告诉交换机:"把消息转发到这个队列"------ 这个关联关系就是「绑定」。

绑定队列到交换机的代码解析:

复制代码
err = ch.QueueBind(
  q.Name, // 队列名称(临时队列的随机名称)
  "",     // routing key:路由键(fanout类型忽略这个值)
  "logs", // 交换机名称(和生产者的交换机同名)
  false,
  nil,
)

绑定后,logs交换机就会把所有收到的消息转发到这个临时队列。

生产者代码(emit_log.go)

核心功能:连接 RabbitMQ → 声明 fanout 交换机 → 发送消息到交换机。

复制代码
package main

import (
	"context"
	"log"
	"os"
	"strings"
	"time"

	amqp "github.com/rabbitmq/amqp091-go"
)

// 错误处理辅助函数:出错时打印信息并终止程序
func failOnError(err error, msg string) {
	if err != nil {
		log.Panicf("%s: %s", msg, err)
	}
}

func main() {
	// 1. 连接RabbitMQ服务器
	conn, err := amqp.Dial("amqp://test:123456@192.168.100.128:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close() // 程序退出前关闭连接

	// 2. 创建信道(所有操作都通过信道完成)
	ch, err := conn.Channel()
	failOnError(err, "Failed to open a channel")
	defer ch.Close() // 程序退出前关闭信道

	// 3. 声明fanout类型的交换机(名称:logs)
	err = ch.ExchangeDeclare(
		"logs",   // 交换机名称
		"fanout", // 类型:广播
		true,     // 持久化
		false,    // 不自动删除
		false,    // 非内部使用
		false,    // 非阻塞
		nil,      // 无额外参数
	)
	failOnError(err, "Failed to declare an exchange")

	// 4. 准备消息内容:从命令行参数获取,默认"hello"
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel() // 超时后取消上下文
	body := bodyFrom(os.Args)

	// 5. 发布消息到logs交换机
	err = ch.PublishWithContext(ctx,
		"logs", // 目标交换机名称(必须和声明的一致)
		"",     // 路由键(fanout类型忽略,填空即可)
		false,  // mandatory:消息是否必须被路由到队列(fanout下无用)
		false,  // immediate:是否立即投递(已废弃)
		amqp.Publishing{ // 消息体
			ContentType: "text/plain", // 消息类型:纯文本
			Body:        []byte(body), // 消息内容
		})
	failOnError(err, "Failed to publish a message")

	log.Printf(" [x] Sent %s", body)
}

// 从命令行参数提取消息体:如果没有参数,返回"hello";有参数则拼接
func bodyFrom(args []string) string {
	var s string
	if (len(args) < 2) || os.Args[1] == "" {
		s = "hello"
	} else {
		s = strings.Join(args[1:], " ")
	}
	return s
}

关键注意点:

  • 必须先声明交换机,再发布消息(否则报错);
  • 发布到logs交换机时,路由键为空(fanout 类型不看路由键);
  • 如果此时没有消费者绑定队列,消息会被丢弃(符合日志的需求:没接收端就不用存)。

消费者代码(receive_logs.go)

核心功能:连接 RabbitMQ → 声明同名交换机 → 创建临时队列 → 绑定队列到交换机 → 消费消息。

复制代码
package main

import (
	"log"

	amqp "github.com/rabbitmq/amqp091-go"
)

func failOnError(err error, msg string) {
	if err != nil {
		log.Panicf("%s: %s", msg, err)
	}
}

func main() {
	// 1. 连接RabbitMQ
	conn, err := amqp.Dial("amqp://test:123456@192.168.100.128:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

	// 2. 创建信道
	ch, err := conn.Channel()
	failOnError(err, "Failed to open a channel")
	defer ch.Close()

	// 3. 声明和生产者同名的交换机(消费者可能先启动,必须确保交换机存在)
	err = ch.ExchangeDeclare(
		"logs",   // 名称和生产者一致
		"fanout", // 类型一致
		true,     // 持久化
		false,    // 不自动删除
		false,    // 非内部
		false,    // 非阻塞
		nil,
	)
	failOnError(err, "Failed to declare an exchange")

	// 4. 创建临时队列(名称随机,断开即删)
	q, err := ch.QueueDeclare(
		"",    // 空名称 → 自动生成
		false, // 不持久化
		false, // 未使用时删除
		true,  // 排他(连接断开删队列)
		false, // 非阻塞
		nil,
	)
	failOnError(err, "Failed to declare a queue")

	// 5. 绑定临时队列到logs交换机
	err = ch.QueueBind(
		q.Name, // 临时队列的名称
		"",     // 路由键(fanout忽略)
		"logs", // 交换机名称
		false,
		nil,
	)
	failOnError(err, "Failed to bind a queue")

	// 6. 注册消费者:监听临时队列的消息
	msgs, err := ch.Consume(
		q.Name, // 要消费的队列(临时队列)
		"",     // 消费者标签(空即可)
		true,   // auto-ack:自动确认(收到消息后告诉RabbitMQ已处理)
		false,  // exclusive:非排他
		false,  // no-local:不接收自己发的消息
		false,  // no-wait:非阻塞
		nil,    // 额外参数
	)
	failOnError(err, "Failed to register a consumer")

	// 7. 阻塞等待消息:用无缓冲通道forever让程序不退出
	var forever chan struct{}

	// 启动协程处理消息(避免主线程阻塞)
	go func() {
		for d := range msgs { // 循环读取消息通道
			log.Printf(" [x] %s", d.Body) // 打印消息内容
		}
	}()

	log.Printf(" [*] Waiting for logs. To exit press CTRL+C")
	<-forever // 阻塞主线程,直到手动终止
}

代码运行

运行消费者

打开两个终端,分别运行:

复制代码
# 终端1:把日志存到文件
go run receive_logs.go *> logs_from_rabbit.log

# 终端2:把日志打印到屏幕
go run receive_logs.go

运行生产者发送消息

打开第三个终端,发送日志消息:

复制代码
# 发送默认消息"hello"
go run emit_log.go

# 发送自定义消息
go run emit_log.go "this is a log message"
go run emit_log.go "error: database connection failed"

验证结果

  • 终端 2(屏幕)会看到所有发送的消息;
  • 查看logs_from_rabbit.log文件,也能看到相同的消息(两个消费者都收到了广播)。

验证绑定关系

rabbitmqctl查看交换机和队列的绑定:

复制代码
# 列出所有绑定
sudo rabbitmqctl list_bindings

输出会看到类似内容:

复制代码
logs    exchange        amq.gen-JzTY20BRgKO-HjmUJj0wLg  queue           []
logs    exchange        amq.gen-vso0PVvyiRIL2WoV3i48Yg  queue           []

说明logs交换机绑定了两个临时队列(对应两个消费者),符合预期。

Routing

Routing 模式的核心概念

1. 为什么需要 Routing 模式?

上一个教程的 Fanout 交换机 是「广播模式」------ 把所有消息无条件发给绑定的所有队列,无法过滤消息。而实际场景中(比如日志系统),我们需要按「消息级别」(info/warning/error)过滤:比如只把 error 日志写入文件,info/warning 只在控制台打印。

2. 核心组件与规则

  • Direct 交换机 :Routing 模式的核心,它的路由规则是「精确匹配」------ 消息的 Routing Key(发送时指定)必须和队列绑定的 Binding Key 完全一致,消息才会被路由到该队列。
  • Routing Key:生产者发送消息时,指定的「消息标识」(比如日志级别 info/warning/error)。
  • Binding Key:队列绑定到交换机时,指定的「匹配标识」,用来匹配生产者的 Routing Key。
  • 多绑定规则:多个队列可以绑定同一个 Binding Key;同一个队列也可以绑定多个 Binding Key(比如消费者想同时接收 warning 和 error 消息)。

3. Direct 交换机的路由逻辑

场景 路由结果
消息 Routing Key = orange,队列 Binding Key = orange 消息路由到该队列
消息 Routing Key = black,队列 1 Binding Key = black、队列 2 Binding Key = black 消息广播到两个队列(类似 Fanout)
消息 Routing Key = test,无队列绑定该 Key 消息被丢弃

生产者代码(emit_log_direct.go)

生产者的作用是:发送不同级别的日志消息到 Direct 交换机,并指定对应的 Routing Key(日志级别)。

复制代码
package main

import (
	"context"
	"log"
	"os"
	"strings"
	"time"

	amqp "github.com/rabbitmq/amqp091-go"
)

// 错误处理工具函数:出错时打印信息并终止程序
func failOnError(err error, msg string) {
	if err != nil {
		log.Panicf("%s: %s", msg, err)
	}
}

func main() {
	// 1. 连接 RabbitMQ 服务器
	conn, err := amqp.Dial("amqp://test:123456@192.168.100.128:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close() // 程序退出前关闭连接

	// 2. 创建信道(Channel):所有操作都通过信道完成
	ch, err := conn.Channel()
	failOnError(err, "Failed to open a channel")
	defer ch.Close() // 程序退出前关闭信道

	// 3. 声明 Direct 交换机
	// 关键:exchange 类型是 "direct",名称是 "logs_direct"
	err = ch.ExchangeDeclare(
		"logs_direct", // 交换机名称
		"direct",      // 交换机类型(核心:Direct)
		true,          // durable:持久化(重启 RabbitMQ 后交换机仍存在)
		false,         // auto-deleted:不自动删除
		false,         // internal:是否为内部交换机(仅用于 RabbitMQ 内部)
		false,         // no-wait:不等待服务器响应
		nil,           // arguments:额外参数
	)
	failOnError(err, "Failed to declare an exchange")

	// 4. 准备消息内容和路由键(日志级别)
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel() // 超时后取消上下文

	body := bodyFrom(os.Args)         // 获取消息体(命令行参数的第3个及以后)
	severity := severityFrom(os.Args) // 获取路由键(日志级别,命令行参数的第2个)

	// 5. 发布消息到 Direct 交换机
	err = ch.PublishWithContext(ctx,
		"logs_direct", // 目标交换机名称
		severity,      // 路由键(核心:指定日志级别,如 error/warning/info)
		false,         // mandatory:消息无法路由时是否返回给生产者
		false,         // immediate:已废弃,忽略
		amqp.Publishing{
			ContentType: "text/plain", // 消息内容类型
			Body:        []byte(body), // 消息体
		})
	failOnError(err, "Failed to publish a message")

	log.Printf(" [x] Sent %s:%s", severity, body)
}

// 从命令行参数提取消息体:如果没有传,默认是 "hello"
func bodyFrom(args []string) string {
	var s string
	if (len(args) < 3) || args[2] == "" {
		s = "hello"
	} else {
		s = strings.Join(args[2:], " ")
	}
	return s
}

// 从命令行参数提取路由键(日志级别):如果没有传,默认是 "info"
func severityFrom(args []string) string {
	var s string
	if (len(args) < 2) || args[1] == "" {
		s = "info"
	} else {
		s = args[1]
	}
	return s
}

核心代码解释

  • Direct 交换机声明ch.ExchangeDeclare 的第二个参数是 "direct",这是 Routing 模式的核心,区别于 Fanout 模式。
  • 路由键指定severityFrom(os.Args) 从命令行参数提取日志级别(如 error),作为消息的 Routing Key
  • 消息发布ch.PublishWithContext 的第二个参数是路由键,消息会被发送到 logs_direct 交换机,由交换机根据路由键路由到匹配的队列。

消费者代码(receive_logs_direct.go)

消费者的作用是:创建临时队列,绑定指定的 Binding Key(如 warning/error)到 Direct 交换机,只接收匹配的消息。

复制代码
package main

import (
	"log"
	"os"

	amqp "github.com/rabbitmq/amqp091-go"
)

func failOnError(err error, msg string) {
	if err != nil {
		log.Panicf("%s: %s", msg, err)
	}
}

func main() {
	// 1. 连接 RabbitMQ 并创建信道(和生产者逻辑一致)
	conn, err := amqp.Dial("amqp://test:123456@192.168.100.128:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

	ch, err := conn.Channel()
	failOnError(err, "Failed to open a channel")
	defer ch.Close()

	// 2. 声明和生产者相同的 Direct 交换机
	// 注意:消费者也声明交换机,是为了确保交换机存在(避免生产者还没创建的情况)
	err = ch.ExchangeDeclare(
		"logs_direct", // 必须和生产者的交换机名称一致
		"direct",      // 类型一致
		true,          // 持久化
		false,
		false,
		false,
		nil,
	)
	failOnError(err, "Failed to declare an exchange")

	// 3. 声明临时队列(核心:排他性、无名称)
	// 特点:程序退出后队列自动删除,适合日志这种临时消费的场景
	q, err := ch.QueueDeclare(
		"",    // 队列名称:空字符串表示由 RabbitMQ 自动生成唯一名称
		false, // durable:不持久化(临时队列)
		false, // delete when unused:不再使用时删除
		true,  // exclusive:排他性(仅当前连接可用,退出后删除)
		false, // no-wait
		nil,   // arguments
	)
	failOnError(err, "Failed to declare a queue")

	// 4. 校验命令行参数:必须指定要接收的日志级别(如 warning error)
	if len(os.Args) < 2 {
		log.Printf("Usage: %s [info] [warning] [error]", os.Args[0])
		os.Exit(0)
	}

	// 5. 绑定队列到交换机:为每个指定的日志级别创建 Binding Key
	// 核心:队列可以绑定多个 Binding Key(比如同时绑定 warning 和 error)
	for _, s := range os.Args[1:] {
		log.Printf("Binding queue %s to exchange %s with routing key %s",
			q.Name, "logs_direct", s)
		err = ch.QueueBind(
			q.Name,        // 队列名称(RabbitMQ 自动生成的)
			s,             // Binding Key(要匹配的日志级别,如 error)
			"logs_direct", // 交换机名称
			false,
			nil)
		failOnError(err, "Failed to bind a queue")
	}

	// 6. 消费队列中的消息
	msgs, err := ch.Consume(
		q.Name, // 要消费的队列名称
		"",     // consumer:消费者标签(空字符串表示默认)
		true,   // auto ack:自动确认消息(消费后立即告诉 RabbitMQ 删除)
		false,  // exclusive:非排他
		false,  // no local
		false,  // no wait
		nil,    // args
	)
	failOnError(err, "Failed to register a consumer")

	// 7. 阻塞接收消息(goroutine + 无缓冲通道)
	var forever chan struct{} // 无缓冲通道,用于阻塞程序退出

	// 启动 goroutine 处理消息
	go func() {
		for d := range msgs { // 循环读取消息通道
			log.Printf(" [x] %s", d.Body) // 打印消息体
		}
	}()

	log.Printf(" [*] Waiting for logs. To exit press CTRL+C")
	<-forever // 阻塞,直到用户按 CTRL+C
}

核心代码

  • 临时队列声明QueueDeclare 的第一个参数是空字符串,RabbitMQ 会自动生成唯一的队列名;exclusive: true 表示队列是「排他的」------ 消费者断开连接后,队列会被自动删除,适合日志这种临时消费场景。
  • 多 Binding Key 绑定 :通过循环 os.Args[1:],为每个指定的日志级别(如 warning/error)创建绑定,这样队列就能接收多个级别的消息。
  • 自动确认消息auto ack: true 表示消费者收到消息后,立即告诉 RabbitMQ "已消费,可删除",简化逻辑(生产环境建议手动确认)。
  • 阻塞程序forever chan struct{} 是无缓冲通道,<-forever 会让程序一直阻塞,直到用户按 CTRL+C 退出。

代码运行

启动消费者 1:只接收 warning/error 日志(写入文件)

复制代码
go run receive_logs_direct.go warning error *> logs_from_rabbit.log
  • *> logs_from_rabbit.log:把输出重定向到文件,只保存 warning/error 日志。

启动消费者 2:接收所有日志(控制台打印)

复制代码
go run receive_logs_direct.go info warning error
# 输出:[*] Waiting for logs. To exit press CTRL+C

发送不同级别的消息(生产者)

复制代码
# 发送 info 级别的消息(只有消费者2能收到)
go run emit_log_direct.go info "System started successfully"
# 输出:[x] Sent info:System started successfully

# 发送 warning 级别的消息(两个消费者都能收到)
go run emit_log_direct.go warning "Low disk space"
# 输出:[x] Sent warning:Low disk space

# 发送 error 级别的消息(两个消费者都能收到)
go run emit_log_direct.go error "Database connection failed"
# 输出:[x] Sent error:Database connection failed

验证结果

  • 消费者 2 的控制台会打印所有 3 条消息;
  • logs_from_rabbit.log 文件中只有 warning 和 error 两条消息(没有 info)。

Topic Exchange 核心概念

在理解代码前,先搞懂 Topic Exchange 的核心价值和规则,这是后续代码的基础:

1. 为什么需要 Topic Exchange?

  • 之前的 Fanout Exchange(广播)太粗放,Direct Exchange(按单个关键字路由)不够灵活;
  • Topic Exchange 支持多维度的路由规则(比如按「来源 + 级别」路由日志),是 RabbitMQ 中最灵活的路由方式。

2. 核心规则

(1)Routing Key 格式要求

发送到 Topic Exchange 的消息,其routing_key必须是点分隔的单词列表 (比如 kern.criticallazy.orange.rabbit),单词数量最多 255 字节,通常每个单词代表一个维度(如第一个是「日志来源」,第二个是「日志级别」)。

(2)Binding Key 匹配规则

消费者绑定队列时用的binding_key格式和routing_key一致,且支持两个通配符:

  • *(星号):匹配恰好一个 单词(比如 kern.* 匹配 kern.infokern.critical,但不匹配 kernkern.info.debug);
  • #(井号):匹配零个或多个 单词(比如 lazy.# 匹配 lazylazy.orangelazy.orange.elephant)。
(3)特殊场景
  • 如果绑定键只用 #,Topic Exchange 等价于 Fanout Exchange(广播所有消息);
  • 如果绑定键不用任何通配符,Topic Exchange 等价于 Direct Exchange(精准匹配)。

3. 经典示例(教程中的动物场景)

Routing Key 绑定键 *.orange.* 绑定键 *.*.rabbit 绑定键 lazy.# 最终接收队列
quick.orange.rabbit Q1 + Q2
lazy.orange.elephant Q1 + Q2
quick.orange.fox Q1
lazy.brown.fox Q2
quick.brown.fox 丢弃
lazy.orange.new.rabbit Q2

二、生产者代码(emit_log_topic.go)

这个文件的作用是:向logs_topic这个 Topic Exchange 发送日志消息,消息的routing_key由命令行参数指定(格式如kern.critical),消息内容也由命令行参数指定。

1. 完整代码 + 逐行注释

复制代码
package main

import (
	"context"   // 用于设置消息发布的超时上下文
	"log"       // 日志输出
	"os"        // 读取命令行参数
	"strings"   // 字符串拼接
	"time"      // 设置超时时间

	// RabbitMQ Go客户端(官方推荐的amqp091版本)
	amqp "github.com/rabbitmq/amqp091-go"
)

// 工具函数:错误处理(出错时打印panic日志)
func failOnError(err error, msg string) {
	if err != nil {
		log.Panicf("%s: %s", msg, err) // Panic会终止程序,适合演示场景
	}
}

func main() {
	// 1. 连接RabbitMQ服务器(默认地址:localhost:5672,账号guest/guest)
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close() // 程序退出前关闭连接

	// 2. 创建信道(Channel):RabbitMQ的所有操作都通过信道完成
	ch, err := conn.Channel()
	failOnError(err, "Failed to open a channel")
	defer ch.Close() // 程序退出前关闭信道

	// 3. 声明Topic类型的交换器(exchange)
	// 注意:生产者和消费者都要声明交换器,确保交换器存在
	err = ch.ExchangeDeclare(
		"logs_topic", // 交换器名称
		"topic",      // 交换器类型(核心:topic)
		true,         // durable:是否持久化(重启RabbitMQ后交换器仍存在)
		false,        // auto-deleted:是否自动删除(无队列绑定时删除)
		false,        // internal:是否内部交换器(仅RabbitMQ内部使用)
		false,        // no-wait:是否非阻塞(不等待服务器确认)
		nil,          // arguments:额外参数(默认nil)
	)
	failOnError(err, "Failed to declare an exchange")

	// 4. 设置上下文超时(5秒):避免发布消息时无限阻塞
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel() // 函数结束时取消上下文

	// 5. 从命令行参数获取消息体和routing key
	body := bodyFrom(os.Args)       // 消息内容(第三个参数开始)
	routingKey := severityFrom(os.Args) // routing key(第二个参数)

	// 6. 发布消息到交换器
	err = ch.PublishWithContext(ctx,
		"logs_topic", // 目标交换器名称
		routingKey,   // 消息的routing key(核心:决定消息路由到哪些队列)
		false,        // mandatory:是否强制(无匹配队列时返回错误)
		false,        // immediate:是否立即(已废弃,固定false)
		amqp.Publishing{ // 消息体配置
			ContentType: "text/plain", // 消息内容类型(文本)
			Body:        []byte(body), // 消息内容
		})
	failOnError(err, "Failed to publish a message")

	// 7. 打印发送成功日志
	log.Printf(" [x] Sent %s", body)
}

// 辅助函数:获取消息体(命令行第三个参数及以后)
// 如果参数不足,默认发送"hello"
func bodyFrom(args []string) string {
	var s string
	if (len(args) < 3) || os.Args[2] == "" {
		s = "hello"
	} else {
		s = strings.Join(args[2:], " ") // 拼接多个参数为一个字符串
	}
	return s
}

// 辅助函数:获取routing key(命令行第二个参数)
// 如果参数不足,默认使用"anonymous.info"
func severityFrom(args []string) string {
	var s string
	if (len(args) < 2) || os.Args[1] == "" {
		s = "anonymous.info"
	} else {
		s = os.Args[1]
	}
	return s
}

2. 核心代码解释

  • 交换器声明ch.ExchangeDeclaretype: "topic" 是核心,明确这是 Topic Exchange;
  • 消息发布ch.PublishWithContext 是核心操作,指定了消息要发送到logs_topic交换器,且用severityFrom返回的字符串作为routing_key
  • 命令行参数处理severityFrom 读取第二个参数作为routing_key(比如kern.critical),bodyFrom 读取第三个及以后参数作为消息内容。

三、消费者代码(receive_logs_topic.go)

这个文件的作用是:创建临时队列,将队列绑定到logs_topic交换器(绑定键由命令行参数指定),然后持续消费队列中的消息。

1. 完整代码 + 逐行注释

go

运行

复制代码
package main

import (
	"log"
	"os"

	amqp "github.com/rabbitmq/amqp091-go"
)

// 工具函数:错误处理(和生产者一致)
func failOnError(err error, msg string) {
	if err != nil {
		log.Panicf("%s: %s", msg, err)
	}
}

func main() {
	// 1. 连接RabbitMQ服务器(和生产者一致)
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

	// 2. 创建信道(和生产者一致)
	ch, err := conn.Channel()
	failOnError(err, "Failed to open a channel")
	defer ch.Close()

	// 3. 声明相同的Topic交换器(确保交换器存在)
	err = ch.ExchangeDeclare(
		"logs_topic", // 交换器名称(必须和生产者一致)
		"topic",      // 类型:topic
		true,         // 持久化
		false,        // 自动删除
		false,        // 内部交换器
		false,        // 非阻塞
		nil,          // 额外参数
	)
	failOnError(err, "Failed to declare an exchange")

	// 4. 声明临时队列(核心:日志系统不需要持久化队列,用完即删)
	q, err := ch.QueueDeclare(
		"",    // 队列名称:空字符串表示由RabbitMQ自动生成唯一名称
		false, // durable:不持久化(重启后队列消失)
		false, // delete when unused:无消费者时自动删除
		true,  // exclusive:排他队列(仅当前连接可用,连接关闭后删除)
		false, // no-wait:非阻塞
		nil,   // 额外参数
	)
	failOnError(err, "Failed to declare a queue")

	// 5. 检查命令行参数:必须传入至少一个binding key
	if len(os.Args) < 2 {
		log.Printf("Usage: %s [binding_key]...", os.Args[0])
		os.Exit(0)
	}

	// 6. 绑定队列到交换器(支持多个binding key)
	// 遍历命令行参数(从第二个开始),每个参数作为一个binding key
	for _, bindingKey := range os.Args[1:] {
		log.Printf("Binding queue %s to exchange %s with routing key %s",
			q.Name, "logs_topic", bindingKey)
		err = ch.QueueBind(
			q.Name,       // 队列名称(自动生成的临时队列)
			bindingKey,   // binding key(核心:匹配routing key的规则)
			"logs_topic", // 交换器名称
			false,        // no-wait
			nil,          // 额外参数
		)
		failOnError(err, "Failed to bind a queue")
	}

	// 7. 注册消费者:监听队列中的消息
	msgs, err := ch.Consume(
		q.Name, // 要消费的队列名称
		"",     // consumer tag:空字符串表示自动生成
		true,   // auto-ack:自动确认(消费后立即告诉RabbitMQ删除消息)
		false,  // exclusive:非排他消费者
		false,  // no-local:不接收当前连接发布的消息
		false,  // no-wait:非阻塞
		nil,    // 额外参数
	)
	failOnError(err, "Failed to register a consumer")

	// 8. 阻塞程序:持续消费消息
	var forever chan struct{} // 无缓冲通道,用于阻塞

	// 启动goroutine处理消息(非阻塞)
	go func() {
		for d := range msgs { // 遍历消息通道,有消息时触发
			log.Printf(" [x] %s", d.Body) // 打印消息内容
		}
	}()

	// 打印提示,然后阻塞(直到按Ctrl+C退出)
	log.Printf(" [*] Waiting for logs. To exit press CTRL+C")
	<-forever // 从空通道读取,永久阻塞
}

2. 核心代码解释

  • 临时队列QueueDeclare 中名称为空字符串,且设置exclusive: true,意味着 RabbitMQ 会生成一个唯一的临时队列,连接关闭后自动删除(适合日志消费场景,每个消费者有独立队列);
  • 多 Binding Key 绑定 :通过循环遍历命令行参数,支持同时绑定多个 binding key(比如kern.**.critical),消费者会收到匹配任意一个 binding key 的消息;
  • 自动确认(auto-ack)ch.Consumeauto-ack: true表示消费者收到消息后立即确认,RabbitMQ 会删除该消息(演示场景简化用,生产环境建议手动确认)。

四、实际运行示例

前置条件

  1. 确保 RabbitMQ 已启动(本地:localhost:5672);
  2. 安装 Go 依赖:go get github.com/rabbitmq/amqp091-go
  3. 将生产者代码保存为emit_log_topic.go,消费者代码保存为receive_logs_topic.go

运行步骤

1. 启动消费者 1:接收所有日志(binding key = #)
复制代码
go run receive_logs_topic.go "#"

2026/02/06 10:00:00 Binding queue amq.gen-xxxx to exchange logs_topic with routing key #
2026/02/06 10:00:00 [*] Waiting for logs. To exit press CTRL+C
2. 启动消费者 2:接收 kern 来源的所有日志(binding key = kern.*)

新开终端:

复制代码
go run receive_logs_topic.go "kern.*"

输出:

复制代码
2026/02/06 10:01:00 Binding queue amq.gen-yyyy to exchange logs_topic with routing key kern.*
2026/02/06 10:01:00 [*] Waiting for logs. To exit press CTRL+C
3. 启动消费者 3:接收 critical 级别的所有日志(binding key = *.critical)

新开终端:

复制代码
go run receive_logs_topic.go "*.critical"

输出:

复制代码
2026/02/06 10:02:00 Binding queue amq.gen-zzzz to exchange logs_topic with routing key *.critical
2026/02/06 10:02:00 [*] Waiting for logs. To exit press CTRL+C
4. 发送测试消息

新开终端,发送routing_key=kern.critical的消息:

复制代码
go run emit_log_topic.go "kern.critical" "A critical kernel error"

输出:

复制代码
2026/02/06 10:03:00 [x] Sent A critical kernel error
5. 查看消费者输出
  • 消费者 1(#):收到 A critical kernel error
  • 消费者 2(kern.*):收到 A critical kernel error
  • 消费者 3(*.critical):收到 A critical kernel error
6. 再发送一个测试消息(routing_key=auth.warning)
复制代码
go run emit_log_topic.go "auth.warning" "Invalid login attempt"

此时只有消费者 1(#)能收到这条消息,消费者 2 和 3 都收不到(因为不匹配 binding key)。


五、总结

关键点回顾

  1. Topic Exchange 核心 :通过「点分隔的单词 + 通配符(*、#)」实现多维度路由,*匹配 1 个单词,#匹配 0 + 个单词;
  2. 生产者关键操作 :声明 Topic 交换器,指定符合规则的routing_key发布消息;
  3. 消费者关键操作:创建临时排他队列,绑定多个 binding key 到 Topic 交换器,持续消费队列消息;
  4. 灵活场景:Topic Exchange 可模拟 Fanout(#)和 Direct(无通配符),是 RabbitMQ 中最通用的交换器类型。

生产环境注意事项(扩展)

  • 避免滥用#通配符(会导致广播,降低性能);
  • 建议routing_key的单词数量固定(比如 2 个:来源。级别),便于维护;
  • 生产环境关闭auto-ack,改用手动确认(避免消息丢失);
  • 交换器和队列建议设置持久化(durable: true),确保重启后不丢失。
相关推荐
惊讶的猫3 小时前
rabbitmq实践小案例
分布式·rabbitmq
AC赳赳老秦4 小时前
代码生成超越 GPT-4:DeepSeek-V4 编程任务实战与 2026 开发者效率提升指南
数据库·数据仓库·人工智能·科技·rabbitmq·memcache·deepseek
惊讶的猫6 小时前
rabbitmq初步介绍
分布式·rabbitmq
惊讶的猫8 小时前
AMQP 与 RabbitMQ 四大模型
分布式·rabbitmq
像少年啦飞驰点、9 小时前
从零开始学 RabbitMQ:小白也能懂的消息队列实战指南
java·spring boot·微服务·消息队列·rabbitmq·异步编程
lekami_兰9 小时前
RabbitMQ 延迟队列实现指南:两种方案手把手教你搞定
后端·rabbitmq·延迟队列
为什么不问问神奇的海螺呢丶1 天前
n9e categraf rabbitmq监控配置
分布式·rabbitmq·ruby
m0_687399841 天前
telnet localhost 15672 RabbitMQ “Connection refused“ 错误表示目标主机拒绝了连接请求。
分布式·rabbitmq
Ronin3051 天前
日志打印和实用 Helper 工具
数据库·sqlite·rabbitmq·文件操作·uuid生成