三套 C++ 微服务常用库,逐个接口拆给你看。每个接口统一格式:函数签名 → 参数表 → 代码片段 → 运行结果。 所有代码来自
/home/dev/workspace/真实工程,所有运行结果是实际跑出来的。
第一部分:gflags(命令行参数解析)
1.1 DEFINE_bool / DEFINE_int32 / DEFINE_string / DEFINE_double --- 定义命令行参数
函数签名
cpp
DEFINE_bool(参数名, 默认值, 帮助描述);
DEFINE_int32(参数名, 默认值, 帮助描述);
DEFINE_string(参数名, 默认值, 帮助描述);
DEFINE_double(参数名, 默认值, 帮助描述);
参数表
| 参数 | 类型 | 含义 |
|---|---|---|
| 参数名 | 标识符 | 后面用 FLAGS_xxx 访问 |
| 默认值 | 对应类型 | 用户不传命令行时用这个值 |
| 帮助描述 | 字符串 | -help 时显示 |
代码片段 (gflags/main.cc:14-17)
cpp
DEFINE_bool(reuse_addr, true, "使用启用地址重用选项");
DEFINE_int32(listen_port, 8080, "服务器监听窗口");
DEFINE_string(listen_ip, "0.0.0.0", "服务器监听ip");
DEFINE_double(PI, 3.14, "圆周率");
运行结果
cpp
# 默认值运行
./main
1 ← bool true 输出 1
8080 ← int32 默认值
0.0.0.0 ← string 默认值
3.14 ← double 默认值
# 命令行覆盖
./main -listen_port=9090 -reuse_addr=false
0 ← false
9090 ← 覆盖
0.0.0.0
3.14
1.2 DECLARE_string / DECLARE_int32 / DECLARE_bool --- 跨文件声明参数
函数签名
cpp
DECLARE_string(参数名); // 在 A.cc 定义的 string 参数,B.cc 要访问就用 DECLARE
DECLARE_int32(参数名);
DECLARE_bool(参数名);
DECLARE_double(参数名);
作用 :DEFINE 只能出现一次(哪个文件定义都行),其他文件要访问同一个 FLAGS_xxx,就先用 DECLARE 声明。
代码片段 (gflags/child.cc:4)
cpp
// child.cc 里要访问 main.cc 定义的 listen_ip
DECLARE_string(listen_ip);
void Print() {
std::cout << FLAGS_listen_ip << std::endl; // 跨文件访问
}
运行结果 
1.3 google::ParseCommandLineFlags --- 解析命令行
函数签名
cpp
bool ParseCommandLineFlags(int* argc, char*** argv, bool remove_flags);
参数表
| 参数 | 类型 | 含义 |
|---|---|---|
| argc | int* | 指针,会被改写 |
| argv | char*** | 指针,会被改写 |
| remove_flags | bool | true=解析完后把 flag 从 argv 里删掉 |
返回值:成功返回 true。
代码片段 (gflags/main.cc:23)
cpp
int main(int argc, char* argv[]) {
google::ParseCommandLineFlags(&argc, &argv, true);
// 之后就能用 FLAGS_xxx 访问参数了
}
运行结果
cpp
./main -listen_port=9090 # 命令行参数被解析
./main --flagfile=main.conf # 也支持从配置文件读取(见下)
1.4 --flagfile= --- 从配置文件读参数
代码片段 (gflags/main.conf)
cpp
--listen_ip=0.0.0.0
--reuse_addr=false
--listen_port=9090
--PI=3.1415926
运行结果
cpp
./main --flagfile=main.conf
0 ← 来自 conf
9090 ← 来自 conf
0.0.0.0 ← 来自 conf
3.14159 ← 来自 conf(double 截断显示)
0.0.0.0 ← child.cc Print()
1.5 gflags 小结:命令行参数优先级
cpp
配置文件 --flagfile < 命令行直接传参
即:配置文件里写的可以被命令行覆盖。
gflags 工程文件一览
| 文件 | 作用 |
|---|---|
| main.cc | DEFINE + Parse + 直接访问 FLAGS |
| child.h | 声明 extern void Print() |
| child.cc | DECLARE + 跨文件访问 FLAGS |
| main.conf | 配置文件 |
| Makefile | g++ main.cc child.cc -o main -lgflags |
第二部分:gtest(单元测试框架)
2.1 testing::InitGoogleTest --- 初始化测试框架
函数签名
void InitGoogleTest(int* argc, char** argv);
作用 :解析 gtest 自己的命令行参数(比如 --gtest_filter),必须在 RUN_ALL_TESTS() 之前调用。
代码片段(所有 gtest 工程 main 都一样)
cpp
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
// ... AddGlobalTestEnvironment 等 ...
return RUN_ALL_TESTS();
}
2.2 RUN_ALL_TESTS() --- 执行所有测试用例
作用 :扫描所有 TEST / TEST_F 宏注册的用例,逐个执行并输出结果。
代码片段
cpp
return RUN_ALL_TESTS(); // main 里的 return 值
2.3 TEST(测试套件, 用例名) --- 定义普通测试用例
宏展开
cpp
#define TEST(test_suite_name, test_name) GTEST_TEST(test_suite_name, test_name)
参数表
| 参数 | 含义 |
|---|---|
| test_suite_name | 测试套件名(分组) |
| test_name | 用例名 |
代码片段 (gtest/simple.cc:13-35)
cpp
TEST(unordered_map_set, insert) {
std::unordered_map<int, int> map;
map.insert(std::make_pair(1, 1));
map.insert(std::make_pair(2, 2));
map.insert(std::make_pair(3, 3));
ASSERT_EQ(map.size(), 3);
}
TEST(unordered_map_test, insert) {
std::unordered_map<int, int> map;
map.insert(std::make_pair(1, 1));
map.insert(std::make_pair(2, 2));
map.insert(std::make_pair(3, 3));
ASSERT_EQ(map.size(), 3);
}
TEST(unordered_map_test, find) {
std::unordered_map<int, int> map;
map.insert(std::make_pair(1, 1));
map.insert(std::make_pair(2, 2));
ASSERT_NE(map.find(1), map.end());
ASSERT_EQ(map.find(1)->second, 1);
}
运行结果 
2.4 ASSERT_EQ / ASSERT_NE / ASSERT_TRUE / ASSERT_FALSE --- 断言宏
作用 :检查条件是否成立。ASSERT_* 是 fatal 断言,失败了当前用例直接终止;EXPECT_* 是 non-fatal,失败了继续往下走。
完整对照表
| 断言 | 含义 | 示例 |
|---|---|---|
ASSERT_EQ(a, b) |
a == b |
ASSERT_EQ(map.size(), 3); |
ASSERT_NE(a, b) |
a != b |
ASSERT_NE(map.find(1), map.end()); |
ASSERT_LT(a, b) |
a < b |
ASSERT_LT(1, 2); |
ASSERT_LE(a, b) |
a <= b |
ASSERT_LE(map.size(), 10); |
ASSERT_GT(a, b) |
a > b |
ASSERT_GT(2, 1); |
ASSERT_GE(a, b) |
a >= b |
ASSERT_GE(map.size(), 3); |
ASSERT_TRUE(x) |
x == true |
ASSERT_TRUE(map.empty()); |
ASSERT_FALSE(x) |
x == false |
ASSERT_FALSE(flag); |
四个类别的助记
| 前缀 | 含义 | 对应英文 |
|---|---|---|
EQ |
等于 | Equal |
NE |
不等于 | N ot Equal |
LT |
小于 | L ess Than |
LE |
小于等于 | L ess or Equal |
GT |
大于 | G reater Than |
GE |
大于等于 | G reater or Equal |
TRUE / FALSE |
布尔判断 | --- |
代码片段
cpp
ASSERT_EQ(map.size(), 3);
ASSERT_NE(map.find(1), map.end());
ASSERT_EQ(map.find(1)->second, 1);
2.5 testing::Test + TEST_F --- 局部测试套件(Fixture)
是什么? 当多个用例共享相同的初始化/清理逻辑时,继承 testing::Test 定义一个 Fixture 类,用 TEST_F 代替 TEST。
Fixture 生命周期
cpp
SetUpTestCase() ← 所有用例之前执行一次(类级)
├── SetUp() ← 每个用例之前执行
│ ├── 测试用例体
│ └── TearDown() ← 每个用例之后执行
├── SetUp()
│ ├── 测试用例体
│ └── TearDown()
TearDownTestCase() ← 所有用例之后执行一次(类级)
代码片段 (gtest/local.cc:14-48)
cpp
class LocalTestSuite : public testing::Test {
public:
static void SetUpTestCase() {
std::cout << "公共环境初始接口--会在所有测试用例之前执行\n";
}
static void TearDownTestCase() {
std::cout << "公共环境清理接口--会在所有测试用例之后执行\n";
}
virtual void SetUp() {
std::cout << "执行于每个用例之前" << std::endl;
map.insert(std::make_pair(1, 1));
map.insert(std::make_pair(2, 2));
map.insert(std::make_pair(3, 3));
}
virtual void TearDown() {
std::cout << "执行于每个用例之后" << std::endl;
map.clear();
}
public:
std::unordered_map<int, int> map;
};
TEST_F(LocalTestSuite, find) {
ASSERT_EQ(map.size(), 3);
ASSERT_EQ(map.find(1)->second, 1);
map.erase(1);
std::cout << "测试用例1" << std::endl;
}
运行结果 
2.6 testing::Environment + AddGlobalTestEnvironment --- 全局测试环境
是什么? 和 Fixture 的区别:Fixture 管一个测试套件 的生命周期,Environment 管整个测试程序的生命周期。所有套件共享。
Environment 生命周期
cpp
Environment::SetUp() ← 所有测试套件、所有用例之前执行一次
├── 套件1 SetUpTestCase → SetUp → 用例1 → TearDown → TearDownTestCase
├── 套件2 SetUpTestCase → SetUp → 用例2 → TearDown → TearDownTestCase
Environment::TearDown() ← 所有测试套件、所有用例之后执行一次
代码片段 (gtest/global.cc:22-50)
cpp
// 全局变量,所有测试共用
std::unordered_map<int, int> g_map;
class GlobalTestEnvironment : public testing::Environment {
public:
virtual void SetUp() override {
std::cout << "执行于所有例子之前" << std::endl;
g_map.insert(std::make_pair(1, 1));
g_map.insert(std::make_pair(2, 2));
g_map.insert(std::make_pair(3, 3));
}
virtual void TearDown() override {
std::cout << "执行于所有用例之后" << std::endl;
g_map.clear();
}
};
TEST(GlobalMapTest, Insert) {
ASSERT_EQ(g_map.size(), 3);
}
TEST(GlobalMapTest, Find) {
ASSERT_NE(g_map.find(1), g_map.end());
ASSERT_EQ(g_map.find(1)->second, 1);
}
int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
testing::AddGlobalTestEnvironment(new GlobalTestEnvironment);
return RUN_ALL_TESTS();
}
运行结果

