OpenCode 实践课:4 分钟用 Go 写出一个命令行随机密码生成器

项目介绍

今天我们用 OpenCode 做一个命令行随机密码生成器。输入一条指令,它就能生成指定长度、指定强度的随机密码,支持排除易混淆字符(如 i/1/l),还能批量生成并显示密码熵值——让你一眼看出密码够不够安全。

  • 语言:Go(标准库,零依赖)
  • 代码量:94 行
  • 功能:自定义长度/字符集、排除混淆字符、批量生成、熵值估算

准备工作

确保本地已安装 Go(1.18+),然后打开终端进入工作目录。在 VS Code / Cursor / 任意终端中启动 OpenCode:

opencode

进入交互模式后,直接开始对话。

实践过程

第一步:一句话创建项目骨架

我对 OpenCode 说的第一句话:

> "用 Go 写一个命令行随机密码生成器。支持 -l 指定长度、-n 指定生成数量,使用 crypto/rand 保证密码学安全,不要任何外部依赖。"

OpenCode 直接生成了 main.go,包含 flag 参数解析和 crypto/rand 随机生成逻辑。核心生成函数长这样:

func generate(length int, charset string) (string, error) {
	var builder strings.Builder
	charsLen := len(charset)
	for i := 0; i < length; i++ {
		n, err := rand.Int(rand.Reader, big.NewInt(int64(charsLen)))
		if err != nil {
			return "", err
		}
		builder.WriteByte(charset[n.Int64()])
	}
	return builder.String(), nil
}

关键点:用 crypto/rand 而非 math/rand,因为 crypto/rand 从操作系统熵源读取,适合生成密码。math/rand 是伪随机,不适合安全场景。

第二步:增加易混淆字符排除

默认字符集中有些字符肉眼难以分辨——i1lIo0O0。我对 OpenCode 说:

> "加一个 --no-ambiguous 选项,排除 il1Lo0O 这些易混淆字符。同时加上 --no-upper、--no-digit、--no-symbol 选项让用户自定义字符集。"

OpenCode 新增了 removeAmbiguous 函数和 buildCharset 构造逻辑:

const ambiguous = "il1Lo0O"

func removeAmbiguous(s string) string {
	var result strings.Builder
	for _, c := range s {
		if !strings.ContainsRune(ambiguous, c) {
			result.WriteRune(c)
		}
	}
	return result.String()
}

这里我注意到了 OpenCode 的一个好习惯:它把字符集构造单独抽成 buildCharset 函数,而不是把所有逻辑堆在 main 里。即使是很小的项目,它也保持了良好的代码结构。

第三步:加入密码强度评估

密码安不安全,光看长度不够,关键看"熵"——组合可能性的数学度量。我说:

> "再加个密码熵值显示。非静默模式下输出 [强]/[中]/[弱] 标签和具体 bits 数。80 bits 以上算强,60-80 算中等,60 以下算弱。"

OpenCode 在打印逻辑中加入熵计算:

entropy := float64(*length) * math.Log2(float64(len(charset)))
if entropy >= 80 {
    fmt.Print("[强] ")
} else if entropy >= 60 {
    fmt.Print("[中] ")
} else {
    fmt.Print("[弱] ")
}
fmt.Printf("(%.0f bits) ", entropy)

同时加上了 -q 静默模式,方便在脚本中直接用 $(passgen -q) 取值。

第四步:编译测试

我对 OpenCode 说:

> "帮我初始化 Go module,编译并跑几个测试用例。"

OpenCode 执行了:

go mod init passgen
go build -o passgen .
./passgen -n 3 -l 20

输出结果:

[1] [强] (129 bits) bVx_nahS%#5Cif,OJT38
[2] [强] (129 bits) $e%Z@+p_XL0*=K3aO]4@
[3] [强] (129 bits) a0)4d%!BTXU@5LU$EF%$

20 位密码,字符集 87 个字符,熵值 129 bits——远超 80 bits 的安全基线。

再测试短密码和弱字符集:

