使用 go 遇到的一个奇怪问题,求教

235 天前
 afxcn

下面这段代码在长时间运行后,有一定的机率会出错,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)
}
5092 次点击
所在节点    Go 编程语言
56 条回复
kneo
235 天前
不是线程安全的吧。
afxcn
235 天前
@kneo 应该和线程安全没什么关系吧,我也不确定。

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

```go
func createRand() *rand.Rand {

if _seededRand == nil {
_seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
}

return _seededRand
}

// 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[createRand().Intn(l)]
}
return string(b)
}
```
AceGo
235 天前
@afxcn #2 你是怎么判断是否出现 0 的
pathletboy
235 天前
这么改试试
```go
package helper

import (
"math/rand"
"time"
)

const _charsetRand = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$"

var _seededRand *rand.Rand

func init() {
_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)
}
```
afxcn
235 天前
@AceGo 我们在预上线的测试环境上发现的,我们用它来生成 token ,突然发现 token 表里出现大量全是 32 个 0 这样的 token 。
AceGo
235 天前
@afxcn #5 你的意思是换成新的方法在线上没有出现过 0 了?
afxcn
235 天前
@AceGo 换了写法后,好几年了,没出现过这种情况,不过后来我们又改了,改成用 crypto/rand 了,所以也不是 100%确定#2 的写法是不是对的。

```go
package utils

import (
"crypto/rand"
"encoding/hex"
)

// RandomString random string
func RandomString(len int) (string, error) {

b := make([]byte, len/2)

_, err := rand.Read(b)

if err != nil {
return "", err
}

return hex.EncodeToString(b), nil
}
```
Citrus
235 天前
@afxcn crypto rand 用来生成 token 性能非常差的,建议别这么改
yin1999
235 天前
现在最新版本 Go 里面,rand 包的全局随机数生成器的随机种子也是每次自动生成的了,而且有自带的加速特性,可以考虑切回 rand 的全局随机数生成器试试会不会有这个问题。
R18
235 天前
目前最新的 Go 版本也会出错吗?
dododada
235 天前
chatgpt

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

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

通过这样的修改,确保了在并发情况下每个 goroutine 都有自己的 _seededRand 实例,从而解决了竞争条件问题,确保了程序的稳定性。
ixiaohei
235 天前
```rand.NewSource(time.Now().UnixNano())```不是线程安全的。并发情况下会出现一些未定义的异常,比如 panic
R18
235 天前
```go
charset[_seededRand.Intn(l)]
```

我有点好奇,就算不是线程安全的,这代码也不该返回字符串"0"啊,"0"在整个 charset 里也没有处在一个特殊的位置。
keakon
235 天前
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 的
retanoj
235 天前
@R18 #13
参考 #11 的回答,如果发生静态条件,那这句会报错,赋值不会被完成,所以 b 数组全零。

感觉是这样
R18
235 天前
@retanoj b 是一个 byte[], byte 的零值是 0 没错,但是经过 string() 类型转换后会变成 “ ” ,我特意试了一下。
AceGo
235 天前
@retanoj 也应该是 panic 退出,不应该继续执行
hxzhouh1
235 天前
up 能说一下复现的方式跟 go 版本嘛? 我目前没法复现
yianing
235 天前
@ixiaohei 这个是全局变量,只会在包引入时初始化一次
ixiaohei
235 天前
@yianing New 出来的 Source 不是线程安全的。如果是 rand 包导出的方法是线程安全的,因为里面的 source 是并发安全的

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1038255

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX