Làm cách nào để tạo một chuỗi ngẫu nhiên có độ dài cố định trong Go?


300

Tôi muốn một chuỗi ký tự ngẫu nhiên duy nhất (chữ hoa hoặc chữ thường), không có số, trong Go. Cách nhanh nhất và đơn giản nhất để làm điều này là gì?


2
@VinceEmigh: Đây là một chủ đề meta thảo luận về các câu hỏi cơ bản. meta.stackoverflow.com/q/274645/395461 Cá nhân, tôi nghĩ rằng các câu hỏi cơ bản là ổn nếu được viết tốt và có chủ đề. Nhìn vào các câu trả lời dưới đây, chúng minh họa một loạt những điều sẽ hữu ích cho người mới đi. Đối với các vòng lặp, nhập kiểu, tạo (), v.v.
Shannon Matthews

2
@Shannon " Câu hỏi này không cho thấy bất kỳ nỗ lực nghiên cứu nào " (câu trả lời được đánh giá cao đầu tiên trong liên kết của bạn) - Đó là những gì tôi đã đề cập. Ông cho thấy không có nỗ lực nghiên cứu. Không có nỗ lực nào cả (một nỗ lực, hoặc thậm chí nói rằng anh ta nhìn trực tuyến, điều mà rõ ràng là anh ta không có). Mặc dù nó sẽ hữu ích cho người mới , nhưng trang web này không tập trung vào việc dạy người mới. Nó tập trung vào việc trả lời các vấn đề / câu hỏi lập trình cụ thể, không phải hướng dẫn / hướng dẫn. Mặc dù nó có thể được sử dụng cho cái sau, nhưng đó không phải là trọng tâm, và do đó câu hỏi này nên được đóng lại. Thay vào đó, muỗng của nó /:
Vince Emigh

9
@VinceEmigh Tôi hỏi câu hỏi này một năm trước. Tôi đã tìm kiếm trực tuyến các chuỗi ngẫu nhiên và đọc tài liệu quá. Nhưng nó không hữu ích. Nếu tôi không viết trong câu hỏi, thì điều đó không có nghĩa là tôi chưa nghiên cứu.
Anish Shah

Câu trả lời:


809

Giải pháp của Paul cung cấp một giải pháp chung, đơn giản .

Câu hỏi yêu cầu "cách nhanh nhất và đơn giản nhất" . Hãy giải quyết phần nhanh nhất quá. Chúng tôi sẽ đến mã cuối cùng, nhanh nhất của chúng tôi theo cách lặp. Điểm chuẩn mỗi lần lặp có thể được tìm thấy ở cuối câu trả lời.

Tất cả các giải pháp và mã điểm chuẩn có thể được tìm thấy trên Sân chơi Go . Mã trên Playground là một tệp thử nghiệm, không phải là tệp thực thi. Bạn phải lưu nó vào một tệp có tên XX_test.govà chạy nó với

go test -bench . -benchmem

Lời nói đầu :

Giải pháp nhanh nhất không phải là giải pháp đi tới nếu bạn chỉ cần một chuỗi ngẫu nhiên. Vì thế, giải pháp của Paul là hoàn hảo. Đây là nếu hiệu suất không thành vấn đề. Mặc dù 2 bước đầu tiên ( Byte và phần còn lại ) có thể là một sự thỏa hiệp chấp nhận được: chúng cải thiện hiệu suất lên tới 50% (xem số chính xác trong phần II. Điểm chuẩn ) và chúng không làm tăng độ phức tạp đáng kể.

Đã nói rằng, ngay cả khi bạn không cần giải pháp nhanh nhất, đọc qua câu trả lời này có thể là phiêu lưu và giáo dục.

I. Cải tiến

1. Genesis (giai điệu)

Xin nhắc lại, giải pháp chung, nguyên bản mà chúng tôi đang cải thiện là:

func init() {
    rand.Seed(time.Now().UnixNano())
}

var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func RandStringRunes(n int) string {
    b := make([]rune, n)
    for i := range b {
        b[i] = letterRunes[rand.Intn(len(letterRunes))]
    }
    return string(b)
}

2. byte

Nếu các ký tự để chọn và lắp ráp chuỗi ngẫu nhiên chỉ chứa các chữ cái viết hoa và viết thường của bảng chữ cái tiếng Anh, chúng ta chỉ có thể làm việc với các byte vì các chữ cái trong bảng chữ cái tiếng Anh ánh xạ tới các byte 1 trong 1 trong mã hóa UTF-8 (mà là cách Go lưu trữ chuỗi).

Vì vậy, thay vì:

var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

