Contents

布尔参数的罪恶:为什么你的函数签名里不该出现 true

一个 true 引发的血案

你一定见过这样的代码:

1
processOrder(order, true)

第一次看到这行代码时,你的脑子里会冒出一个问号:那个 true 是什么意思? 是跳过校验?是异步执行?还是发送通知?不看函数定义根本猜不出来。

更可怕的是,三个月后你半夜被叫醒排查线上问题,打开代码看到满屏的 processOrder(order, true)processOrder(order, false),没有任何注释,函数定义在另一个文件里。这一刻,你会深刻理解什么叫"代码写出来是给人看的"。

这不是编造的场景。我在真实项目中见过 sendEmail(to, subject, body, true, false, true) 这样的函数调用。六个布尔参数,含义全靠猜。

布尔参数的三宗罪

第一宗罪:调用处丧失语义

布尔参数最大的问题是在调用处丢失了参数名。函数定义时你写了 func CreateAccount(name string, sendEmail bool),看起来还算清楚。但调用时变成了 CreateAccount("alice", true),“true” 和 “sendEmail” 之间的关联完全断裂。

对比一下 Python 的情况。Python 支持关键字参数,调用时可以写 create_account("alice", send_email=True),可读性立刻好了一个量级。但 Go、Java、C# 这些语言没有这个语法糖,布尔参数就是一颗 readability 地雷。

第二宗罪:扩展时的参数爆炸

一开始只有一个布尔参数,后来产品说"加个是否记录日志的选项",再后来"加个是否走异步队列的选项"。参数列表从 3 个变成 5 个、7 个,其中 4 个是布尔值。

1
2
3
4
5
// 从这样
func SendNotification(user *User, message string) error

// 进化成这样
func SendNotification(user *User, message string, async bool, retry bool, log bool, useQueue bool) error

调用处变成 SendNotification(user, "欢迎", true, false, true, false),修改任何一个布尔值都要数手指头确认位置。

第三宗罪:合并本不该合并的逻辑

布尔参数本质上是在说"一个函数干两件事"。true 走 A 路径,false 走 B 路径。这违反了单一职责原则。两个路径的测试用例排列组合后翻倍,维护成本也随之倍增。

四种替代方案

方案一:用枚举替代布尔值(最简单)

最直接的改法是把布尔值换成有意义的枚举:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type DeliveryMode int

const (
    DeliverySync  DeliveryMode = iota // 同步发送
    DeliveryAsync                     // 异步发送
)

func SendNotification(user *User, message string, mode DeliveryMode) error {
    switch mode {
    case DeliverySync:
        return sendSync(user, message)
    case DeliveryAsync:
        return sendAsync(user, message)
    }
    return fmt.Errorf("unknown delivery mode: %d", mode)
}

调用处变成 SendNotification(user, "欢迎", DeliveryAsync),含义一目了然。即使不看函数定义,光看枚举名就知道在干什么。

方案二:拆成独立函数(最干净)

如果两个分支的逻辑差异较大,直接拆成两个函数更好:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 之前:一个函数两个布尔参数
func ProcessPayment(payment *Payment, isRecurring bool, dryRun bool) error

// 之后:拆成语义明确的独立函数
func ProcessOneTimePayment(payment *Payment) error {
    // 一次性支付逻辑
}

func ProcessRecurringPayment(payment *Payment) error {
    // 循环扣款逻辑
}

func ValidatePayment(payment *Payment) error {
    // 等价于 dryRun=true 的校验逻辑
}

好处是什么?调用处的意图无比清晰:ProcessRecurringPayment(payment) 不需要任何额外注释。

方案三:选项模式(Go 的惯用做法)

当参数确实需要灵活组合时,选项模式是 Go 社区的标准答案:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type SendOption func(*sendConfig)

type sendConfig struct {
    async    bool
    retry    int
    useQueue bool
}

func WithAsync() SendOption {
    return func(c *sendConfig) { c.async = true }
}

func WithRetry(times int) SendOption {
    return func(c *sendConfig) { c.retry = times }
}

func WithQueue() SendOption {
    return func(c *sendConfig) { c.useQueue = true }
}

func SendNotification(user *User, message string, opts ...SendOption) error {
    cfg := &sendConfig{} // 默认值
    for _, opt := range opts {
        opt(cfg)
    }
    // 根据 cfg 执行发送逻辑
    return doSend(user, message, cfg)
}

调用处变成这样:

1
2
3
4
5
// 异步 + 重试3次
SendNotification(user, "欢迎", WithAsync(), WithRetry(3))

// 默认同步,无重试
SendNotification(user, "验证码")

每个选项的名字就是语义,不需要猜,不需要数位置。新增选项不影响旧调用,接口天然向后兼容。

方案四:配置对象(参数多时用)

当配置项真的很多时,直接用一个结构体更直接:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type NotificationConfig struct {
    Async    bool
    Retry    int
    UseQueue bool
    Timeout  time.Duration
}

func SendNotification(user *User, message string, cfg NotificationConfig) error {
    // ...
}

调用时用具名字段初始化,Go 的结构体字段名天然充当了"参数名":

1
2
3
4
5
SendNotification(user, "欢迎", NotificationConfig{
    Async:   true,
    Retry:   3,
    Timeout: 5 * time.Second,
})

什么时候布尔参数是 OK 的

不是说布尔参数一律不能用。以下场景中布尔参数是合理的:

  1. 参数名本身就是布尔语义:如 strings.Contains(s, substr) 返回 bool,这不是参数而是返回值,不存在可读性问题。
  2. 只有一个布尔参数且语义极其明确:如 flag.Parse() 内部的 exitOnError bool,虽然不够完美但可以接受。
  3. 内部私有函数:不对外暴露的辅助函数,调用处就在眼前,可读性不是问题。

判断标准很简单:合上函数定义文件,光看调用处能不能理解参数含义? 如果不能,就该重构。

总结

方案 适用场景 优点 缺点
枚举替代 布尔值代表互斥模式 改动最小,语义清晰 只能替代单个布尔值
拆分函数 两条逻辑路径差异大 职责单一,调用清晰 函数数翻倍
选项模式 参数可组合,需默认值 灵活,向后兼容 代码量较多
配置对象 参数多且需灵活配置 直观,易序列化 没有默认值需自己处理

下次写函数签名时,手停在 bool 上多想一秒:这个 true/false,三个月后的自己还认得出来吗?如果不确定,换一种方式。你的同事——以及未来的你——会感谢这个决定。