./passgen -l 6 --no-upper --no-digit --no-symbol
# [弱] (28 bits) thhiys

只用 6 位小写字母,熵值仅 28 bits,立刻暴露出密码不安全。

总共从写下第一句提示词到拿到可运行的二进制文件,大约 4 分钟。

完整代码

package main

import (
	"crypto/rand"
	"flag"
	"fmt"
	"math"
	"math/big"
	"strings"
)

const (
	lowercase = "abcdefghijklmnopqrstuvwxyz"
	uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	digits    = "0123456789"
	symbols   = "!@#$%^&*()-_=+[]{}|;:,.<>?"
	ambiguous = "il1Lo0O"
)

func main() {
	length := flag.Int("l", 16, "密码长度")
	noUpper := flag.Bool("no-upper", false, "排除大写字母")
	noDigit := flag.Bool("no-digit", false, "排除数字")
	noSymbol := flag.Bool("no-symbol", false, "排除特殊符号")
	noAmbig := flag.Bool("no-ambiguous", false, "排除易混淆字符")
	count := flag.Int("n", 1, "生成数量")
	quiet := flag.Bool("q", false, "静默模式,只输出密码")
	flag.Parse()

	charset := buildCharset(*noUpper, *noDigit, *noSymbol, *noAmbig)

	for i := 0; i < *count; i++ {
		pass, err := generate(*length, charset)
		if err != nil {
			fmt.Fprintf(flag.CommandLine.Output(), "生成失败: %v\n", err)
			return
		}
		if !*quiet && *count > 1 {
			fmt.Printf("[%d] ", i+1)
		}
		if !*quiet {
			entropy := float64(*length) * math.Log2(float64(len(charset)))
			if entropy >= 80 {
				fmt.Print("[强] ")
			} else if entropy >= 60 {
				fmt.Print("[中] ")
			} else {
				fmt.Print("[弱] ")
			}
			fmt.Printf("(%.0f bits) ", entropy)
		}
		fmt.Println(pass)
	}
}

func buildCharset(noUpper, noDigit, noSymbol, noAmbig bool) string {
	charset := lowercase
	if !noUpper {
		charset += uppercase
	}
	if !noDigit {
		charset += digits
	}
	if !noSymbol {
		charset += symbols
	}
	if noAmbig {
		charset = removeAmbiguous(charset)
	}
	return charset
}

func removeAmbiguous(s string) string {
	var result strings.Builder
	for _, c := range s {
		if !strings.ContainsRune(ambiguous, c) {
			result.WriteRune(c)
		}
	}
	return result.String()
}

func generate(length int, charset string) (string, error) {
	var builder strings.Builder
	charsLen := len(charset)
	for i := 0; i < length; i++ {
		n, err := rand.Int(rand.Reader, big.NewInt(int64(charsLen)))
		if err != nil {
			return "", err
		}
		builder.WriteByte(charset[n.Int64()])
	}
	return builder.String(), nil
}

小结

这个项目的完整开发过程不到 4 分钟——从描述需求到拿到可用的二进制文件。整个过程我没有写一行 Go 代码,全部通过自然语言对话完成。

与传统开发方式的对比:

| 环节 | 手动开发 | 用 OpenCode |
|------|---------|------------|
| 参数解析 | 查 flag 文档、手写 | 一句话描述需求 |
| crypto/rand | 查文档确认 API | 自动选用正确库 |
| 熵值计算 | 查公式、手写 math.Log2 | 直接说"显示熵值" |
| 编译测试 | 手动 go build | OpenCode 一步执行 |

核心体会是:OpenCode 不是帮你"写代码",而是帮你"做项目"。你只需要描述你想要什么,它负责从代码编写到编译测试的全流程。这种体验对于 Prototype 阶段的小工具开发效率提升非常明显。

你可以把 passgen 安装到系统路径中日常使用:

go build -o passgen . && sudo mv passgen /usr/local/bin/

下次再也不用在网页上点"生成密码"了,终端里敲一行搞定。