chúng ta có thể sử dụng:

var letters = []bytes("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

Hoặc thậm chí tốt hơn:

const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

Bây giờ đây đã là một cải tiến lớn: chúng ta có thể đạt được nó là một const(có stringhằng số nhưng không có hằng số lát ). Là một lợi ích thêm, biểu thức len(letters)cũng sẽ là a const! (Biểu thức len(s)là hằng số nếu slà hằng chuỗi.)

Và với giá nào? Không có gì đâu. strings có thể được lập chỉ mục mà lập chỉ mục các byte của nó, hoàn hảo, chính xác những gì chúng ta muốn.

Điểm đến tiếp theo của chúng tôi trông như thế này:

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

func RandStringBytes(n int) string {
    b := make([]byte, n)
    for i := range b {
        b[i] = letterBytes[rand.Intn(len(letterBytes))]
    }
    return string(b)
}

3. Phần còn lại

Các giải pháp trước đó có được một số ngẫu nhiên để chỉ định một chữ cái ngẫu nhiên bằng cách gọi rand.Intn()đại biểu Rand.Intn()nào được ủy quyền Rand.Int31n().

Điều này chậm hơn nhiều so với việc rand.Int63()tạo ra một số ngẫu nhiên với 63 bit ngẫu nhiên.

Vì vậy, chúng tôi chỉ có thể gọi rand.Int63()và sử dụng phần còn lại sau khi chia cho len(letterBytes):

func RandStringBytesRmndr(n int) string {
    b := make([]byte, n)
    for i := range b {
        b[i] = letterBytes[rand.Int63() % int64(len(letterBytes))]
    }
    return string(b)
}

Điều này hoạt động và nhanh hơn đáng kể, nhược điểm là xác suất của tất cả các chữ cái sẽ không hoàn toàn giống nhau (giả sử rand.Int63()tạo ra tất cả các số 63 bit với xác suất bằng nhau). Mặc dù độ méo là cực kỳ nhỏ vì số lượng chữ cái 52nhỏ hơn rất nhiều so với 1<<63 - 1thực tế, nhưng trong thực tế, điều này là hoàn toàn tốt.

Để làm cho điều này dễ hiểu hơn: giả sử bạn muốn có một số ngẫu nhiên trong phạm vi 0..5. Sử dụng 3 bit ngẫu nhiên, điều này sẽ tạo ra các số 0..1có xác suất gấp đôi so với từ phạm vi 2..5. Sử dụng 5 bit ngẫu nhiên, các số trong phạm vi 0..1sẽ xảy ra với 6/32xác suất và các số trong phạm vi 2..5với 5/32xác suất hiện gần với mong muốn. Việc tăng số lượng bit làm cho điều này ít quan trọng hơn, khi đạt tới 63 bit, nó không đáng kể.

4. Đắp mặt nạ

Dựa trên giải pháp trước đó, chúng ta có thể duy trì phân phối các chữ cái bằng nhau bằng cách chỉ sử dụng càng nhiều bit thấp nhất của số ngẫu nhiên, càng nhiều số được yêu cầu để thể hiện số lượng chữ cái. Vì vậy, ví dụ nếu chúng ta có 52 chữ cái, nó cần 6 bit để thể hiện nó : 52 = 110100b. Vì vậy, chúng tôi sẽ chỉ sử dụng 6 bit thấp nhất của số được trả về rand.Int63(). Và để duy trì phân phối các chữ cái bằng nhau, chúng tôi chỉ "chấp nhận" số nếu nó nằm trong phạm vi 0..len(letterBytes)-1. Nếu các bit thấp nhất lớn hơn, chúng tôi loại bỏ nó và truy vấn một số ngẫu nhiên mới.

Lưu ý rằng cơ hội của các bit thấp nhất lớn hơn hoặc bằng len(letterBytes)nhỏ hơn so với 0.5nói chung ( 0.25trung bình), điều đó có nghĩa là ngay cả khi điều này xảy ra, việc lặp lại trường hợp "hiếm" này làm giảm cơ hội không tìm thấy hàng hóa con số. Sau khi nlặp lại, cơ hội mà chúng ta không có chỉ số tốt ít hơn nhiều pow(0.5, n)và đây chỉ là ước tính cao hơn. Trong trường hợp 52 chữ cái, khả năng 6 bit thấp nhất là không tốt (64-52)/64 = 0.19; có nghĩa là ví dụ cơ hội không có số tốt sau 10 lần lặp lại là 1e-8.

Vì vậy, đây là giải pháp:

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
    letterIdxBits = 6                    // 6 bits to represent a letter index
    letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
)

