Go 里一直没有三元运算符,因此“没有值就用默认值”这类逻辑,过去通常只能写成一段段 if-else。到了 Go 1.22,标准库新增 cmp.Or,把这个高频场景收成了更短的表达式。
不过它并不只是“更省代码”这么简单:cmp.Or 判断的是零值,不是很多人直觉里的“空”或 nil。这篇文章会从语法、典型用法、边界条件到实现原理逐步拆开,帮助你判断它什么时候值得用,什么时候该继续保留显式判断。
cmp.Or 到底解决了什么问题
cmp.Or 是 Go 1.22 在 cmp 包中提供的新函数,用来返回参数列表里第一个非零值。
它的核心思路很直接:按从左到右的顺序检查多个同类型参数,遇到第一个不是零值的参数就立刻返回;如果所有参数都是零值,就返回该类型的零值。
基本语法
func Or[T comparable](vals ...T) T这里的 T 必须是可比较类型,也就是支持 == 和 != 的类型。常见可用类型包括:
- 布尔类型
- 数字类型,如
int、float64 - 字符串
- 指针
- 通道
- 接口
- 结构体(前提是所有字段都可比较)
- 数组(前提是元素类型可比较)
如果你熟悉其他语言,可以把它理解成一种“取第一个有效候选值”的标准库写法,只不过 Go 没有把它做成运算符,而是提供了函数形式。
常见场景里,cmp.Or 怎么替代 if-else
真正让 cmp.Or 有价值的,不是概念本身,而是它能把重复出现的默认值判断收短,同时保留清晰的意图表达。

字符串默认值
比如用户注册时允许填写昵称,没填就使用默认展示名。
传统写法通常是这样:
func GetDisplayName(nickname string) string {
if nickname != "" {
return nickname
}
return "匿名用户"
}换成 cmp.Or 后,意图会更集中:
func GetDisplayName(nickname string) string {
return cmp.Or(nickname, "匿名用户")
}这里的好处不是“少写几行”这么简单,而是读代码的人一眼就能看出:这是在做默认值兜底,而不是处理更复杂的分支逻辑。
多层回退值
当配置来源不止一个时,cmp.Or 会更有优势。比如优先使用用户配置,其次使用环境变量,最后再退回硬编码默认值。
传统写法:
func GetConfig() string {
if userConfig != "" {
return userConfig
}
if envVar != "" {
return envVar
}
return "default"
}使用 cmp.Or 后可以压缩成一行:
func GetConfig() string {
return cmp.Or(userConfig, envVar, "default")
}这种从“高优先级到低优先级”的回退顺序,本身就和 cmp.Or 的左到右扫描机制高度一致,因此非常适合用它表达。
数值类型也能用,但要先确认 0 的含义
cmp.Or 不只适合字符串,数值类型同样适用:
// 设置超时时间,0 表示使用默认值
timeout := cmp.Or(userTimeout, 30*time.Second)
// 设置端口号,0 表示使用默认端口
port := cmp.Or(configPort, 8080)但这里也正好引出一个关键前提:对于数值类型,0 就是零值。也就是说,只有当业务语义明确规定“0 代表未设置”时,这种写法才成立。
使用前先看清这几个边界条件
cmp.Or 上手很快,但很多误用也都集中在几个固定位置。尤其是“零值”和“有效值”混在一起时,代码虽然短了,语义反而可能出错。

