关于 UUID 的总体介绍可以查看这篇文章,其包含阅读此篇文章的前置内容。
UUID version 1 在 RFC 4122 文件中定义,其实现基于节点 ID、时钟序列以及当前时间(距离格里历改日【1582年10月15日】 的100纳秒数,具体介绍可以看Go 语言 UUID 库 google/uuid 源码解析:时钟信息)。
目前还没有详细的文章介绍节点 ID 的实现,但是可以知道的是,节点 ID 是利用网络接口硬件地址生成的,定义在 node.go 文件的 setNodeInterface 函数中。其逻辑大致如下:
- 如果你指定了网络接口的名称,则它回尝试获取该接口的硬件地址(即 MAC 地址)作为节点 ID。
- 如果没有指定,则选择第一个可用接口的硬件地址。
- 如果没有可用的硬件地址则会随机生成一个节点 ID。
UUID version 1 在 google/uuid 中的实现则定义在 version1.go 文件中。
函数接口
UUID version 1 定义的接口为 NewUUID()
,其返回值为 (UUID, error)
即返回 UUID 序列以及错误信息。其具体代码放在文章末尾,存在困惑的地方,可以看看源码。
具体实现
UUID 的存储结构
首先我们知道 UUID 实际是长 16 字节的序列,其表现是 32 个十六进制数。google 则是将 UUID 序列使用长 16 的字节切片进行存储。其实现如下:
- 首先在 uuid.go 文件中声明
type UUID [16]byte
将长 16 的字节切片起别名为 UUID,使其含义更加清晰。 - 然后在 version1.go 文件 NewUUID 函数中定义 uuid 变量供后续使用
var uuid UUID
获取时间与时钟序列
时间戳与时钟序列通过 GetTime()
函数直接获取。(GetTime()
的详细介绍可以看 Go 语言 UUID 库 google/uuid 源码解析:时钟信息)。得到两个变量 now
和 seq
,now, seq, err := GetTime()
。
分割时间信息
首先我们需要知道获取到的 now
类型为 int64
,即其二进制有 64 位,uuid 中的时间信息会被“切割”为三段:timeHi(16)、timeMid(16)、timeLow(32),具体“切割”如下:
xxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
timeHi /timeMid /timeLow
需要提起知道的是类似 x & 0xff
的代码用于保留低位比特值,抹除高位比特值。示例如下:
x: 10101010 11011011
0xff: 00000000 11111111
-------------------------
&: 00000000 11011011
“切割”实现代码如下:
// 低32位
timeLow := uint32(now & 0xffffffff)
// 高32位中的低16位
timeMid := uint16((now >> 32) & 0xffff)
// 高16位
timeHi := uint16((now >> 48) & 0x0fff)
上述代码详解如下:
- timeLow
now & 0xffffffff
:取 now(int64) 的低 32 位。uint32(x)
:将结果转为 uint32。
- timeMid
now >> 32
将 now(int64) 的高 32 位挪到低 32 位,高 32 位置 0。(x) & 0xffff
取当前(新)低 32 位中的低 16 位。uint16(x)
将结果转为 uint 16。
- timeHi
now >> 48
将 now(int64) 的高 16 位挪到低 16 位,高 48 位置 0。(x) & 0xfff
取当前(新)低 16 位中的低 12 位。uint16(x)
将结果转位 uint 16。
之所以 timeHi 只取到低 12 位,是因为需要保留 4 位作为标志位,此次是用于标识 UUID 版本。
我们需要提前知道的是:类似于 x |= 0x1000
的代码,使用于将某个特殊位置为 1 的,此次是将第 13 位(从右往左)置为1:
x: 00000011 00110011
0x1000: 00010000 00000000
--------------------------
|: 00010000 00110011
标识版本代码如下:
timeHi |= 0x1000 // 版本 1
将时间信息和时钟序列放置到 uuid 的正确位置
首先我们需要知道最终的 uuid 结构组成如何:
(🂓代表标志位)
十六进制字符数|8 |4 |4 🂓 |4 |12
二进制数 |xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|xxxxxxxxxxxxxxxx|xxxxxxxxxxxxxxxx|xxxxxxxxxxxxxxxx|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
具体含义 |timeLow |timeMid |timeHi |seq |clockSeq
我们只需要按照这个结构以大端序的方式(RFC 变体使用大端序)依此填入即可,实现代码如下:
// 将 timeLow 填充到前 4 个字节
binary.BigEndian.PutUint32(uuid[0:], timeLow)
// 将 timeMid 填充到第4和第5个字节
binary.BigEndian.PutUint16(uuid[4:], timeMid)
// 将 timeHi 填充到第6和第7个字节
binary.BigEndian.PutUint16(uuid[6:], timeHi)
// 将时钟序列填充到第8和第9个字节
binary.BigEndian.PutUint16(uuid[8:], seq)
填充 NodeID
时间和时钟序列填充完毕之后,最后只需填充 NodeID 即可。其基本逻辑为:
- 加锁
- 如果当前 nodeID 未设置,则通过 setNodeInterface 生成。
- 将 nodeID 拷贝至 uuid 的第10到最后一个节点。
- 解锁
实现代码如下:
nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
copy(uuid[10:], nodeID[:])
nodeMu.Unlock()
返回 uuid
最后返回填充好的 uuid 和 nil(error) 即可。return uuid, nil
到这里,完整的 UUID version1 源码解析便完成了,希望你能有所收获。
NewUUID 源码
func NewUUID() (UUID, error) {
var uuid UUID
now, seq, err := GetTime()
if err != nil {
return uuid, err
}
// 标志位
// 🂓
// xxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// timeHi /timeMid /timeLow
// 低32位
timeLow := uint32(now & 0xffffffff)
// 高32位中的低16位
timeMid := uint16((now >> 32) & 0xffff)
// 高16位
timeHi := uint16((now >> 48) & 0x0fff)
// 将第4位置为1,作为标志位,标志为版本号1
timeHi |= 0x1000 // 版本 1
// 8 /4 /4 🂓 /4 /12 /16进制字符数
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/二进制数
// timeLow /timeMid /timeHi /seq /clockSeq /具体含义
binary.BigEndian.PutUint32(uuid[0:], timeLow)
binary.BigEndian.PutUint16(uuid[4:], timeMid)
binary.BigEndian.PutUint16(uuid[6:], timeHi)
binary.BigEndian.PutUint16(uuid[8:], seq)
nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
copy(uuid[10:], nodeID[:])
nodeMu.Unlock()
return uuid, nil
}