func RandStringBytesMask(n int) string {
    b := make([]byte, n)
    for i := 0; i < n; {
        if idx := int(rand.Int63() & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i++
        }
    }
    return string(b)
}

5. Mặt nạ được cải thiện

Giải pháp trước đó chỉ sử dụng 6 bit thấp nhất trong số 63 bit ngẫu nhiên được trả về rand.Int63(). Đây là một sự lãng phí vì nhận được các bit ngẫu nhiên là phần chậm nhất trong thuật toán của chúng tôi.

Nếu chúng ta có 52 chữ cái, điều đó có nghĩa là 6 bit mã chỉ mục chữ cái. Vì vậy, 63 bit ngẫu nhiên có thể chỉ định 63/6 = 10các chỉ số chữ cái khác nhau. Hãy sử dụng tất cả 10:

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
    letterIdxBits = 6                    // 6 bits to represent a letter index
    letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
    letterIdxMax  = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
)

func RandStringBytesMaskImpr(n int) string {
    b := make([]byte, n)
    // A rand.Int63() generates 63 random bits, enough for letterIdxMax letters!
    for i, cache, remain := n-1, rand.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = rand.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return string(b)
}

6. Nguồn

Mặt nạ cải tiến là khá tốt, chúng ta không thể cải thiện nó nhiều. Chúng ta có thể, nhưng không đáng để phức tạp.

Bây giờ hãy tìm một cái gì đó khác để cải thiện. Nguồn của các số ngẫu nhiên.

Có một crypto/randgói cung cấp một Read(b []byte)hàm, vì vậy chúng ta có thể sử dụng gói đó để nhận được nhiều byte với một cuộc gọi mà chúng ta cần. Điều này sẽ không giúp ích gì về mặt hiệu suất khi crypto/randtriển khai trình tạo số giả ngẫu nhiên được bảo mật bằng mật mã nên chậm hơn nhiều.

Vì vậy, hãy gắn bó với math/randgói. Việc rand.Randsử dụng rand.Sourcenhư là nguồn của các bit ngẫu nhiên. rand.Sourcelà một giao diện chỉ định một Int63() int64phương thức: chính xác và là thứ duy nhất chúng tôi cần và sử dụng trong giải pháp mới nhất của chúng tôi.

Vì vậy, chúng tôi không thực sự cần một rand.Rand(rõ ràng hoặc toàn cầu, được chia sẻ một trong các randgói), một rand.Sourcelà hoàn toàn đủ cho chúng tôi:

var src = rand.NewSource(time.Now().UnixNano())

func RandStringBytesMaskImprSrc(n int) string {
    b := make([]byte, n)
    // A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
    for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = src.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return string(b)
}

Cũng lưu ý rằng giải pháp cuối cùng này không yêu cầu bạn khởi tạo (hạt giống) toàn cầu Randcủa math/randgói vì nó không được sử dụng (và chúng tôi rand.Sourceđược khởi tạo / gieo hạt đúng cách).

Một điều nữa cần lưu ý ở đây: gói doc của các math/randtrạng thái:

Nguồn mặc định là an toàn để sử dụng đồng thời bởi nhiều con khỉ đột.

Vì vậy, nguồn mặc định chậm hơn nguồn Sourcecó thể có được bởi rand.NewSource()vì nguồn mặc định phải cung cấp sự an toàn theo truy cập / sử dụng đồng thời, trong khi rand.NewSource()không cung cấp điều này (và do đó, Sourcenó được trả về có nhiều khả năng nhanh hơn).

7. Sử dụng strings.Builder

Tất cả các giải pháp trước đó trả về một stringnội dung có nội dung đầu tiên được xây dựng trong một lát ( []runetrong Genesis[]bytetrong các giải pháp tiếp theo), sau đó được chuyển đổi thành string. Chuyển đổi cuối cùng này phải tạo một bản sao nội dung của lát cắt, bởi vì stringcác giá trị là bất biến và nếu chuyển đổi không tạo ra một bản sao, không thể đảm bảo rằng nội dung của chuỗi không được sửa đổi thông qua lát cắt ban đầu. Để biết chi tiết, hãy xem Cách chuyển đổi chuỗi utf8 thành [] byte? golang: [] byte (chuỗi) so với [] byte (* chuỗi) .

