1. md5算法简介
md5算法全称Message Digest Algorithm 5,中文名消息摘要算法第五版,主要用于文件或数据的完整性校验。我们在日常的研发中通常会见到32位的字符串。
md5算法具有以下特点:
- md5算法可输入任意长度的数据,输出均为128bit的二进制数据。
- md5算法不可逆(或者说碰撞概率非常小)
2. Golang md5包
在golang中,我们可以通过crypto/md5
来直接调用md5算法。
常见的调用方式有以下四种:
func MD5_1(s string) string {
hash := md5.New()
_, err := hash.Write([]byte(s))
if err != nil {
panic(err)
}
sum := hash.Sum(nil)
return fmt.Sprintf("%x\n", sum)
}
func MD5_2(s string) string {
sum := md5.Sum([]byte(s))
return fmt.Sprintf("%x\n", sum)
}
func MD5_3(s string) string {
hash := md5.New()
_, _ = io.WriteString(hash, s)
return hex.EncodeToString(hash.Sum(nil))
}
func MD5_4(s string) string {
sum := md5.Sum([]byte(s))
return hex.EncodeToString(sum[:])
}
我们通过基准测试(benchmark)来比较四种写法的性能。
➜ go test -bench .
goos: darwin
goarch: arm64
BenchmarkMD5_1-10 5461878 217.1 ns/op
BenchmarkMD5_2-10 4637865 258.8 ns/op
BenchmarkMD5_3-10 5880980 205.1 ns/op
BenchmarkMD5_4-10 8062526 147.0 ns/op
PASS
ok _/Users/code/project/md5_test/source 7.441s
我们可以看出方法4的性能最优,因此我们更推荐这种写法。
3.Golang 分块计算大文件md5
我们有时候还需要面对下载大文件的情况,而此时我们可以考虑分块计算,从而降低时间消耗。
func Md5check2(fileName, md5FileName string) (error, bool) {
file, err := os.Open(fileName)
if err != nil {
return err, false
}
defer file.Close()
md5String, err := ioutil.ReadFile(md5FileName)
if err != nil {
return err, false
}
md5Bytes, _ := hex.DecodeString(string(md5String))
return md5Check_2(fileName, md5Bytes)
}
func md5Check_2(fileName string, md5sum []byte) (error, bool) {
fileMd5, err := calcMd5_2(fileName)
if err != nil {
return err, false
}
if !bytes.Equal(fileMd5, md5sum) {
return errors.New("the md5 check failed, fileMD5 not equal to md5sum"), false
}
return nil, true
}
func calcMd5_2(fileName string) ([]byte, error) {
f, err := os.Open(fileName)
if err != nil {
return nil, err
}
defer f.Close()
const bufferSize = 65536
hash := md5.New()
for buf, reader := make([]byte, bufferSize), bufio.NewReader(f); ; {
n, err := reader.Read(buf)
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
hash.Write(buf[:n])
}
return hash.Sum(nil), nil
}
4. crypto/md5源码分析
md5包中定义了如下两个常量:
// The size of an MD5 checksum in bytes. md5校验和字节数
const Size = 16
// The blocksize of MD5 in bytes. md5字节块大小
const BlockSize = 64
我们调用md5.New()
方法时,会返回一个名为digest的结构体。
func New() hash.Hash {
d := new(digest)
d.Reset()
return d
}
type digest struct {
s [4]uint32
x [BlockSize]byte
nx int
len uint64
}
我们调用md5.Sum()
方法,可以直接返回md5值。
func Sum(data []byte) [Size]byte {
var d digest
d.Reset()
d.Write(data)
return d.checkSum()
}
我们可以看到,md5.Sum()
内部主要是通过write方法来实现。
func (d *digest) Write(p []byte) (nn int, err error) {
// Note that we currently call block or blockGeneric
// directly (guarded using haveAsm) because this allows
// escape analysis to see that p and d don't escape.
nn = len(p)
d.len += uint64(nn)
if d.nx > 0 {
n := copy(d.x[d.nx:], p)
d.nx += n
if d.nx == BlockSize {
if haveAsm {
block(d, d.x[:])
} else {
blockGeneric(d, d.x[:])
}
d.nx = 0
}
p = p[n:]
}
if len(p) >= BlockSize {
n := len(p) &^ (BlockSize - 1)
if haveAsm {
block(d, p[:n])
} else {
blockGeneric(d, p[:n])
}
p = p[n:]
}
if len(p) > 0 {
d.nx = copy(d.x[:], p)
}
return
}