
先看一段代码
//Nginx(高性能 Web 服务器)
#define ngx_log_error(level, log, err, fmt, ...) \
do { \
if (log->log_level >= level) \
ngx_log_write(log, level, err, fmt, ##__VA_ARGS__); \
} while(0)
//Redis(最流行的缓存数据库)
#define redisLog(level, fmt, ...) \
do { \
if (level <= server.verbosity) \
_redisLog(level, fmt, ##__VA_ARGS__); \
} while(0)
你发现2个问题
为何 Linux 内核、Redis、Nginx、FreeBSD、TCP/IP 协议栈 都采用这个方式
先说答案:这个规则是Linux 内核编码的强制要求
Linux 内核官方《CodingStyle》文档明确规定:
所有包含多条语句的宏,都必须用
do { ... } while(0)包裹,保证宏像函数一样安全调用
do-while(0) 用引言
深耕 C/C++ 开发的人,几乎都踩过分号带来的诡异语法坑:
if-else 因多余分号编译报错、多行宏为规避语法漏洞,被迫用上反直觉的 do{...}while(0) 写法。
究其根源,是传统 C/C++ 编译器无视换行、只认分号作为语句结束标记,硬生生衍生出一堆语法补丁和编码潜规则。
而跳出老式语言的桎梏,再看 Rust 的语言设计: 编译器依靠语法结构自动断句,彻底摆脱分号作为语句结束符的束缚; 分号也被赋予全新语义,仅用来丢弃表达式返回值。

普通函数调用要:
压栈 → 跳转 → 执行 → 返回 → 出栈
哪怕再小的函数,也有栈开销、指令跳转开销。
Linux 内核、驱动、嵌入式、高频底层代码:
很多代码每毫秒执行上万次,承受不起函数调用损耗。 【日志优化需求关于日志打印不要太多哦,百万千万行日志很容易出现】
宏是预处理直接文本粘贴,原地嵌代码,没有任何调用跳转,和直接写裸代码一模一样快
C 没有 C++ 模板、没有泛型。
比如 Linux 内核经典:链表遍历
list_for_each_entry(pos, head, member)
能遍历任意结构体的链表:设备、进程、文件、驱动节
普通函数必须固定死结构体类型,写一百种结构就要写一百个遍历函数,根本不现实。
宏可以参数化类型、偏移,一个宏通吃所有结构体,函数做不到
日志、断言、报错定位,必须知道:
在哪一行、哪个文件、哪个函数出的错。
内置宏:
__FILE____LINE____func__
do{...}while(0) 是保护语法的,
但保护不了你在宏里写 return 这种情况
原文:
Things to avoid when using macros:
使用宏时应避免的事项:
macros that affect control flow:
影响控制流的宏:
#define FOO(x) \
do { \
if (blah(x) < 0) \
return -EBUGGERED; \
} while (0) ?
is a very bad idea.
It looks like a function call but exits the calling function;
don’t break the internal parsers of those who will read the code
义了一个带 return 的宏,
看起来是普通函数调用,实际会直接退出调用它的外层函数
这会彻底欺骗读代码的人,破坏程序控制流,是绝对的坏写法
FOO(val); //所有人都会以为这只是一个普通的函数调用,执行完会继续往下走。 但实际:满足条件直接 return,函数直接退出,后面的代码全不执行!
do_something(); // 不在执行
save_data(); // 不在执行
do{...}while(0) 保证代码执行到 while(0)
do{...}while(0) 就是为了 吃掉这个分号,让语法永远正确
不然:error: ‘else’ without a previous ‘if’
基本原理:
if(条件) 后面 只能跟 1 条语句{} 包起来 → {} 整体算 1 条语句{} 后面绝对不能写分号 ;**!!举例:
{})#define INIT() { a=0; b=0; }
// 你调用时写了分号!
if(flag)
INIT();
else
...
#展开后(自动多加了分号,直接炸)
if(flag)
{ a=0; b=0; }; // ❌ 错误:{} + 分号
else
...
do{...}while(0))#define INIT() do{ a=0; b=0; } while(0)
// 调用加分号完全没问题
if(flag)
INIT();
else
...
if(flag)
do{ a=0; b=0; } while(0); // ✅ 正确:单语句+分号
else
...
规则
编译器看到 换行,就自动在行尾插入分号。
package main
import "fmt"
func main() {
a := 10 // 编译器自动加 ;
fmt.Println(a) // 编译器自动加 ;
}
Rust 比 Go 更彻底:
Rust 函数返回值(最经典用法)
Rust 函数最后一行不写分号,就是函数的返回值
// 定义加法函数,返回 i32 类型
fn add(a: i32, b: i32) -> i32 {
a + b // 无分号 = 表达式 = 函数返回值
}
fn main() {
let sum = add(10, 20);
println!("{}", sum); // 输出:30
}
main 是特殊函数,强制返回空值 所以 main 里的所有代码,都必须是**语句(丢弃返回值),因此要分号
do{...}while(0)?答案:
因为 C/C++ 分号是强制语句结束符,{} 宏加分号会语法爆炸,
必须用 do-while 补坑。
而 Rust 没有这个破问题:
do{...}while(0) 这种丑陋的语法补丁特性 | C++ | Go | Rust |
|---|---|---|---|
分号是谁写的? | 程序员强制手写 | 编译器自动插入 | 程序员按需写 |
分号用来干嘛? | 标记语句结束 | 编译器内部用,人类无感 | 丢弃返回值(语义) |
代码块 {} 后加分号 | ❌ 致命错误 | ✅ 自动处理,不会错 | ✅ 完全没问题 |
会有 else 匹配错误 | ✅ 高频出现(宏的根源) | ❌ 绝对不会 | ❌ 绝对不会 |
需不需要 do{...}while(0) | ✅ 必须用(修复宏 bug) | ❌ 不需要 | ❌ 不需要 |
; 作为语句结束标记,编译器才能正确解析代码?\t 统统被当作「无意义分隔符」,不参与语法解析。; 标记语句结束。{} 是复合语句,本身不是终结符;Go 保留了分号作为语法分隔符,但彻底交给编译器自动处理:
运行
a := 10
b := 20
// 编译器自动转为:a := 10; b := 20;
✅ 程序员不写分号,编译器内部补全,无任何语法风险。
Rust 从语法层面彻底废除了分号作为语句结束符:
; 被重新定义为 「丢弃表达式返回值」的语义符号 **,和语句结束无关。// 无分号 = 表达式(有返回值) fn add(a:i32, b:i32) -> i32 { a + b }
// 有分号 = 语句(丢弃返回值) let x = 10;
{...}; → 语法崩溃 → 必须用 do{...}while(0) 修复。知识地图:年度目标--沟通交流
目前状态:
换个方式:
最小行动: