Golang Go语言中遇到的一个奇怪问题,求教

发布于 1周前 作者 eggper 来自 Go语言

下面这段代码在长时间运行后,有一定的机率会出错,RandString(32)返回的全是 0.

从网上查的资料全局变量应该不会被回收才对。

package helper

import ( “math/rand” “time” )

const _charsetRand = “abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$”

var _seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))

// RandStringWithCharset rand string with charset func RandStringWithCharset(length int, charset string) string { b := make([]byte, length) l := len(charset) for i := range b { b[i] = charset[_seededRand.Intn(l)] } return string(b) }

// RandString rand string func RandString(length int) string { return RandStringWithCharset(length, _charsetRand) }

// RandInt rand int between [min, max) func RandInt(min int, max int) int { if min <= 0 || max <= 0 { return 0 }

if min &gt;= max {
	return max
}

return _seededRand.Intn(max-min) + min

}

// RandMax rand int between [0, max) func RandMax(max int) int { if max <= 1 { return 0 }

return _seededRand.Intn(max)

}


Golang Go语言中遇到的一个奇怪问题,求教

更多关于Golang Go语言中遇到的一个奇怪问题,求教的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

57 回复

不是线程安全的吧。

更多关于Golang Go语言中遇到的一个奇怪问题,求教的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


应该和线程安全没什么关系吧,我也不确定。

换成下面的代码这样,就不会出现返回全为 0 的情况了。

go<br>func createRand() *rand.Rand {<br><br> if _seededRand == nil {<br> _seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))<br> }<br><br> return _seededRand<br>}<br><br>// RandStringWithCharset rand string with charset<br>func RandStringWithCharset(length int, charset string) string {<br> b := make([]byte, length)<br> l := len(charset)<br> for i := range b {<br> b[i] = charset[createRand().Intn(l)]<br> }<br> return string(b)<br>}<br>

#2 你是怎么判断是否出现 0 的

这么改试试
go<br>package helper<br><br>import (<br> "math/rand"<br> "time"<br>)<br><br>const _charsetRand = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$"<br><br>var _seededRand *rand.Rand<br><br>func init() {<br> _seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))<br>}<br><br>// RandStringWithCharset rand string with charset<br>func RandStringWithCharset(length int, charset string) string {<br> b := make([]byte, length)<br> l := len(charset)<br> for i := range b {<br> b[i] = charset[_seededRand.Intn(l)]<br> }<br> return string(b)<br>}<br><br>// RandString rand string<br>func RandString(length int) string {<br> return RandStringWithCharset(length, _charsetRand)<br>}<br><br>// RandInt rand int between [min, max)<br>func RandInt(min int, max int) int {<br> if min &lt;= 0 || max &lt;= 0 {<br> return 0<br> }<br><br> if min &gt;= max {<br> return max<br> }<br><br> return _seededRand.Intn(max-min) + min<br>}<br><br>// RandMax rand int between [0, max)<br>func RandMax(max int) int {<br> if max &lt;= 1 {<br> return 0<br> }<br><br> return _seededRand.Intn(max)<br>}<br>

我们在预上线的测试环境上发现的,我们用它来生成 token ,突然发现 token 表里出现大量全是 32 个 0 这样的 token 。

#5 你的意思是换成新的方法在线上没有出现过 0 了?

换了写法后,好几年了,没出现过这种情况,不过后来我们又改了,改成用 crypto/rand 了,所以也不是 100%确定#2 的写法是不是对的。

go<br>package utils<br><br>import (<br> "crypto/rand"<br> "encoding/hex"<br>)<br><br>// RandomString random string<br>func RandomString(len int) (string, error) {<br><br> b := make([]byte, len/2)<br><br> _, err := rand.Read(b)<br><br> if err != nil {<br> return "", err<br> }<br><br> return hex.EncodeToString(b), nil<br>}<br>

crypto rand 用来生成 token 性能非常差的,建议别这么改

现在最新版本 Go 里面,rand 包的全局随机数生成器的随机种子也是每次自动生成的了,而且有自带的加速特性,可以考虑切回 rand 的全局随机数生成器试试会不会有这个问题。

目前最新的 Go 版本也会出错吗?

chatgpt

这段代码的问题在于并发访问了全局的 _seededRand 变量,导致了竞争条件( race condition )。在多个 goroutine 同时调用 RandStringWithCharset 函数时,它们可能会同时访问和修改 _seededRand ,从而导致不可预测的结果,甚至造成程序崩溃。

