
本文详解 go 中使用 `crypto/sha1` 计算文件 sha1 哈希时常见错误——尤其是空文件场景下因误写入字节导致哈希不一致的问题,并提供标准、可复用的文件哈希实现。
在 Go 中调用 crypto/sha1 计算哈希时,一个典型误区是混淆「输入数据」与「文件内容」。例如,你执行了 touch test.txt 创建了一个真正的空文件(长度为 0 字节),而 OpenSSL 正确地对其计算出标准空字符串 SHA1:da39a3ee5e6b4b0d3255bfef95601890afd80709。但你的 Go 代码却写入了 []byte{0x00}(1 字节),这实际计算的是单字节 \x00 的哈希(结果为 5ba93c9db0cff93f52b521d7420e43f6eda2784f),自然不匹配。
✅ 正确做法是:对空文件,不应调用 hash.Write(),或显式传入空切片 []byte{}(等价于 nil)。sha1.New() 初始化后直接调用 hash.Sum(nil) 即可得到空输入的哈希:
package main
import (
"crypto/sha1"
"fmt"
)
func main() {
hash := sha1.New()
// ✅ 空文件对应空输入:无需 Write,或 Write([]byte{})
// hash.Write([]byte{}) // 显式写入空切片(可选,效果同无操作)
result := hash.Sum(nil)
fmt.Printf("SHA1(empty) = %x\n", result) // 输出: da39a3ee5e6b4b0d3255bfef95601890afd80709
}⚠️ 注意事项:
- hash.Write([]byte{0x00}) ≠ 空文件,它等价于 echo -n $'\x00' | openssl sha1,结果完全不同;
- hash.Write(nil) 是安全的,Go 的 hash.Hash 接口明确支持 nil 切片(视为零长度输入);
- 实际读取文件时,务必使用 io.Copy 或逐块读取,避免内存加载全量内容(尤其大文件)。
以下是生产就绪的命令行 SHA1 工具示例,行为完全对标 openssl sha1:
package main
import (
"crypto/sha1"
"fmt"
"io"
"log"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "usage: %s \n", os.Args[0])
os.Exit(1)
}
f, err := os.Open(os.Args[1])
if err != nil {
log.Fatal("open:", err)
}
defer f.Close()
hash := sha1.New()
if _, err := io.Copy(hash, f); err != nil {
log.Fatal("read:", err)
}
fmt.Printf("%x\n", hash.Sum(nil))
} 编译运行后验证:
$ touch test.txt $ go run sha.go test.txt da39a3ee5e6b4b0d3255bfef95601890afd80709 $ openssl sha1 test.txt SHA1(test.txt)= da39a3ee5e6b4b0d3255bfef95601890afd80709
二者输出完全一致。核心原则始终如一:哈希值只取决于输入字节流的内容和长度,而非文件名、权限或元数据;Go 中必须忠实还原该字节流,而非凭直觉构造测试数据。










