【C++微服务项目开发脚手架】(接口篇一)gflags + gtest + spdlog 接口学习笔记

三套 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 全部核对过

第五部分:代码如下:



相关推荐
星期八不上发条1 小时前
智能指针是什么?使用场景,循环引用解决办法,面试回答
开发语言·c++·stl
ao-weilai1 小时前
Linux网络编程:Socket UDP
linux·服务器·网络·c++
彧azz1 小时前
图的最短路径:Dijkstra与Floyd算法
数据结构·笔记·学习
笨鸟先飞的橘猫2 小时前
系统设计复盘2026-09-18
学习
.YM.Z3 小时前
C++——【继承】的底层逻辑与【多态】实现机制
c++·多态·继承
汉克老师3 小时前
GESP2026年9月认证C++三级( 第一部分选择题(1~7题)精讲
c++·gesp·小学生·学c++编程
传奇开心果编程3 小时前
【Rust入门练中学】 第1课:从零开始
开发语言·学习·rust
高亦真4 小时前
今天是学习嵌入式的第39天
linux·学习·算法
小小龙学IT4 小时前
Qt QPainter 2D 绘图系统深度解析:从画笔到渲染引擎适
c++·qt