Giới thiệu 1.10 strings.Builder. strings.Buildermột loại mới chúng ta có thể sử dụng để xây dựng nội dung stringtương tự bytes.Buffer. Nó thực hiện bên trong bằng cách sử dụng a []bytevà khi hoàn thành, chúng ta có thể nhận được stringgiá trị cuối cùng bằng Builder.String()phương thức của nó . Nhưng điều thú vị ở đây là nó thực hiện điều này mà không thực hiện bản sao mà chúng ta vừa nói ở trên. Nó dám làm như vậy bởi vì lát cắt byte được sử dụng để xây dựng nội dung của chuỗi không bị lộ, do đó, đảm bảo rằng không ai có thể sửa đổi nó vô tình hoặc độc hại để thay đổi chuỗi "bất biến" được tạo ra.

Vì vậy, ý tưởng tiếp theo của chúng tôi là không xây dựng chuỗi ngẫu nhiên trong một lát, nhưng với sự trợ giúp của a strings.Builder, vì vậy, khi chúng tôi hoàn thành, chúng tôi có thể lấy và trả về kết quả mà không cần phải sao chép chuỗi đó. Điều này có thể giúp về tốc độ, và nó chắc chắn sẽ giúp về mặt sử dụng và phân bổ bộ nhớ.

func RandStringBytesMaskImprSrcSB(n int) string {
    sb := strings.Builder{}
    sb.Grow(n)
    // A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
    for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = src.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            sb.WriteByte(letterBytes[idx])
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return sb.String()
}

Xin lưu ý rằng sau khi tạo mới strings.Buidler, chúng tôi đã gọi Builder.Grow()phương thức của nó , đảm bảo rằng nó phân bổ một lát nội bộ đủ lớn (để tránh phân bổ lại khi chúng tôi thêm các chữ cái ngẫu nhiên).

8. "Bắt chước" strings.Buildervới góiunsafe

strings.Builderxây dựng chuỗi trong một nội bộ []byte, giống như chúng ta đã làm. Vì vậy, về cơ bản thực hiện nó thông qua một strings.Buildersố chi phí, điều duy nhất chúng tôi chuyển sang strings.Builderlà để tránh việc sao chép cuối cùng của lát cắt.

strings.Buildertránh bản sao cuối cùng bằng cách sử dụng gói unsafe:

// String returns the accumulated string.
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))
}

Vấn đề là, chúng ta cũng có thể tự làm điều này. Vì vậy, ý tưởng ở đây là chuyển trở lại để xây dựng chuỗi ngẫu nhiên trong một []byte, nhưng khi chúng ta hoàn thành, đừng chuyển đổi nó stringthành trả về, nhưng thực hiện chuyển đổi không an toàn: lấy một stringđiểm trỏ đến lát byte của chúng ta dưới dạng dữ liệu chuỗi .

Đây là cách nó có thể được thực hiện:

func RandStringBytesMaskImprSrcUnsafe(n int) string {
    b := make([]byte, n)
    // A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
    for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = src.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return *(*string)(unsafe.Pointer(&b))
}

(9. Sử dụng rand.Read())

Đi 1.7 thêm một rand.Read()chức năng và một Rand.Read()phương thức. Chúng ta nên sử dụng chúng để đọc bao nhiêu byte mà chúng ta cần trong một bước, để đạt được hiệu suất tốt hơn.

Có một "vấn đề" nhỏ với điều này: chúng ta cần bao nhiêu byte? Chúng ta có thể nói: nhiều như số lượng chữ cái đầu ra. Chúng tôi sẽ nghĩ rằng đây là một ước tính trên, vì một chỉ mục chữ sử dụng ít hơn 8 bit (1 byte). Nhưng tại thời điểm này, chúng tôi đã làm tồi tệ hơn (vì nhận được các bit ngẫu nhiên là "phần cứng") và chúng tôi đang nhận được nhiều hơn mức cần thiết.

Cũng lưu ý rằng để duy trì phân phối đồng đều của tất cả các chỉ mục chữ cái, có thể có một số dữ liệu ngẫu nhiên "rác" mà chúng tôi sẽ không thể sử dụng, vì vậy chúng tôi sẽ bỏ qua một số dữ liệu và do đó kết thúc ngắn khi chúng tôi trải qua tất cả các lát byte. Chúng ta sẽ cần thêm các byte ngẫu nhiên, "đệ quy". Và giờ chúng ta thậm chí còn mất lợi thế "một cuộc gọi đến randgói" ...

Chúng tôi có thể "phần nào" tối ưu hóa việc sử dụng dữ liệu ngẫu nhiên mà chúng tôi có được math.Rand(). Chúng tôi có thể ước tính cần bao nhiêu byte (bit). 1 chữ cái yêu cầu letterIdxBitsbit và chúng ta cần nchữ cái, vì vậy chúng ta cần n * letterIdxBits / 8.0byte làm tròn. Chúng tôi có thể tính xác suất của một chỉ số ngẫu nhiên không thể sử dụng được (xem ở trên), vì vậy chúng tôi có thể yêu cầu nhiều hơn "nhiều khả năng" là đủ (nếu nó không thành công, chúng tôi sẽ lặp lại quy trình). Ví dụ, chúng ta có thể xử lý lát byte dưới dạng "luồng bit", trong đó chúng ta có một lib bên thứ 3 đẹp: github.com/icza/bitio(tiết lộ: Tôi là tác giả).

