登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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 都是针对这个整数的原子加法,调用方拿到的是本次更新后的值。

Go atomic.Int64 通过 Add 和 Load 管理有符号偏移量的静态结构图
图1:有符号偏移量结构说明图,展示 Cursor、atomic.Int64、正负 delta 与 Load 快照的静态关系;这是原创说明图,不是运行截图。

用 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 的业务范围,不应把溢出当作正常的偏移策略。

Go atomic.Int64 使用 CompareAndSwap 表达偏移量范围约束的静态结构图
图2:边界条件结构说明图,展示当前值、候选值、下界和 CompareAndSwap 之间的静态关系;这是原创说明图,不代表真实运行结果。

最后确认 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 之类的条件更新,或者在更大的同步边界内完成。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>