2.7 gtest 三种测试方式对比
| 方式 | 宏/类 | 生命周期 | 适用场景 |
|---|---|---|---|
| 普通测试 | TEST(Suite, Name) |
无共享,每次独立 | 简单、无依赖的测试 |
| 局部套件 | TEST_F(Fixture, Name) + 继承 testing::Test |
每个套件独立的 SetUp/TearDown | 同套件多个用例共享数据 |
| 全局环境 | AddGlobalTestEnvironment + 继承 testing::Environment |
整个程序只执行一次 | 所有套件共享的全局资源 |
gtest 工程文件一览
| 文件 | 作用 |
|---|---|
| simple.cc | TEST 宏 + ASSERT 断言 |
| local.cc | TEST_F + testing::Test Fixture |
| global.cc | testing::Environment + AddGlobalTestEnvironment |
| Makefile | g++ xxx.cc -o xxx -lgtest |
第三部分:spdlog(高性能日志库)
3.1 spdlog::init_thread_pool --- 初始化异步线程池
函数签名
cpp
void init_thread_pool(size_t queue_size, size_t n_threads);
参数表
| 参数 | 类型 | 含义 |
|---|---|---|
| queue_size | size_t | 日志队列能缓存多少条,默认 8192 |
| n_threads | size_t | 后台消费线程数,默认 1 |
异步模式必须先调用,否则异步工厂会崩溃!
代码片段 (stdout.cc:22)
cpp
spdlog::init_thread_pool(8192, 1);
3.2 spdlog::stdout_color_mt<Factory> --- 彩色控制台 logger 工厂
函数签名
cpp
// 同步版本
std::shared_ptr<logger> stdout_color_mt(const std::string& logger_name, color_mode mode = color_mode::automatic);
// 异步版本(模板参数指定工厂)
template<typename Factory>
std::shared_ptr<logger> stdout_color_mt<Factory>(const std::string& logger_name, color_mode mode = color_mode::automatic);
参数表
| 参数 | 类型 | 含义 |
|---|---|---|
| Factory | 模板参数 | spdlog::async_factory=异步;不传=同步 |
| logger_name | string | logger 名字,用于 drop / registry 查找 |
| mode | color_mode | automatic=tty 才上色,always=强制,never=不上色 |
代码片段
cpp
// 异步
auto logger = spdlog::stdout_color_mt<spdlog::async_factory>("stdout_logger");
// 同步
auto logger2 = spdlog::stdout_color_mt("stdout_logger");
运行结果
cpp
[16:42:07][debug ][test.cc:26]: 小明今年18岁 ← 终端彩色高亮
[16:42:07][info ][test.cc:27]: 小红今年19岁
[16:42:07][warning][test.cc:28]: 小刚今年20岁
[16:42:07][error ][test.cc:29]: 小李今年21岁
3.3 spdlog::basic_logger_mt<Factory> --- 基础文件 logger 工厂
函数签名
cpp
// 同步版本
std::shared_ptr<logger> basic_logger_mt(const std::string& logger_name, const std::string& filename, bool truncate = false);
// 异步版本
template<typename Factory>
std::shared_ptr<logger> basic_logger_mt<Factory>(const std::string& logger_name, const std::string& filename, bool truncate = false);
参数表
| 参数 | 类型 | 含义 |
|---|---|---|
| Factory | 模板参数 | spdlog::async_factory=异步 |
| logger_name | string | logger 名字 |
| filename | string | 日志文件路径 |
| truncate | bool | true=每次清空文件,false=追加(默认) |
代码片段
cpp
g_logger = spdlog::basic_logger_mt<spdlog::async_factory>("file_logger", "./app.log");
运行结果
cpp
./app -log_path=./test.log -log_level=4
cat test.log
[16:42:07][error ][test.cc:29]: 小李今年21岁
3.4 spdlog::drop --- 按名字清理 logger
函数签名
cpp
void drop(const std::string& logger_name);
作用 :spdlog registry 里不允许同名 logger。重复调用初始化函数时,必须先 drop 旧的,否则抛 logger already exists 异常。
代码片段 (log.cc:18-19)
cpp
spdlog::drop("stdout_logger");
spdlog::drop("file_logger");
3.5 logger->set_level --- 设置日志等级
函数签名
cpp
void set_level(level::level_enum log_level);
等级对照表
| 枚举值 | 整数 | 含义 |
|---|---|---|
level::trace |
0 | 最细,什么都打 |
level::debug |
1 | 调试信息 |
level::info |
2 | 一般信息 |
level::warn |
3 | 警告 |
level::err |
4 | 错误 |
level::critical |
5 | 致命错误 |
level::off |
6 | 全部关闭 |
代码片段
cpp
logger->set_level(spdlog::level::err); // 直接传枚举
logger->set_level(spdlog::level::level_enum(settings.level)); // int 转枚举
运行结果
cpp
# 默认 level=1(debug),全部输出
./app
[16:42:07][debug ][test.cc:26]: 小明今年18岁
[16:42:07][info ][test.cc:27]: 小红今年19岁
[16:42:07][warning][test.cc:28]: 小刚今年20岁
[16:42:07][error ][test.cc:29]: 小李今年21岁
[16:42:07][debug ][test.cc:30]: HELLO
# 改成 level=4(err),只留 error
./app -log_level=4
[16:42:07][error ][test.cc:29]: 小李今年21岁
3.6 logger->set_pattern --- 设置输出格式
函数签名
cpp
void set_pattern(const std::string& pattern);
常用占位符
| 占位符 | 含义 | 示例 |
|---|---|---|
%v |
用户消息 | 小明今年18岁 |
%l |
level 全名 | debug |
%-7l |
level 左对齐 7 字符 | debug |
%H:%M:%S |
时分秒 | 16:42:07 |
%Y-%m-%d |
年月日 | 2026-09-18 |
%n |
logger 名 | stdout_logger |
代码片段
cpp
logger->set_pattern("[%H:%M:%S][%l]: %v"); // stdout.cc 用的
logger->set_pattern("[%H:%M:%S][%-7l]: %v"); // 封装默认,level 对齐
3.7 logger->error / info / warn / debug --- 记录日志
函数签名
cpp
template<typename... Args>
void error(fmt::format_string<Args...> fmt, Args&&... args);
// info / warn / debug 签名一样
代码片段
cpp
logger->error("hello world - {}", i); // 一个占位符
logger->info("{}今年{}岁", "小明", 18); // 两个占位符
logger->debug("HELLO"); // 无占位符
运行结果
cpp
./stdout | head -5
[16:42:08][error]: hello world - 0
[16:42:08][error]: hello world - 1
[16:42:08][error]: hello world - 2
[16:42:08][error]: hello world - 3
[16:42:08][error]: hello world - 4
./app
[16:42:07][debug ][test.cc:26]: 小明今年18岁
[16:42:07][info ][test.cc:27]: 小红今年19岁
[16:42:07][warning][test.cc:28]: 小刚今年20岁
[16:42:07][error ][test.cc:29]: 小李今年21岁
3.8 logger->flush --- 刷新异步队列
函数签名
cpp
void flush();
作用 :异步模式日志先进内存队列,flush() 强制落盘/输出。程序退出前调一下,防止崩溃丢日志。
代码片段 (stdout.cc:33)
cpp
logger->flush();
return 0;
3.9 spdlog1 封装:log_settings + spdlog1_init + DBG/INF/WRN/ERR
log_settings 结构体 (log.h:25-30)
cpp
struct log_settings {
bool async = false; // 是否启用异步日志
int level = 1; // 0~6 对应 level_enum
std::string format = "[%H:%M:%S][%-7l]: %v"; // 日志格式
std::string path = "stdout"; // "stdout" 或 文件路径
};
spdlog1_init 内部逻辑 (log.cc:10-37)
cpp
1. async=true → init_thread_pool(8192, 1)
2. drop("stdout_logger") + drop("file_logger")
3. 按 async + path 组合创建 logger
4. set_level + set_pattern
日志宏 (log.h:39-44)
cpp
#define FMT_PREFIX "[{}:{}]: "
#define DBG(fmt, ...) spdlog1::g_logger->debug(FMT_PREFIX fmt, __FILE__, __LINE__, ##__VA_ARGS__)
#define INF(fmt, ...) spdlog1::g_logger->info (FMT_PREFIX fmt, __FILE__, __LINE__, ##__VA_ARGS__)
#define WRN(fmt, ...) spdlog1::g_logger->warn (FMT_PREFIX fmt, __FILE__, __LINE__, ##__VA_ARGS__)
#define ERR(fmt, ...) spdlog1::g_logger->error(FMT_PREFIX fmt, __FILE__, __LINE__, ##__VA_ARGS__)
宏的魔术
FMT_PREFIX fmt:相邻字符串编译期自动拼接,零开销。"[{}:{}]: " "{}今年{}岁"→"[{}:{}]: {}今年{}岁"
__FILE__/__LINE__:编译器预定义宏,自动带文件名和行号
##__VA_ARGS__:没额外参数时把逗号吃了,也能编译运行结果
第四部分:三库协作的完整运行链路
4.1 spdlog1 + gflags 链路(test.cc)
cpp
main()
├── google::ParseCommandLineFlags ← gflags 解析命令行
├── 组装 log_settings ← 把 FLAGS 搬进配置
├── spdlog1::spdlog1_init(settings) ← 初始化 logger
│ ├── init_thread_pool(async=true 时)← spdlog
│ ├── spdlog::drop × 2 ← spdlog 清理旧 logger
│ ├── stdout_color_mt / basic_logger_mt ← spdlog 创建 logger
│ ├── set_level ← spdlog
│ └── set_pattern ← spdlog
├── DBG / INF / WRN / ERR ← spdlog1 宏展开
│ └── g_logger->debug/info/warn/error ← spdlog 记录日志
└── return 0
4.2 工程文件总览
| 库 | 工程目录 | 关键文件 | 编译命令 |
|---|---|---|---|
| gflags | /home/dev/workspace/gflags/ |
main.cc + child.cc + main.conf | g++ main.cc child.cc -o main -lgflags |
| gtest | /home/dev/workspace/gtest/ |
simple.cc + local.cc + global.cc | g++ xxx.cc -o xxx -lgtest |
| spdlog | /home/dev/workspace/spdlog/ |
stdout.cc + spdlog1/log.h/log.cc/test.cc | g++ xxx.cc -o xxx -lspdlog -lpthread -lfmt |
自检清单
| 检查项 | 结果 |
|---|---|
| gflags 编译 | make clean && make main 通过 |
| gtest 编译(三个) | make clean && make ALL 通过 |
| spdlog1 编译 | make clean && make 通过 |
| gflags 默认运行 | 输出 1/8080/0.0.0.0/3.14/0.0.0.0 |
| gflags 命令行覆盖 | -listen_port=9090 生效 |
| gflags 配置文件 | --flagfile=main.conf 生效 |
| gtest simple | 3 tests passed |
| gtest local | 生命周期输出顺序正确(SetUpTestCase→SetUp→用例→TearDown→TearDownTestCase) |
| gtest global | Environment 生命周期正确 |
| spdlog1 默认 | 5 行彩色输出 |
| spdlog1 -log_level=4 | 只剩 1 行 error |
| spdlog1 文件输出 | 文件内容正确 |
| stdout.cc | 10000 条 error 日志 |
| 函数签名 vs 系统头文件 | InitGoogleTest / AddGlobalTestEnvironment / TEST / TEST_F / Environment / Test / init_thread_pool / stdout_color_mt / basic_logger_mt / drop 全部核对过 |
第五部分:代码如下:
















