Go语言中比较PHP生成的MD5值

GoPHPMD5 by 达达 at 2012-05-31

在一些验证的场景经常会用到MD5或SHA1值的比较,Go语言中并没有PHP那样直接计算MD5和SHA1的函数,该如何得到和PHP格式一致的计算结果并进行比较呢?这篇文章描述这一问题的解决过程,顺便也演示一段代码是如何逐步优化性能的。

首先我们需要满足基本需求,计算出MD5值。我们知道MD5是通用算法,语言虽然不同但是算法相同,最终是可以得到相同结果的。例如以前我就贴过一篇笔记《C#中与PHP格式对应的md5函数》。

Go语言中提供了一个md5包,用法如下:

点击运行代码

package main

import "fmt"
import "crypto/md5"

func main() {
    h := md5.New()
    
    h.Write([]byte("1234"))
    
    x := h.Sum(nil)
    
    fmt.Println(x)
}

用以计算MD5的PHP实验代码:

php -r "echo md5('1234').\"\n\";"

我们用PHP计算字符串"1234"的MD5值,得到的是:

81dc9bdb52d04dc20036dbd8313ed055

Go语言计算出来的是一个字节数组,而PHP计算出来的是16进制表示的数据,如何把字节数组编码成16进制格式呢?

在fmt包中我找到一个办法:

点击运行代码

package main

import "fmt"
import "crypto/md5"

func main() {
    h := md5.New()
    
    h.Write([]byte("1234"))
    
    x := h.Sum(nil)
    
    y := fmt.Sprintf("%x", x)
    
    fmt.Println(y)
}

经过格式化后,y的值跟我们用PHP计算的结果就是一样的了。

那是不是可以打完收工了呢?当然不是。

fmt模块是通用化设计的,它有两个问题。

首先,进行格式化的时候内部需要解析理解"%x",然后再采取对应措施对x进行编码。

其次,我们计算的MD5结果长度是固定的,而fmt内部为了通用化必然是动态分配长度的。

以上两点都是优化空间,所以我继续在文档中找,终于找到encoding/hex包,它专门用来做16进制编码。

实验代码如下:

点击运行代码

package main

import "fmt"
import "crypto/md5"
import "encoding/hex"

func main() {
    h := md5.New()
    
    h.Write([]byte("1234"))
    
    x := h.Sum(nil)
    
    y := make([]byte, 32)
    hex.Encode(y, x)
    
    fmt.Println(string(y))
}

换成hex模块后,避免了动态解析,也利用了MD5值长度固定的特性,但是否还可以继续优化呢?

答案是可以。还有一个优化点没有体现在上面的代码中。

我们虽然计算出了和PHP格式相同的MD5值,但是如何才能高效的进行比较呢?

我们先用基本的方式,hex模块计算出的是字节数组,用bytes模块的函数进行比较,代码如下:

点击运行代码

package main

import "fmt"
import "bytes"
import "crypto/md5"
import "encoding/hex"

func main() {
    h := md5.New()
    
    h.Write([]byte("1234"))
    
    x := h.Sum(nil)
    
    y := make([]byte, 32)
    hex.Encode(y, x)
    
    z := []byte("81dc9bdb52d04dc20036dbd8313ed055")
    
    fmt.Println(bytes.Equal(y, z))
}

假设整个比较过程是我们自己实现的话,上面的代码有两个优化点。

首先,可以避免掉比较时和编码时的字节数组遍历,可以一边编码一边比较,减少遍历次数。

其次,当我们手头有比较目标的时候,我们可以不需要再创建一个存放计算结果的字节数组,拿编码结果和目标进行比较就可以。

按以上两个思路,我剖开hex模块的代码,取出Encode函数的实现代码,并进行改造,结果如下:

点击运行代码

package main

import "fmt"
import "crypto/md5"

func main() {
    h := md5.New()
    
    h.Write([]byte("1234"))
    
    x := h.Sum(nil)
    
    z := []byte("81dc9bdb52d04dc20036dbd8313ed055")
    
    fmt.Println(HexEncodeEqual(z, x))
}

const hextable = "0123456789abcdef"

func HexEncodeEqual(dst, src []byte) bool {
    for i, v := range src {
        if dst[i*2] != hextable[v>>4] || dst[i*2+1] != hextable[v&0x0f] {
            return false
        }
    }
    return true
}

以上就是一个简单的MD5值比较代码的优化过程。

运行效率和负载能力之于服务端,就像表现逻辑和表现能力之于客户端,追求更高的运行效率是服务端程序员的使命。

性能需要一点一点从细节中抠出来,但优化不是凭空的,优化是建立在需求的基础上的。过度的设计,会像内置的通用模块一样增加一些不必要的消耗。

通用意味着封装,封装意味着消耗,在功能性模块中进行通用化设计往往就变成通通不好用,只有正确认识当前场景下我们需要什么不需要什么,才能做出恰到好处的设计。