Golang Md5的4种写法与源码简单分析

1,358 阅读2分钟

1. md5算法简介

md5算法全称Message Digest Algorithm 5,中文名消息摘要算法第五版,主要用于文件或数据的完整性校验。我们在日常的研发中通常会见到32位的字符串。

md5算法具有以下特点:

  1. md5算法可输入任意长度的数据,输出均为128bit的二进制数据。
  1. 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
}