在信息安全测试、弱口令审计等场景中,我们经常需要本地生成指定长度和字符集的全量密码字典。用Go语言来做这件事,既能编译成单文件原生运行,又能方便利用多核并发。下面直接给出可落地的实现方案。

一、问题建模与基础递归实现
生成所有N字符密码,可以看作是从给定字符集中每次取一个字符,连续取N次,把每一次的选择组合起来。当N固定时,写N层for循环就能解决;但N是变量时,必须用递归或模拟进位的办法。递归的思路是:已经拼好了前k个字符,如果k等于N就输出,否则遍历字符集继续往下拼。
下面这段代码用递归生成全部组合,字符集以大小写字母和数字为例,N由变量length控制。需要注意Go里字符串遍历得到的是rune,这里直接用byte切片更简单。
package main
import (
"fmt"
)
var charset = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
func generateRecursive(prefix []byte, length int) {
if len(prefix) == length {
fmt.Println(string(prefix))
return
}
for _, c := range charset {
generateRecursive(append(prefix, c), length)
}
}
func main() {
n := 3
generateRecursive(make([]byte, 0, n), n)
}
上述代码逻辑清晰,但当N较大(例如8以上)且字符集有62个字符时,递归深度达到8层本身没问题,但总调用次数会呈指数增长,输出到终端也会成为瓶颈。递归的主要优点是代码易读,缺点是难以中途暂停或并发控制。
另外,append每次都会产生新切片,在极大量调用时会产生不少临时对象。如果只跑小长度密码(如3到4位),该写法在笔记本上几秒就能出结果,适合快速验证思路。
二、迭代法基于进位思想的实现
为了避免递归的调用栈和临时对象开销,可以用类似数字进制的思路:把每一个位置看作一个独立计数器,初始全为0,每次整体加一,某个位置到达字符集长度就归零并向前进位。这样用长度为N的int切片就能表达所有组合。
以下迭代版本不依赖递归,用一个循环不断进位直到最高位也溢出为止。它更容易接入缓冲写入和并发改造。
package main
import (
"fmt"
)
func generateIterative(length int) {
charset := []byte("abc")
idx := make([]int, length)
for {
// 输出当前组合
out := make([]byte, length)
for i, v := range idx {
out[i] = charset[v]
}
fmt.Println(string(out))
// 进位
carry := true
for i := length - 1; i >= 0 && carry; i-- {
idx[i]++
if idx[i] == len(charset) {
idx[i] = 0
} else {
carry = false
}
}
if carry {
break
}
}
}
func main() {
generateIterative(3)
}
迭代法把组合空间变成了一个从全0到全最大值的计数器,内存占用固定为N个int,不会因为组合数量变多而栈增长。对于需要生成几千万甚至上亿行密码的场景,这种写法配合下面说的缓冲写入是更稳的选择。
上面的例子字符集只写了abc三个便于演示,实际使用时把charset换成62个字符、length设为所需位数即可。若length为5,总组合数就是62的5次方约9亿,单线程写盘要花不少时间,后面会讲并发切分。
三、写入文件与性能优化
把密码打印到终端只是演示,真实需求往往是写进txt做字典。直接用fmt.Println写文件会产生大量系统调用,必须借助bufio.Writer做缓冲。下面示例把迭代生成的结果写入本地文件。
package main
import (
"bufio"
"os"
)
func writePasswords(length int, path string) error {
charset := []byte("abcdefghijklmnopqrstuvwxyz0123456789")
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
w := bufio.NewWriter(f)
idx := make([]int, length)
for {
out := make([]byte, length)
for i, v := range idx {
out[i] = charset[v]
}
w.Write(out)
w.WriteByte('n')
carry := true
for i := length - 1; i >= 0 && carry; i-- {
idx[i]++
if idx[i] == len(charset) {
idx[i] = 0
} else {
carry = false
}
}
if carry {
break
}
}
return w.Flush()
}
func main() {
_ = writePasswords(4, "passwords.txt")
}
bufio默认缓冲4096字节,积攒一批再落盘,能大幅降低IO次数。如果N较大,建议定期调用Flush或者依赖函数结束前的一次Flush。此外,若担心单个文件过大,可以按前缀拆分多个文件。
当字符集为36、长度为6时,组合数量超过20亿,单线程写可能要几十分钟。此时可按首字母把任务拆成多个goroutine,各自写不同文件,最后再合并,能充分利用多核。但要注意每个goroutine独立bufio和文件句柄,避免同时写同一个文件造成错行。
四、并发拆分示例与注意事项
并发的核心是把首字符固定,其余位置交给一个goroutine生成。下面代码演示按首字符起多个协程,每个协程写独立文件。
package main
import (
"bufio"
"os"
"sync"
)
func worker(first byte, length int, wg *sync.WaitGroup) {
defer wg.Done()
charset := []byte("abcdefghijklmnopqrstuvwxyz0123456789")
f, _ := os.Create("part_" + string(first) + ".txt")
defer f.Close()
bw := bufio.NewWriter(f)
idx := make([]int, length-1)
for {
out := make([]byte, 0, length)
out = append(out, first)
for _, v := range idx {
out = append(out, charset[v])
}
bw.Write(out)
bw.WriteByte('n')
carry := true
for i := len(idx) - 1; i >= 0 && carry; i-- {
idx[i]++
if idx[i] == len(charset) {
idx[i] = 0
} else {
carry = false
}
}
if carry {
break
}
}
bw.Flush()
}
func main() {
var wg sync.WaitGroup
for c := 'a'; c <= 'z'; c++ {
wg.Add(1)
go worker(byte(c), 5, &wg)
}
wg.Wait()
}
这种拆法把总任务分成26份(仅小写字母开头),每份独立运行互不干扰。实际中还可以用sync.WaitGroup配合channel控制并发度,防止打开过多文件。另外,生成密码属于计算密集且IO密集混合,若机器内存小,就不要把所有部分一次性并发,可以限定同时跑4到8个协程。
最后提醒,本文代码仅用于授权环境下的安全自检或算法学习。若把生成的字典用于未授权系统测试,会违反相关法律规定。在自有环境跑完之后,记得及时删除大文件释放磁盘空间。