云计算百科
云计算领域专业知识百科平台

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

三套 C++ 微服务常用库,逐个接口拆给你看。每个接口统一格式:函数签名 → 参数表 → 代码片段 → 运行结果。 所有代码来自 /home/dev/workspace/ 真实工程,所有运行结果是实际跑出来的。


第一部分:gflags(命令行参数解析)

1.1 DEFINE_bool / DEFINE_int32 / DEFINE_string / DEFINE_double — 定义命令行参数

函数签名

DEFINE_bool(参数名, 默认值, 帮助描述);
DEFINE_int32(参数名, 默认值, 帮助描述);
DEFINE_string(参数名, 默认值, 帮助描述);
DEFINE_double(参数名, 默认值, 帮助描述);

参数表

参数

类型

含义

参数名

标识符

后面用 FLAGS_xxx 访问

默认值

对应类型

用户不传命令行时用这个值

帮助描述

字符串

-help 时显示

代码片段(gflags/main.cc:14-17)

DEFINE_bool(reuse_addr, true, "使用启用地址重用选项");
DEFINE_int32(listen_port, 8080, "服务器监听窗口");
DEFINE_string(listen_ip, "0.0.0.0", "服务器监听ip");
DEFINE_double(PI, 3.14, "圆周率");

运行结果

# 默认值运行
./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 — 跨文件声明参数

函数签名

DECLARE_string(参数名); // 在 A.cc 定义的 string 参数,B.cc 要访问就用 DECLARE
DECLARE_int32(参数名);
DECLARE_bool(参数名);
DECLARE_double(参数名);

作用:DEFINE 只能出现一次(哪个文件定义都行),其他文件要访问同一个 FLAGS_xxx,就先用 DECLARE 声明。

代码片段(gflags/child.cc:4)

// child.cc 里要访问 main.cc 定义的 listen_ip
DECLARE_string(listen_ip);

void Print() {
std::cout << FLAGS_listen_ip << std::endl; // 跨文件访问
}

运行结果


1.3 google::ParseCommandLineFlags — 解析命令行

函数签名

bool ParseCommandLineFlags(int* argc, char*** argv, bool remove_flags);

参数表

参数

类型

含义

argc

int*

指针,会被改写

argv

char***

指针,会被改写

remove_flags

bool

true=解析完后把 flag 从 argv 里删掉

返回值:成功返回 true。

代码片段(gflags/main.cc:23)

int main(int argc, char* argv[]) {
google::ParseCommandLineFlags(&argc, &argv, true);
// 之后就能用 FLAGS_xxx 访问参数了
}

运行结果

./main -listen_port=9090 # 命令行参数被解析
./main –flagfile=main.conf # 也支持从配置文件读取(见下)


1.4 –flagfile= — 从配置文件读参数

代码片段(gflags/main.conf)

–listen_ip=0.0.0.0
–reuse_addr=false
–listen_port=9090
–PI=3.1415926

运行结果

./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 小结:命令行参数优先级

配置文件 –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 都一样)

int main(int argc, char* argv[]) {
testing::InitGoogleTest(&argc, argv);
// … AddGlobalTestEnvironment 等 …
return RUN_ALL_TESTS();
}


2.2 RUN_ALL_TESTS() — 执行所有测试用例

作用:扫描所有 TEST / TEST_F 宏注册的用例,逐个执行并输出结果。

代码片段

return RUN_ALL_TESTS(); // main 里的 return 值


2.3 TEST(测试套件, 用例名) — 定义普通测试用例

宏展开

#define TEST(test_suite_name, test_name) GTEST_TEST(test_suite_name, test_name)

参数表

参数

含义

test_suite_name

测试套件名(分组)

test_name

用例名

代码片段(gtest/simple.cc:13-35)

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 不等于 Not Equal
LT 小于 Less Than
LE 小于等于 Less or Equal
GT 大于 Greater Than
GE 大于等于 Greater or Equal
TRUE / FALSE 布尔判断

代码片段

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 生命周期

SetUpTestCase() ← 所有用例之前执行一次(类级)
├── SetUp() ← 每个用例之前执行
│ ├── 测试用例体
│ └── TearDown() ← 每个用例之后执行
├── SetUp()
│ ├── 测试用例体
│ └── TearDown()
TearDownTestCase() ← 所有用例之后执行一次(类级)

代码片段(gtest/local.cc:14-48)

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 生命周期

Environment::SetUp() ← 所有测试套件、所有用例之前执行一次
├── 套件1 SetUpTestCase → SetUp → 用例1 → TearDown → TearDownTestCase
├── 套件2 SetUpTestCase → SetUp → 用例2 → TearDown → TearDownTestCase
Environment::TearDown() ← 所有测试套件、所有用例之后执行一次

代码片段(gtest/global.cc:22-50)

// 全局变量,所有测试共用
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 — 初始化异步线程池

函数签名

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)

spdlog::init_thread_pool(8192, 1);


3.2 spdlog::stdout_color_mt<Factory> — 彩色控制台 logger 工厂

函数签名

// 同步版本
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=不上色

代码片段

// 异步
auto logger = spdlog::stdout_color_mt<spdlog::async_factory>("stdout_logger");
// 同步
auto logger2 = spdlog::stdout_color_mt("stdout_logger");

运行结果

[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 工厂

函数签名

// 同步版本
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=追加(默认)

代码片段

g_logger = spdlog::basic_logger_mt<spdlog::async_factory>("file_logger", "./app.log");

运行结果

./app -log_path=./test.log -log_level=4
cat test.log
[16:42:07][error ][test.cc:29]: 小李今年21岁


3.4 spdlog::drop — 按名字清理 logger

函数签名

void drop(const std::string& logger_name);

作用:spdlog registry 里不允许同名 logger。重复调用初始化函数时,必须先 drop 旧的,否则抛 logger already exists 异常。

代码片段(log.cc:18-19)

spdlog::drop("stdout_logger");
spdlog::drop("file_logger");


3.5 logger->set_level — 设置日志等级

函数签名

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

全部关闭

代码片段

logger->set_level(spdlog::level::err); // 直接传枚举
logger->set_level(spdlog::level::level_enum(settings.level)); // int 转枚举

运行结果

# 默认 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 — 设置输出格式

函数签名

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

代码片段

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 — 记录日志

函数签名

template<typename… Args>
void error(fmt::format_string<Args…> fmt, Args&&… args);
// info / warn / debug 签名一样

代码片段

logger->error("hello world – {}", i); // 一个占位符
logger->info("{}今年{}岁", "小明", 18); // 两个占位符
logger->debug("HELLO"); // 无占位符

运行结果

./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 — 刷新异步队列

函数签名

void flush();

作用:异步模式日志先进内存队列,flush() 强制落盘/输出。程序退出前调一下,防止崩溃丢日志。

代码片段(stdout.cc:33)

logger->flush();
return 0;


3.9 spdlog1 封装:log_settings + spdlog1_init + DBG/INF/WRN/ERR

log_settings 结构体(log.h:25-30)

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)

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)

#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)

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


第五部分:代码如下:



赞(0)
未经允许不得转载:网硕互联帮助中心 » 【C++微服务项目开发脚手架】(接口篇一)gflags + gtest + spdlog 接口学习笔记
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!