Nhưng mã điểm chuẩn vẫn cho thấy chúng ta không chiến thắng. Tại sao nó như vậy?

Câu trả lời cho câu hỏi cuối cùng là bởi vì rand.Read()sử dụng một vòng lặp và tiếp tục gọi Source.Int63()cho đến khi nó lấp đầy lát cắt đã qua. Chính xác những gì RandStringBytesMaskImprSrc()giải pháp làm, không có bộ đệm trung gian và không có sự phức tạp thêm vào. Đó là lý do tại sao RandStringBytesMaskImprSrc()vẫn còn trên ngai vàng. Có, RandStringBytesMaskImprSrc()sử dụng không đồng bộ rand.Sourcekhông giống như rand.Read(). Nhưng lý luận vẫn được áp dụng; và được chứng minh nếu chúng ta sử dụng Rand.Read()thay vì rand.Read()(trước đây cũng không được đồng bộ hóa).

II. Điểm chuẩn

Được rồi, đã đến lúc điểm chuẩn các giải pháp khác nhau.

Khoảnh khắc của sự thật:

BenchmarkRunes-4                     2000000    723 ns/op   96 B/op   2 allocs/op
BenchmarkBytes-4                     3000000    550 ns/op   32 B/op   2 allocs/op
BenchmarkBytesRmndr-4                3000000    438 ns/op   32 B/op   2 allocs/op
BenchmarkBytesMask-4                 3000000    534 ns/op   32 B/op   2 allocs/op
BenchmarkBytesMaskImpr-4            10000000    176 ns/op   32 B/op   2 allocs/op
BenchmarkBytesMaskImprSrc-4         10000000    139 ns/op   32 B/op   2 allocs/op
BenchmarkBytesMaskImprSrcSB-4       10000000    134 ns/op   16 B/op   1 allocs/op
BenchmarkBytesMaskImprSrcUnsafe-4   10000000    115 ns/op   16 B/op   1 allocs/op

Chỉ bằng cách chuyển từ rune sang byte, chúng tôi ngay lập tức có hiệu suất tăng 24% và yêu cầu bộ nhớ giảm xuống còn một phần ba .

Loại bỏ rand.Intn()và sử dụng rand.Int63()thay vào đó giúp tăng thêm 20% .

Mặt nạ (và lặp lại trong trường hợp chỉ số lớn) chậm lại một chút (do các cuộc gọi lặp lại): -22% ...

Nhưng khi chúng ta sử dụng tất cả (hoặc hầu hết) trong số 63 bit ngẫu nhiên (10 chỉ số từ một rand.Int63()cuộc gọi): sẽ tăng tốc thời gian lớn: 3 lần .

Nếu chúng tôi giải quyết bằng một (không mặc định, mới) rand.Sourcethay vì rand.Rand, chúng tôi lại nhận được 21%.

Nếu chúng ta sử dụng strings.Builder, chúng ta đạt được một nhỏ 3,5% trong tốc độ , nhưng chúng tôi cũng đã đạt được 50% giảm sử dụng bộ nhớ và phân bổ! Thật tuyệt!

Cuối cùng, nếu chúng tôi dám sử dụng gói unsafethay vì strings.Builder, chúng tôi lại nhận được 14% tốt đẹp .

So sánh trận chung kết các giải pháp ban đầu: RandStringBytesMaskImprSrcUnsafe()nhanh hơn 6,3 lần so với RandStringRunes(), sử dụng một phần sáu bộ nhớ và một nửa số phân bổ . Nhiệm vụ đã hoàn thành.


8
@RobbieV Yup, vì một chia sẻ rand.Sourceđược sử dụng. Một cách giải quyết tốt hơn sẽ được vượt qua một rand.Sourceđến RandStringBytesMaskImprSrc()chức năng, và như vậy không khóa là cần thiết và do đó hiệu suất / hiệu quả là không bị ảnh hưởng. Mỗi con goroutine có thể có cái riêng của nó Source.
icza

113
@icza, đó là một trong những câu trả lời hay nhất tôi thấy trong một thời gian dài trên SO!
astropanic