在第一段代码中,_seededRand 被多个 goroutine 同时访问和修改,因为没有对其进行同步操作或者使用互斥锁。而在第二段代码中,通过在 RandStringWithCharset 函数中调用 createRand 函数,每次都创建一个新的 _seededRand 实例,避免了并发访问全局变量的问题。

通过这样的修改,确保了在并发情况下每个 goroutine 都有自己的 _seededRand 实例,从而解决了竞争条件问题,确保了程序的稳定性。

rand.NewSource(time.Now().UnixNano())不是线程安全的。并发情况下会出现一些未定义的异常,比如 panic

go<br>charset[_seededRand.Intn(l)]<br>

我有点好奇,就算不是线程安全的,这代码也不该返回字符串"0"啊,"0"在整个 charset 里也没有处在一个特殊的位置。

https://pkg.go.dev/math/rand#NewSource
Unlike the default Source used by top-level functions, this source is not safe for concurrent use by multiple goroutines.

现在不需要手动初始化 seed 的

#13
参考 #11 的回答,如果发生静态条件,那这句会报错,赋值不会被完成,所以 b 数组全零。

感觉是这样

b 是一个 byte[], byte 的零值是 0 没错,但是经过 string() 类型转换后会变成 “ ” ,我特意试了一下。

也应该是 panic 退出,不应该继续执行

up 能说一下复现的方式跟 go 版本嘛? 我目前没法复现

这个是全局变量,只会在包引入时初始化一次

New 出来的 Source 不是线程安全的。如果是 rand 包导出的方法是线程安全的,因为里面的 source 是并发安全的

看了下全局用的是 lockedSource ,new 出来的是 rngSource ,使用确实要加锁

https://pkg.go.dev/math/rand#NewSource

NewSource returns a new pseudo-random Source seeded with the given value. Unlike the default Source used by top-level functions, this source is not safe for concurrent use by multiple goroutines. The returned Source implements Source64.

很难重现了,21 年的事情,应该是当时最新版本的 go ,但我当时在自己的电脑跑,都没办法重现。

只有在测试服务器上出现,而且不是一次;第一次出现的时候以为是服务器被黑了,后来才定位到这段代码。

/*
* Top-level convenience functions
*/

var globalRand = New(&lockedSource{src: NewSource(1).(Source64)})

这个 math/rand 的 IntN 在搞并发下运行会有概率 panic 的, 是不是上层有 recover 默认值 导致的 0 ?

使用go run -race命令检测这段代码,确实是存在竞态并发安全问题,具体是在b[i] = charset[_seededRand.Intn(l)]这一行代码处

靠,你这生成随机字符代码难怪看着眼熟,我前段时候从网络上 copy 的一份跟你的几乎一模一样,我得赶紧 fix 一下。copy 来源: https://github.com/BelphegorPrime/lib/blob/master/RandString.go

这里能展示一下, 真正写入 全是 0 ,上层的函数吗?
比如这个函数上面有 recover , 这个时候你们得到的 值可能是 空字符串, 但是在你们存入的时候, 这个空字符串经过一些 编码,比如 hex 这种, 就会变成 0000…000 但是是 64 位, 假设你存入的 varchar(32) 在不经过 mysql 严格模式下, 就能把 空字符串 变成 32 位 000… 然后误解,是这里生成的了。

当前上面也只是我的一种 推理可能性

想象一下同一 ns 情况下 两个 routine 调用了 你哪个不安全的 var , 得到的结果

遇到过差不多的问题,不过因为代码不多,很快就发现是 race condition
https://imgur.com/dp2lnEy

#28 现在肯定能够确认这段代码非线程安全的,会有数据竞争,出现 Panic
但是就是特别想知道为啥是 32 个 0 看是否是我上面描述的猜测

#2 NewSource 是线程不安全的,在并发下会 panic 。
这两种写法都用到 NewSource ,应该会报一样的错误

#19 Intn 方法会最终调用到 Source 的 Int 方法,所以最终是线程不安全的

回答竟然都是竞态并发安全问题, 你们真的是认真的吗

这样是解决不了本质问题的。因为 rand.NewSource 不是并发安全的。另外 createRand 中_seededRand 初始化的逻辑也不是并发安全的。你应该直接用全局函数 rand.Intn()。

这就是最大可能阿,难道不是并发问题?起码代码就不是并发安全
你是认真的吗?

对于 rand.NewSource 存在竞态导致 panic 这一点应该没有异议,而 panic 没有中断而是正常被调用,说明上层应该是存在 recover 逻辑的。结合 OP #23 指出本地正常而测试服务器异常,推测本地测试的生成逻辑,而服务器完成的是全部调用逻辑。

