Golang 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 >= 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
应该和线程安全没什么关系吧,我也不确定。
换成下面的代码这样,就不会出现返回全为 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 <= 0 || max <= 0 {<br> return 0<br> }<br><br> if min >= 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 <= 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)})
使用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… 然后误解,是这里生成的了。
当前上面也只是我的一种 推理可能性
-race
想象一下同一 ns 情况下 两个 routine 调用了 你哪个不安全的 var , 得到的结果
AI 回答.
遇到过差不多的问题,不过因为代码不多,很快就发现是 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 它排最前面.
恕我直言,我不认为几年前的问题你今天还能根据记忆还原出真相。除非你现在依然能重现它。
- 现在的 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 是并发不安全的
亲测并发会 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编译器和工具链,因为某些问题可能已经在最新版本中得到了修复。
接下来,考虑以下几点常见的排查方向:
-
并发问题:如果你的代码涉及并发编程,确保你正确使用了goroutine和channel,并避免了数据竞争。
-
内存管理:Go有垃圾回收机制,但不当的内存使用(如内存泄漏)仍然可能导致问题。检查你的内存分配和释放逻辑。
-
类型断言和接口:确保在使用类型断言时,接口变量确实持有期望的具体类型。
-
外部依赖:如果你的代码依赖于外部库或系统调用,请确保这些依赖项正常工作,并且版本兼容。
-
编译器和运行时标志:尝试使用不同的编译器标志(如
-race
检测数据竞争)来编译和运行你的代码,这可能会揭示隐藏的问题。
如果以上步骤仍未解决问题,你可以将问题的详细描述、相关代码片段以及你已经尝试过的解决方法发布到Go语言的社区论坛或GitHub等平台上,通常会有经验丰富的开发者提供帮助。