1
@MikeAtlas: Nên tránh sử dụng deferkhi rõ ràng là bạn không cần nó. Xem grokbase.com/t/gg/golang-nuts/158zz5p42w/
Zan Lynx

1
@ZanLynx thx cho tiền boa; Mặc dù deferđể mở khóa một mutex ngay lập tức trước hoặc sau khi gọi khóa là IMO chủ yếu là một ý tưởng rất tốt; Bạn được đảm bảo cả hai không quên mở khóa mà còn mở khóa ngay cả trong chức năng trung gian hoảng loạn không gây tử vong.
Mike Atlas

1
@RobbieV có vẻ như mã này là thread / goroutine an toàn vì nguồn chia sẻ cơ bản đã là KhóaSource thực hiện mutex ( golang.org/src/math/rand/rand.go:259 ).
adityajones

130

Bạn chỉ có thể viết mã cho nó. Mã này có thể đơn giản hơn một chút nếu bạn muốn dựa vào tất cả các chữ cái là các byte đơn khi được mã hóa trong UTF-8.

package main

import (
    "fmt"
    "time"
    "math/rand"
)

var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func randSeq(n int) string {
    b := make([]rune, n)
    for i := range b {
        b[i] = letters[rand.Intn(len(letters))]
    }
    return string(b)
}

func main() {
    rand.Seed(time.Now().UnixNano())

    fmt.Println(randSeq(10))
}

30
Đừng quên rand.Seed (), nếu không bạn sẽ có cùng một chuỗi mỗi lần khởi chạy ... rand.Seed (time.Now (). UTC (). UnixNano ())
Evan Lin

2
Bổ sung của Evan là chính xác, tuy nhiên có những lựa chọn tương tự khác: rand.Seed(time.Now().Unix())hoặcrand.Seed(time.Now().UnixNano())
openwonk

7
Đối với một bí mật khó đoán - mật khẩu, khóa mật mã, v.v .-- không bao giờ sử dụng math/rand; thay vào đó, hãy sử dụng crypto/rand(như @ Not_A_Golfer's tùy chọn 1).
twotwotwo

1
@EvanLin Điều này có thể đoán được không? Nếu tôi phải gieo hạt cho trình tạo, thì kẻ tấn công có thể đoán thời gian tôi gieo hạt giống và dự đoán cùng một đầu ra mà tôi đang tạo.
Matej

4
Lưu ý rằng nếu bạn đang thử chương trình trên với seed, trên sân chơi, nó sẽ trả lại kết quả tương tự mọi lúc. Tôi đã thử nó trên sân chơi và sau một thời gian nhận ra điều này. Nó làm việc tốt cho tôi. Hy vọng nó sẽ tiết kiệm thời gian của ai đó :)
Gaurav Sinha

18

Sử dụng gói uniuri , tạo ra các chuỗi thống nhất (không thiên vị) bảo mật bằng mật mã.

Tuyên bố miễn trừ trách nhiệm: Tôi là tác giả của gói


1
Ngoài ra: tác giả, dchest, là một nhà phát triển xuất sắc và đã sản xuất một số gói nhỏ, hữu ích như thế này.
Roshambo

16

Hai tùy chọn có thể (có thể có nhiều khóa học hơn):

  1. Bạn có thể sử dụng crypto/randgói hỗ trợ đọc các mảng byte ngẫu nhiên (từ / dev / urandom) và hướng đến việc tạo ngẫu nhiên bằng mật mã. xem http://golang.org/pkg/crypto/rand/#example_Read . Nó có thể chậm hơn so với thế hệ số giả ngẫu nhiên bình thường.

  2. Lấy một số ngẫu nhiên và băm nó bằng md5 hoặc một cái gì đó như thế này.


4

Sau icza'sgiải pháp tuyệt vời được giải thích, đây là một sửa đổi của nó sử dụng crypto/randthay vì math/rand.

const (
    letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // 52 possibilities
    letterIdxBits = 6                    // 6 bits to represent 64 possibilities / indexes
    letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
)

func SecureRandomAlphaString(length int) string {

    result := make([]byte, length)
    bufferSize := int(float64(length)*1.3)
    for i, j, randomBytes := 0, 0, []byte{}; i < length; j++ {
        if j%bufferSize == 0 {
            randomBytes = SecureRandomBytes(bufferSize)
        }
        if idx := int(randomBytes[j%length] & letterIdxMask); idx < len(letterBytes) {
            result[i] = letterBytes[idx]
            i++
        }
    }

    return string(result)
}

