用户创建
在 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.critical、lazy.orange.rabbit),单词数量最多 255 字节,通常每个单词代表一个维度(如第一个是「日志来源」,第二个是「日志级别」)。
(2)Binding Key 匹配规则
消费者绑定队列时用的binding_key格式和routing_key一致,且支持两个通配符:
*(星号):匹配恰好一个 单词(比如kern.*匹配kern.info、kern.critical,但不匹配kern或kern.info.debug);#(井号):匹配零个或多个 单词(比如lazy.#匹配lazy、lazy.orange、lazy.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.ExchangeDeclare中type: "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.Consume中auto-ack: true表示消费者收到消息后立即确认,RabbitMQ 会删除该消息(演示场景简化用,生产环境建议手动确认)。
四、实际运行示例
前置条件
- 确保 RabbitMQ 已启动(本地:
localhost:5672); - 安装 Go 依赖:
go get github.com/rabbitmq/amqp091-go; - 将生产者代码保存为
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)。
五、总结
关键点回顾
- Topic Exchange 核心 :通过「点分隔的单词 + 通配符(*、#)」实现多维度路由,
*匹配 1 个单词,#匹配 0 + 个单词; - 生产者关键操作 :声明 Topic 交换器,指定符合规则的
routing_key发布消息; - 消费者关键操作:创建临时排他队列,绑定多个 binding key 到 Topic 交换器,持续消费队列消息;
- 灵活场景:Topic Exchange 可模拟 Fanout(#)和 Direct(无通配符),是 RabbitMQ 中最通用的交换器类型。
生产环境注意事项(扩展)
- 避免滥用
#通配符(会导致广播,降低性能); - 建议
routing_key的单词数量固定(比如 2 个:来源。级别),便于维护; - 生产环境关闭
auto-ack,改用手动确认(避免消息丢失); - 交换器和队列建议设置持久化(durable: true),确保重启后不丢失。