Go sync/atomic 怎样用 atomic.Int64 记录有符号偏移量
来源:17golang原创
时间:2026-10-18 05:50:52 336浏览 收藏
如果多个 goroutine 共同维护一个可能增加、也可能减少的偏移量,可以把字段声明为 atomic.Int64,用 Add 接收正负 int64,用 Load 读取当前快照。它的零值就是 0,不需要额外初始化;但这个类型一旦参与并发访问,就不能再复制,也不能把它当成相关业务状态的万能锁。
官方文档:https://pkg.go.dev/sync/atomic
- 有符号偏移量可以直接用
Add(delta)表示前进和回退,正数与负数由调用方定义。 Load得到的是这个整数在某一时刻的原子快照;Store是无条件覆盖,Swap还会返回旧值。- 需要“读取、判断、写回”保持为一次条件决策时,用
CompareAndSwap,不要把普通读取和赋值拼在一起。
先把偏移量定义成可正可负的 int64
例如一个分片消费器会因为确认消息向前移动,也会因为回放或纠偏向后移动。这个量不是“永远递增的计数器”,而是相对基准的偏移量,因此用 int64 更贴合模型:
package main
import (
"fmt"
"sync/atomic"
)
type Cursor struct {
// offset 保存相对基准的有符号位置;零值表示没有偏移。
offset atomic.Int64
}
func (c *Cursor) move(delta int64) int64 {
// delta 可以为正,也可以为负;Add 返回更新后的值。
return c.offset.Add(delta)
}
func (c *Cursor) current() int64 {
// Load 只读取 offset,不会把 Cursor 的其他字段一起锁住。
return c.offset.Load()
}
func main() {
var c Cursor
fmt.Println(c.move(120), c.move(-20), c.current())
}
这里的关键不是把负数“转换”为无符号数,而是让业务约定直接体现在 delta 上:向前为正,回退为负。多个 goroutine 同时调用 move 时,每次 Add 都是针对这个整数的原子加法,调用方拿到的是本次更新后的值。

用 Add 和 Load 管理并发增减与读取
在实际接口里,最好把原子字段藏在小对象后面,只暴露“移动”和“读取”两个语义清楚的方法。这样调用方不会误以为读取快照与其他字段天然一致:
type Offset struct {
// value 只负责一个偏移量,相关元数据不要假定会被它一并保护。
value atomic.Int64
}
func (o *Offset) Advance(n int64) int64 {
// n 为负数时表示回退,仍由同一个原子加法完成。
return o.value.Add(n)
}
func (o *Offset) Snapshot() int64 {
// 读取快照不修改值;返回值可能在返回后立即过时。
return o.value.Load()
}
Load 适合日志、指标或下一次计算的输入,但它不是事务快照。比如你先 Load 再修改另一个字段,期间其他 goroutine 可能已经改变了偏移量;如果业务要求两个字段同时变化,就要把整体状态放进更合适的同步边界。
重置和替换时区分 Store 与 Swap
偏移量需要回到某个基线时,可以使用 Store。如果重置动作还需要知道旧值,例如记录“从哪里切换到哪里”,则用 Swap 直接取得旧值:
func (o *Offset) Reset(base int64) int64 {
// Store 无条件写入新基线,不关心旧值是什么。
o.value.Store(base)
return base
}
func (o *Offset) Replace(base int64) (old int64) {
// Swap 原子地写入 base,并把替换前的值交给调用方。
return o.value.Swap(base)
}
另外,atomic.Int64 的零值可以直接使用;它首次使用后不能复制。不要把含有已使用 atomic.Int64 的结构体按值传递、放进会产生副本的容器,或通过返回值复制它。共享对象通常应使用指针传递。
需要覆盖边界时用 CompareAndSwap
如果偏移量不能低于下界,单独的 Load 加 Add 不够:两个调用者都可能先看到同一个旧值。此时把“旧值仍然符合预期”和“写入候选值”交给 CompareAndSwap:
func (o *Offset) MoveIfAllowed(delta, min int64) (int64, bool) {
for {
old := o.value.Load()
next := old + delta
// 先拒绝越过业务下界的候选值,避免写入无效偏移。
if next
这个循环把条件判断和替换连接成一次原子决策:如果别的 goroutine 在中间改过值,CAS 会返回 false,代码重新读取最新值。这里的加法仍要遵守 int64 的业务范围,不应把溢出当作正常的偏移策略。

最后确认 atomic.Int64 的责任边界
atomic.Int64 只负责这个 int64 的原子读写与更新。它不会自动保护旁边的状态,也不会让“读取偏移、取出任务、更新任务状态”变成一个整体事务。需要多个值保持一致时,应重新设计临界区或把状态组合为一个可整体替换的对象。
| 需求 | 调用 | 语义 |
|---|---|---|
| 向前或回退 | Add(delta) | 原子加上有符号增量并返回新值 |
| 读取当前值 | Load() | 获得单个整数的原子快照 |
| 无条件重置 | Store(value) | 直接覆盖旧值 |
| 替换并记录旧值 | Swap(value) | 原子写入并返回旧值 |
| 满足条件才更新 | CompareAndSwap(old, new) | 旧值匹配时才替换 |
相关问题
atomic.Int64 的零值能直接使用吗?
可以。官方文档把零值定义为 0,声明一个未初始化的 atomic.Int64 就能调用 Add 或 Load。
为什么不能复制已经使用过的 atomic.Int64?
它内部维护着原子状态和不可复制标记;首次使用后复制会破坏调用方对同一共享状态的理解,因此应通过指针共享外层对象。
Add 能不能防止偏移量超过业务范围?
不能。Add 只负责原子加法;范围校验需要使用 CompareAndSwap 之类的条件更新,或者在更大的同步边界内完成。
-
151 收藏
-
101 收藏
-
323 收藏
-
428 收藏
-
143 收藏
-
437 收藏
-
478 收藏
-
367 收藏
-
230 收藏
-
499 收藏
-
330 收藏
-
385 收藏
-
318 收藏
-
163 收藏
-
244 收藏
-
118 收藏
-
433 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习