// SecureRandomBytes returns the requested number of bytes using crypto/rand
func SecureRandomBytes(length int) []byte {
    var randomBytes = make([]byte, length)
    _, err := rand.Read(randomBytes)
    if err != nil {
        log.Fatal("Unable to generate random bytes")
    }
    return randomBytes
}

Nếu bạn muốn một giải pháp chung hơn, cho phép bạn chuyển vào lát byte ký tự để tạo chuỗi ra, bạn có thể thử sử dụng cách này:

// SecureRandomString returns a string of the requested length,
// made from the byte characters provided (only ASCII allowed).
// Uses crypto/rand for security. Will panic if len(availableCharBytes) > 256.
func SecureRandomString(availableCharBytes string, length int) string {

    // Compute bitMask
    availableCharLength := len(availableCharBytes)
    if availableCharLength == 0 || availableCharLength > 256 {
        panic("availableCharBytes length must be greater than 0 and less than or equal to 256")
    }
    var bitLength byte
    var bitMask byte
    for bits := availableCharLength - 1; bits != 0; {
        bits = bits >> 1
        bitLength++
    }
    bitMask = 1<<bitLength - 1

    // Compute bufferSize
    bufferSize := length + length / 3

    // Create random string
    result := make([]byte, length)
    for i, j, randomBytes := 0, 0, []byte{}; i < length; j++ {
        if j%bufferSize == 0 {
            // Random byte buffer is empty, get a new one
            randomBytes = SecureRandomBytes(bufferSize)
        }
        // Mask bytes to get an index into the character slice
        if idx := int(randomBytes[j%length] & bitMask); idx < availableCharLength {
            result[i] = availableCharBytes[idx]
            i++
        }
    }

    return string(result)
}

Nếu bạn muốn vượt qua trong nguồn ngẫu nhiên của riêng mình, việc sửa đổi những điều trên để chấp nhận io.Readerthay vì sử dụng là chuyện nhỏ crypto/rand.


2

Nếu bạn muốn số ngẫu nhiên được bảo mật bằng mật mã và bộ ký tự chính xác là linh hoạt (giả sử, cơ sở 64 là tốt), bạn có thể tính chính xác độ dài của các ký tự ngẫu nhiên bạn cần từ kích thước đầu ra mong muốn.

Văn bản cơ sở 64 dài hơn 1/3 so với cơ sở 256. (tỷ lệ 2 ^ 8 so với 2 ^ 6; 8 tỷ / 6 bit = 1.333)

import (
    "crypto/rand"
    "encoding/base64"
    "math"
)

func randomBase64String(l int) string {
    buff := make([]byte, int(math.Round(float64(l)/float64(1.33333333333))))
    rand.Read(buff)
    str := base64.RawURLEncoding.EncodeToString(buff)
    return str[:l] // strip 1 extra character we get from odd length results
}

Lưu ý: bạn cũng có thể sử dụng RawStdEncoding nếu bạn thích + và / ký tự cho - và _

Nếu bạn muốn hex, cơ sở 16 dài hơn 2 lần so với cơ sở 256. (tỷ lệ 2 ^ 8 so với 2 ^ 4; 8 bit / 4 bit = 2x)

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


func randomBase16String(l int) string {
    buff := make([]byte, int(math.Round(float64(l)/2)))
    rand.Read(buff)
    str := hex.EncodeToString(buff)
    return str[:l] // strip 1 extra character we get from odd length results
}

Tuy nhiên, bạn có thể mở rộng nó thành bất kỳ bộ ký tự tùy ý nào nếu bạn có bộ mã hóa base256 sang baseN cho bộ ký tự của mình. Bạn có thể thực hiện phép tính kích thước tương tự với số lượng bit cần thiết để thể hiện bộ ký tự của bạn. Tính toán tỷ lệ cho bất kỳ bộ ký tự tùy ý là ratio = 8 / log2(len(charset)):).

Mặc dù cả hai giải pháp này đều an toàn, đơn giản, nên nhanh chóng và không lãng phí nhóm entropy tiền điện tử của bạn.

Đây là sân chơi cho thấy nó hoạt động cho mọi kích cỡ. https://play.golang.org/p/i61WUVR8_3Z


đáng nói là Go Playground luôn trả về cùng một số ngẫu nhiên, vì vậy bạn sẽ không thấy ở đó các chuỗi ngẫu nhiên khác nhau ở các lần thực thi khác nhau của mã đó
TPPZ

2
func Rand(n int) (str string) {
    b := make([]byte, n)
    rand.Read(b)
    str = fmt.Sprintf("%x", b)
    return
}

Tại sao nó tạo ra n * 2 []byte?
M. Rostami

1

Đây là cách của tôi) Sử dụng toán rand hoặc crypto rand như bạn muốn.