#16 指出 byte 空值转换为 string 之后是空字符串,说明在可见代码的部分,只可能产生空字符串。 和 判断出是上层调用将空字符串变成了全 0 字符串。

我这里做一点补充,一般要保证字符串定长都会做 padding ,用字符串 0 做填充是最常见的。

你的意思是现在重现不了了,2021 年能重现?是我理解错了吗?几年前的事情你现在才来问?

确实是几年前遇到的问题,当时解决起来也容易,换种写法就可以了,只是到目前为止还是没搞清楚是因为什么,所以来问了。

说是返回 32 个 0 可能也不是绝对准确的,也可能只是注意到了全是 0 的 token ,因为它最明显。

func main() {
var wait sync.WaitGroup
for i := 0; i < 100; i++ {
wait.Add(1)
go func() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
defer wait.Done()
for j := 0; j < 100; j++ {
helper.RandString(10)
}
}()
}

wait.Wait()
}

运行这个程序,可以看到有的 goroutine 出现了 painc

我也是从网上搜的,印像中 golang random string 它排最前面.

恕我直言,我不认为几年前的问题你今天还能根据记忆还原出真相。除非你现在依然能重现它。

  1. 现在的 go1.22.2 版本,并发调用 Intn 并不会 panic ,源码里就没有相关的检查
    2. 当 go 协程调用 _seededRand 的时候,程序的运行时已经将 _seededRand 初始化完成,并不存在说你加了 if _seededRand == nil {…init…} 就会好的情况
    3. 几年前的问题了,你也不知道当时具体是什么 go 版本,现有的数据不足以用来判断

rand.NewSource 记得会有读写过程,你多个协程并发会有 data race

当时猜可能是因为_seededRand 被回收了,所以加了个判断。

因为知道自己是猜的,所以心里不踏实,一段时间后改成用 crypto/rand 。

crypto/rand 倒是没出过问题,只是生成出来的字符串中没有大写的字母,有点奇怪。

说 crypto/rand 性能很差,因为只是生成 token 的时候用到,还未成为优先问题。

rand 不是线程安全的

所有顶层方法都是并发安全的,Source 是并发不安全的

怎么个不安全法?我看源码也没问题啊,没有改 seed ,也没有竞争

亲测并发会 panic ,楼主说的返回 32 个 0 可能是自己的业务代码做了默认值处理吧

是怎么 panic 的,可以贴个代码吗

https://go.dev/play/p/GaXBGyvGkEn 我试出来了,确实会 panic ,Source 的 Uint64 方法内部会索引一个定长的数组,并发的情况下可能会出现索引越界的情况

https://imgur.com/QsyRcDU

rand 中 Source 的注释:
// A Source represents a source of uniformly-distributed
// pseudo-random int64 values in the range [0, 1<<63).
//
// A Source is not safe for concurrent use by multiple goroutines.
NewSource 方法的
// NewSource returns a new pseudo-random Source seeded with the given value.
// Unlike the default Source used by top-level functions, this source is not
// safe for concurrent use by multiple goroutines.
// The returned Source implements Source64.

在Go语言中遇到奇怪问题确实可能会让人困惑,不过别担心,让我们一步步来分析。

首先,请确保你遇到的问题是可复现的,并尝试在一个最小化的代码示例中重现它。这有助于排除其他潜在干扰因素,并更清晰地展示问题所在。

其次,检查你的Go语言环境。确保你使用的是最新稳定版本的Go编译器和工具链,因为某些问题可能已经在最新版本中得到了修复。

接下来,考虑以下几点常见的排查方向:

  1. 并发问题:如果你的代码涉及并发编程,确保你正确使用了goroutine和channel,并避免了数据竞争。

  2. 内存管理:Go有垃圾回收机制,但不当的内存使用(如内存泄漏)仍然可能导致问题。检查你的内存分配和释放逻辑。

  3. 类型断言和接口:确保在使用类型断言时,接口变量确实持有期望的具体类型。

  4. 外部依赖:如果你的代码依赖于外部库或系统调用,请确保这些依赖项正常工作,并且版本兼容。

  5. 编译器和运行时标志:尝试使用不同的编译器标志(如-race检测数据竞争)来编译和运行你的代码,这可能会揭示隐藏的问题。

如果以上步骤仍未解决问题,你可以将问题的详细描述、相关代码片段以及你已经尝试过的解决方法发布到Go语言的社区论坛或GitHub等平台上,通常会有经验丰富的开发者提供帮助。

回到顶部