1. 它判断的是零值,不是单纯的 nil
cmp.Or 的判断标准是“是否等于该类型零值”,这和很多人以为的“是否为 nil”并不一样。不同类型的零值分别是:
- 字符串是
"" - 数字是
0 - 布尔是
false - 指针是
nil - 切片的零值是
nil
因此,只要你的业务里零值本身可能是一个合法输入,就要格外小心。
// 问题场景:0 可能是有效值
count := cmp.Or(userCount, 0) // 如果 userCount 是 0,会返回 0 吗?
// 答案:会,因为第二个参数 0 也是零值这个例子看上去没错,但也说明了一个现实:如果你想区分“用户明确传了 0”和“用户根本没传值”,cmp.Or 并不能替你表达这个语义,往往需要额外的状态位、指针或显式判断。
2. 参数类型必须一致
所有传入 cmp.Or 的参数都必须是同一种类型,不能混用:
// 错误示例
result := cmp.Or("default", 0) // 编译错误:类型不匹配
// 正确示例
result := cmp.Or("user_input", "default")这是泛型带来的类型安全优势,同时也是它的使用约束。好处是问题会在编译期暴露,而不是拖到运行时。
3. 可读性不能只看行数
cmp.Or 的目标是让默认值逻辑更清楚,但如果写法开始嵌套,就可能适得其反。
// 不推荐:过度嵌套
result := cmp.Or(
cmp.Or(a, b),
cmp.Or(c, d),
)
// 推荐:扁平化使用
result := cmp.Or(a, b, c, d)一个实用判断标准是:当你的逻辑本质上还是“按优先级依次取第一个有效值”,就适合用 cmp.Or;如果已经掺杂条件分支、转换、校验、副作用,那还是显式写开更稳妥。
4. 性能通常不是问题,但极端场景仍应实测
cmp.Or 由泛型实现,日常业务代码里和手写 if-else 的性能差异基本可以忽略。对绝大多数 Web 服务、工具程序和普通后端逻辑来说,它不是性能瓶颈。
只有在极度性能敏感的路径里,比如高频交易、游戏引擎核心循环这类场景,才值得用基准测试确认是否存在可见差异。是否采用,应该由数据决定,而不是凭感觉判断。
实现原理很简单,但能解释大多数行为
理解 cmp.Or 的内部逻辑后,前面那些使用限制会变得非常直观。

// cmp 包的简化实现
package cmp
func Or[T comparable](vals ...T) T {
var zero T
for _, v := range vals {
if v != zero {
return v
}
}
return zero
}它的执行过程可以概括为四步:
- 先创建一个类型
T的零值zero - 按顺序遍历所有参数
- 遇到第一个不等于
zero的值就返回 - 如果全都等于零值,就返回零值
也正因为实现是这样,才会出现前面提到的那些特性:它必须要求类型可比较,必须保证参数同类型,而且天然会把 0、""、false、nil 等都视为待跳过的“空位”。
和其他语言相比,Go 的做法差在哪
很多语言都提供过类似能力,只是形式不同:
JavaScript:
??运算符const name = nickname ?? "匿名用户";C#:
??运算符var name = nickname ?? "匿名用户";Kotlin:
?:运算符val name = nickname ?: "匿名用户"
不过要注意,其他语言里的空值合并运算符,很多时候重点在“空引用”或“null”,而 Go 的 cmp.Or 是以“零值”为判断基础。两者看起来接近,语义却并不完全相同。
这也是它最容易被误解的地方:它确实像三元表达式的替代方案,但更准确地说,它是 Go 里“按零值规则选第一个有效值”的工具函数。
什么时候该用,什么时候别硬用
把 cmp.Or 用好,关键不在于到处替换 if-else,而在于选对场景。
适合使用的情况通常有这几类:
- 默认值兜底逻辑很明确
- 多个候选值之间只是简单的优先级回退
- 零值在当前业务里恰好代表“未设置”
- 你希望把“取第一个有效值”的意图写得更直接
不太适合的情况也同样明确:
0、false、空字符串本身就是合法业务值- 分支里包含额外校验、转换或副作用
- 嵌套后反而让阅读路径变复杂
综合来看,cmp.Or 是 Go 1.22 带来的一个很实用的小工具。它真正的价值不在于“像不像三元运算符”,而在于能把一类重复、机械、语义固定的判断压缩成更稳定的表达方式。只要先确认零值语义和参数类型,再把它用在合适的位置,代码通常会更短,也更好读。