func randStr(len int) string {
    buff := make([]byte, len)
    rand.Read(buff)
    str := base64.StdEncoding.EncodeToString(buff)
    // Base 64 can be longer than len
    return str[:len]
}

0

Nếu bạn sẵn sàng thêm một vài ký tự vào nhóm ký tự được phép, bạn có thể làm cho mã hoạt động với bất kỳ thứ gì cung cấp byte ngẫu nhiên thông qua io.Reader. Ở đây chúng tôi đang sử dụng crypto/rand.

// len(encodeURL) == 64. This allows (x <= 265) x % 64 to have an even
// distribution.
const encodeURL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"

// A helper function create and fill a slice of length n with characters from
// a-zA-Z0-9_-. It panics if there are any problems getting random bytes.
func RandAsciiBytes(n int) []byte {
    output := make([]byte, n)

    // We will take n bytes, one byte for each character of output.
    randomness := make([]byte, n)

    // read all random
    _, err := rand.Read(randomness)
    if err != nil {
        panic(err)
    }

    // fill output
    for pos := range output {
        // get random item
        random := uint8(randomness[pos])

        // random % 64
        randomPos := random % uint8(len(encodeURL))

        // put into output
        output[pos] = encodeURL[randomPos]
    }

    return output
}

tại sao random % 64cần thiết
Sung Cho

2
Bởi vì len(encodeURL) == 64. Nếu random % 64không được thực hiện, randomPoscó thể> = 64 và gây ra sự hoảng loạn trong thời gian chạy.
0xcaff

-1
/*
    korzhao
*/

package rand

import (
    crand "crypto/rand"
    "math/rand"
    "sync"
    "time"
    "unsafe"
)

// Doesn't share the rand library globally, reducing lock contention
type Rand struct {
    Seed int64
    Pool *sync.Pool
}

var (
    MRand    = NewRand()
    randlist = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
)

// init random number generator
func NewRand() *Rand {
    p := &sync.Pool{New: func() interface{} {
        return rand.New(rand.NewSource(getSeed()))
    },
    }
    mrand := &Rand{
        Pool: p,
    }
    return mrand
}

// get the seed
func getSeed() int64 {
    return time.Now().UnixNano()
}

func (s *Rand) getrand() *rand.Rand {
    return s.Pool.Get().(*rand.Rand)
}
func (s *Rand) putrand(r *rand.Rand) {
    s.Pool.Put(r)
}

// get a random number
func (s *Rand) Intn(n int) int {
    r := s.getrand()
    defer s.putrand(r)

    return r.Intn(n)
}

//  bulk get random numbers
func (s *Rand) Read(p []byte) (int, error) {
    r := s.getrand()
    defer s.putrand(r)

    return r.Read(p)
}

func CreateRandomString(len int) string {
    b := make([]byte, len)
    _, err := MRand.Read(b)
    if err != nil {
        return ""
    }
    for i := 0; i < len; i++ {
        b[i] = randlist[b[i]%(62)]
    }
    return *(*string)(unsafe.Pointer(&b))
}

24,0 ns / op 16 B / op 1 allocs /


Xin chào! Chào mừng bạn đến với StackOverflow. Mặc dù bạn đã thêm một đoạn mã, câu trả lời của bạn không bao gồm bất kỳ ngữ cảnh nào về "cách thức hoạt động" hoặc "tại sao đây là cách thực hiện". Ngoài ra, hãy nhớ câu hỏi được hỏi bằng tiếng Anh để bình luận của bạn cũng phải bằng tiếng Anh.
Cengiz Can

-2
const (
    chars       = "0123456789_abcdefghijkl-mnopqrstuvwxyz" //ABCDEFGHIJKLMNOPQRSTUVWXYZ
    charsLen    = len(chars)
    mask        = 1<<6 - 1
)

var rng = rand.NewSource(time.Now().UnixNano())

// RandStr 返回指定长度的随机字符串
func RandStr(ln int) string {
    /* chars 38个字符
     * rng.Int63() 每次产出64bit的随机数,每次我们使用6bit(2^6=64) 可以使用10次
     */
    buf := make([]byte, ln)
    for idx, cache, remain := ln-1, rng.Int63(), 10; idx >= 0; {
        if remain == 0 {
            cache, remain = rng.Int63(), 10
        }
        buf[idx] = chars[int(cache&mask)%charsLen]
        cache >>= 6
        remain--
        idx--
    }
    return *(*string)(unsafe.Pointer(&buf))
}

Điểm chuẩnRandStr16-8 20000000 68,1 ns / op 16 B / op 1 allocs / op

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.