我们平时在 Go 开发里调用 `unique.Make` 时,经常会碰到入参为空、输入类型不匹配这类边界情况,很多人踩过不少意料外的坑,实际这个函数对空值和异常分支的处理逻辑是有明确规则的,顺着设计思路走就能避开绝大多数问题。
只要入参不是非导出类型、不存在循环引用,哪怕传入 nil 空指针、空切片空map,`unique.Make` 都能正常生成全局唯一的实例标识,不会直接panic,异常情况只会返回对应类型的零值和明确的错误信息。
我第一次把 unique.Make 放进值对象代码时,真正容易混淆的不是“怎么调用”,而是空值和异常到底应该由谁处理:空字符串是不是错误?nil 指针能不能规范化?切片传进去会不会得到一个 error?答案可以先记成一句话:unique.Make 负责把可比较值规范化成 Handle,它不负责业务校验,也没有 error 返回值。
本文围绕 Go 1.23 引入的 unique 包,把输入校验、空值表示、编译期类型边界和句柄生命周期串成一条工作流。官方资料入口如下,复制后可以直接打开:
官方资料:https://pkg.go.dev/unique、https://go.dev/doc/go1.23、https://go.dev/blog/unique
先确认 unique.Make 的职责和返回值
unique.Make[T comparable](value T) Handle[T] 的关键约束是 T comparable。它把相等的输入映射到相等的句柄;两个 Handle[T] 的比较通常比直接比较较长的值更便宜。句柄通过 Value() 取回产生它的值的浅拷贝,而且 Make 与 Value 都可以被多个 goroutine 并发使用。
这里有一个很重要的职责分界:
| 问题 | 应该由谁处理 | 结果 |
|---|---|---|
| 类型是否满足 comparable | Go 编译器 | 不满足时编译失败 |
| 空字符串是否允许 | 业务输入校验 | 允许就继续,不允许就返回业务 error |
| 两个值是否规范化为同一身份 | unique.Make | 得到可比较的 Handle |
| 规范化值如何读取 | Handle.Value | 得到 T 的浅拷贝 |
因此,看到标题里的“异常分支”时,不要先去寻找一个不存在的 err。先判断这个分支是类型错误、业务无效,还是后续存储或解析失败。
空字符串和 nil 指针可以直接处理
空值不等于异常。字符串的空字符串是合法的 string 值,指针的 nil 也是合法的指针值;它们都可以比较,所以可以交给 unique.Make。是否把它们判为“无效输入”,取决于你的业务语义。
package main
import (
"fmt"
"unique"
)
type User struct {
ID string
}
func main() {
// 空字符串是 string 的合法零值;两个相等输入会得到相等句柄。
emptyA := unique.Make("")
emptyB := unique.Make(string([]byte{}))
fmt.Println("empty handles equal:", emptyA == emptyB)
// 指针类型可比较;nil 可以作为一种明确的“没有对象”状态。
var first *User
var second *User
firstHandle := unique.Make(first)
secondHandle := unique.Make(second)
fmt.Println("nil handles equal:", firstHandle == secondHandle)
fmt.Println("value is nil:", firstHandle.Value() == nil)
}
这个例子里,emptyA.Value() 会得到空字符串,firstHandle.Value() 会得到 nil 指针。Value 是浅拷贝,因此对于指针、切片内部元素或含引用的结构体,不要把它理解成深复制。
实践中我会先写清楚零值的含义:
- 空字符串表示“匿名键”或“默认键”时,可以规范化并正常参与去重。
- 空字符串表示“用户没有填写必填字段”时,应在 Make 之前返回业务错误。
nil指针表示“没有关联对象”时,可以作为一个稳定状态。nil指针表示数据损坏时,应在进入规范化层之前拒绝。
先看 comparable 边界,再讨论异常
unique.Make 的类型参数不能是切片、map 或函数,因为这些类型不能作为普通的可比较值参与这种规范化。这个错误首先表现为编译错误,而不是运行时返回的 error。
package main
import "unique"
func main() {
// []byte 不满足 comparable;这一行不能通过编译。
// _ = unique.Make([]byte(nil))
// map 和 func 也不能作为 unique.Make 的 T。
// _ = unique.Make(map[string]int(nil))
// _ = unique.Make(func() {})
}
如果要把一段可变数据用于规范化,先把它转换成稳定的可比较表示,例如把字节序列转成字符串,或定义一个只包含可比较字段的结构体。转换过程可能涉及编码错误、长度限制和内存开销,这些才是应用需要显式设计的异常分支。
还要特别小心接口类型:接口本身能否进入泛型约束,不等于它装入的动态值一定适合你的规范化策略。工程上更稳妥的做法是让公共函数接收具体的可比较类型,少把 any 作为身份键的入口;如果必须接收接口,就在调用前明确检查动态值和允许的类型集合。
把业务校验放在 Make 之前
假设系统把用户输入转换成规范化的字符串键,空白键不允许进入索引。正确的写法是先校验,再调用 unique.Make;不要期待 Make 替你判断“空字符串是否有业务意义”。
package keys
import (
"errors"
"strings"
"unique"
)
var ErrEmptyKey = errors.New("key is empty")
func CanonicalKey(raw string) (unique.Handle[string], error) {
// 业务规则先于规范化:空白键被拒绝,而不是交给 Make 产生一个特殊错误。
key := strings.TrimSpace(raw)
if key == "" {
return unique.Handle[string]{}, ErrEmptyKey
}
// Make 只做可比较字符串的规范化;它本身不返回 error。
return unique.Make(key), nil
}
调用方可以据此把流程分成两类结果:返回 ErrEmptyKey 是业务拒绝;返回非空句柄则表示输入已经通过本层规则。后续写数据库、访问网络或解析文件产生的错误,继续沿着各自的 error 返回链处理,不要把它们混成 unique.Make 的异常。
如果空字符串在你的系统中就是合法键,那么可以去掉这段拒绝逻辑,让 unique.Make("") 参与正常的身份比较。关键不是“所有空值都要报错”,而是让规则在调用边界显式出现。
按完整工作流保留句柄和结果
从目标边界到生命周期,我建议按下面的顺序组织代码:
- 确认输入模型:优先使用具体的 comparable 类型,决定空字符串、nil 或默认值是否有业务含义。
- 完成前置校验:处理空值、格式、长度和权限;失败直接返回可识别的业务 error。
- 生成句柄:通过
unique.Make得到Handle[T],用句柄作为轻量身份。 - 保存必要状态:如果后续还要取回规范化值,就保留句柄;不要只保存一次
Value()后误以为它仍由 unique 永久维护。 - 读取和比较:用
h1 == h2判断身份,用h.Value()读取值;如果值包含引用,继续遵循浅拷贝语义。 - 处理后续失败:数据库、序列化或网络层的错误由对应层返回,不要重试一个已经成功完成规范化的句柄。
这里还有一个容易被忽略的生命周期点:官方介绍说明,句柄存在时,unique 内部会保留对应的规范化副本;当指向该值的句柄都不再存在时,内部条目才有机会在之后被回收。因此,它适合做有生命周期的身份去重,不应被当成永不释放的全局字符串池。
常见误区和速查表
| 写法或想法 | 问题 | 建议 |
|---|---|---|
| 期待 Make 返回 error | 函数签名没有 error | 把业务校验写在调用前 |
| 把空字符串一律当异常 | 空值是否有效是业务语义 | 先定义空值含义,再决定放行或拒绝 |
| 直接传入切片或 map | 类型不满足 comparable | 改用字符串或可比较结构体表示 |
| 只保存 Value,不保存 Handle | 忽略句柄的保留作用 | 需要稳定规范化生命周期时保留 Handle |
| 把 Value 当成深拷贝 | Value 返回浅拷贝 | 对内部指针或引用数据继续设定所有权规则 |
最后把判断压缩成三步:类型先过 comparable,业务再判空值,成功后才用 Handle 做身份比较。这样处理时,空字符串和 nil 不会被无差别地当成异常,真正的类型错误会在编译阶段暴露,真正的业务错误也会保留清晰的返回路径。
相关问题
unique.Make 可以用于切片去重吗?
不能直接把切片作为 T。可以先把内容编码成稳定字符串,或定义可比较的值对象,再调用 unique.Make;编码失败和规范化成本需要由业务代码承担。
空字符串调用 unique.Make 会报错吗?
空字符串是可比较的 string 值,调用本身不会产生 error。是否拒绝它,取决于调用前的业务校验。
Handle.Value 返回的是原对象吗?
它返回产生句柄的 T 值的浅拷贝。对于指针或含引用字段的结构体,仍然要单独设计可变数据的所有权。

Go unique.Make 怎样组合到一个小型工具中