Golang Go语言结构体字段被异常修改的问题,大家帮我看看
简单描述,就是在内存保存数据,在创建和查询过程中,某些字段的值会在查询时意外的被改变,改变的方式也很奇怪。
例如存在一个结构体 Task 和一个全局变量 list:
var list sync.Map
type Task struct {
ID int64
Name string
User string
}
创建并把 task 保存在全局变量 list 中;
task := Task {
ID: now.UnixMicro(),
Name: "agent-web",
User: "wangds",
}
list.Store(task.Name, task)
执行查询时,task 的值可会意外的改变,发生概率盲猜有 0.1-0.4 ; 而且每次更改代码后,只遵循以下 5 种改变模式中的 1 种:
{
ID: 1677200690411702,
Name: "agent-web",
User: "agent-",
}
{
ID: 1677200690411702,
Name: "1gent-web",
User: "wangds",
}
{
ID: 1677200690411702,
Name: "agent-web",
User: "1angds",
}
{
ID: 1677200690411702,
Name: "agent-web",
User: "167720",
}
{
ID: 1677200690411702,
Name: "167720069",
User: "wangds",
}
全局变量试过其他类型,比如 map 、slice ,还试过一个第三方的内存缓存工具 ristretto ,都有这个问题。
Golang Go语言结构体字段被异常修改的问题,大家帮我看看
更多关于Golang Go语言结构体字段被异常修改的问题,大家帮我看看的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
逻辑都没写全,怎么判断哪里有问题……
更多关于Golang Go语言结构体字段被异常修改的问题,大家帮我看看的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
所有更新数据的地方打 LOG 嘛,很快就能找到。
文章底部有代码,可以复现
有在协程里持续打印,发现是查询的一瞬间改变的,但是不知道为什么会改变
帮你看看 != 帮你 review 整个项目,你发个 repo 的链接是要闹哪样
使用内存地址而不是值试试
不是整个项目啊,是创建和查询的最小实现。
我再试试,当时好像也试过指针
另外,你这个 list 和 list2 是 package 私有变量,不是全局变量
你的 map key 为什么是 Name 而不是 ID ?
我记得用 ID 也会变
是要把 list 和 list2 放到 main 包里吗
你这创建 task 的时候,判断同名的 task 是否存和创建 task 不是原子操作吧
model 里创建 func init(){
}
在 main 里面使用 import _ “web-deploy-task-manage/model”
应该是线程不安全,所以要使用读写锁
以前有个版本是加了锁的,也有这个问题。我给代码加个延时试试
老弟问题能不能在一个文件里面复现
创建方法里查,判断同名代码后加了个延时;测试的代码里每次创建、查询、循环之间都加了延时;
肉眼可看的一个一个蹦日志,也会出现问题,哭了
#17 你加锁 所有写操作的地方都要加锁
我试试
是的,当时是读、写都加了锁,当时用的 sync.RWMutex
在 model 写个能复现问题的单元测试吧,实在看不懂什么叫能复现
看了看,楼上说了的,包级别的全局变量最好通过 Init() 来初始化。
还有 sync.Map 适用的场景你怕不是根本就没思考过,你这么写,最起码也得用读写锁 + map
用 map 来管理,我看你还有更新值的操作,你不存指针,你想怎么更新 map 里面的值?
你这一通操作,*Task 是不安全的,把你的 map 加好锁吧。读写锁估计都没用,你几个方法都有写操作
- https://gitee.com/tianshuapp/web-deploy-task-manage/blob/master/services/task.go#L19-39
改成一个方法 GetOrCreate ,内部加锁
model 下面的方法加锁。不要想着先读取,所以加一个读写锁,读完了释放。然后再加写锁,去更新。
这期间,你的 *Task 都变了。。。
还有返回全部内容的方法,返回的数据是不能有指针的,除非和上面一样加锁。
我下午再优化改一下,感觉受益良多
如果是数据竞争导致的,写个单测, go test -race
很容易看出来
哇塞,我去看看
更新了一下:
不再缓存指针了;
代码放到单文件里了,init 函数初始化全局变量;
map 的 key 改为 id ;
担心 id 太长,现在从 1 自增;
加了读写锁,且测试加了延时;
现在代码精简了,创建请求只涉及创建,没有查询了;目前只有创建、查询两种请求操作;
通过go run -race main.go
来执行程序,没有报任何异常;
字段异常修改的问题依然存在。
我在 main 方法的协程里直接测试,就一切正常,请求通过 gofiber 就会有问题。
破案了,代码增加了 gin 框架模式,在 gin 下就正常,在 fiber 下就异常。
感谢大家的帮助!
我试试
fiber 的 Context 会复用,见 fiber 文档首页“Zero Allocation”章节,https://docs.gofiber.io/#zero-allocation
你从*fiber.Ctx 拿数据的时候得 memory copy ,https://gitee.com/tianshuapp/web-deploy-task-manage/blob/master/main.go#L98
user := c.Query(“user”, “anonymous”)
arch := c.Query(“arch”, “”)
改成
user := utils.CopyString(c.Query(“user”, “anonymous”))
arch := utils.CopyString(c.Query(“arch”, “”))
或者 fiber 全局配置添加
app := fiber.New(fiber.Config{
Immutable: true,
})
并发下确实会报 DATA RACE ,我看看楼下的方法
我试试
加了 Immutable: true 正常了,拜谢!!
怪不得有人不推荐 fiber 优化玩的太狠了
task := Task{
ID: idCounter,
//ID: 1677200690411702,
Name: strings.Clone(name),
User: strings.Clone(user),
Stats: StatRunning,
Message: “”,
Arch: strings.Clone(arch),
CreateTime: &now,
UpdateTime: nil,
DoneTime: nil,
Expires: expires,
Deleted: false,
} debug 了下,虽然没去看 fiber , 结论是一样的。 其实是和 string 的实现有关
看了下例子,有点不懂的地方想问下楼主和留言的大神。
sync.map{} 按照官方的描述就是并发安全的,而且内部实现也是加了 Mutex 锁,为啥请求中还加了读写锁呢?麻烦指教
var rwLock sync.RWMutex
// mode=2
var List2 sync.Map
…
// GetTaskByIDModel 查询 task
func GetTaskByIDModel(id int64) (Task, error) {
var task Task
var ok bool
rwLock.RLock()
defer rwLock.RUnlock(). // 此处还加读写锁是否多余呢?
if mode == 1 {
task, ok = List[id]
} else if mode == 2 {
v, o := List2.Load(id)
if o {
task, ok = v.(Task)
if !ok {
return Task{}, errors.New(“not found”)
}
} else {
log.Println(“从 sync.Map 中获取 task 失败”)
}
}
if !ok {
return Task{}, errors.New(“not found”)
}
return task, nil
}
我感觉应该不用再加锁了
针对您提到的Golang中结构体字段被异常修改的问题,这里有几个可能的原因及排查思路:
-
并发访问:如果在多个goroutine中并发访问和修改结构体字段,且没有使用适当的同步机制(如mutex),可能会导致数据竞争和字段异常修改。建议检查代码中的并发逻辑,并使用
sync.Mutex
或sync.RWMutex
来保护共享数据。 -
指针传递:如果结构体是通过指针传递的,任何对指针的修改都会影响到原始结构体。确保在传递结构体时,明确是否需要传递指针,以及这种传递方式是否会导致意外的修改。
-
方法接收器:如果定义了结构体的方法,并使用了值接收器而非指针接收器,但在方法内部修改了结构体的字段,这种修改不会影响到原始结构体(除非显式传递了指针)。然而,如果方法接收器使用不当,也可能引起混淆。检查方法定义,确保接收器类型与预期一致。
-
外部库或依赖:如果使用了第三方库来操作结构体,确保这些库的行为符合预期,没有意外地修改结构体字段。
建议从上述几个方面入手,逐一排查问题。同时,可以使用Go的race detector工具(通过运行go run -race
)来检测数据竞争问题。如果问题依旧存在,可以提供更具体的代码片段,以便进一步分析。