Golang标准库源码学习-hash/fnv | 青训营

840 阅读4分钟

简介

Go语言的hash库提供了一系列用于数据哈希的功能。哈希函数是将输入数据映射为固定大小的哈希值的算法。哈希函数可以用于各种用途,包括数据完整性验证、密码学安全、数据索引和散列表等。

Go语言的hash库提供了多个哈希函数的实现,包括常见的哈希算法如MD5、SHA-1、SHA-256等,以及一些非加密哈希函数如Adler-32、CRC-32等。这些哈希函数可以通过调用相应的方法来计算给定数据的哈希值。

golang的hash库,大致可以完成以下任务

  1. 数据完整性验证:通过计算数据的哈希值,可以在数据传输或存储过程中验证数据的完整性。接收方可以重新计算哈希值并与发送方提供的哈希值进行比较,以确定数据是否被篡改。
  2. 密码学安全:哈希函数在密码学中起到重要作用。比如,存储密码时,通常不会将明文密码存储在数据库中,而是将密码的哈希值存储起来。当用户登录时,输入的密码经过哈希计算后与存储的哈希值进行比较,以验证密码的正确性。
  3. 数据索引:哈希函数可以用于数据索引和散列表的实现。通过将数据的哈希值作为索引,可以快速查找、插入和删除数据。
  4. 散列算法:哈希函数还可以用于散列算法,将数据映射到散列桶中。这在分布式系统中常用于数据分片、负载均衡等场景。

顶层接口

type Hash interface {
    // Write (via the embedded io.Writer interface) adds more data to the running hash.
    // It never returns an error.
    io.Writer// Sum appends the current hash to b and returns the resulting slice.
    // It does not change the underlying hash state.
    Sum(b []byte) []byte
​
    // Reset resets the Hash to its initial state.
    Reset()
​
    // Size returns the number of bytes Sum will return.
    Size() int
​
    // BlockSize returns the hash's underlying block size.
    // The Write method must be able to accept any amount
    // of data, but it may operate more efficiently if all writes
    // are a multiple of the block size.
    BlockSize() int
}
​
// Hash32 is the common interface implemented by all 32-bit hash functions.
type Hash32 interface {
    Hash
    Sum32() uint32
}
​
// Hash64 is the common interface implemented by all 64-bit hash functions.
type Hash64 interface {
    Hash
    Sum64() uint64
}

Hash是所有Hash算法的顶层接口。

  • io.WriterWrite(p []byte) (n int, err error)方法用于将外部数据p通过指定的哈希算法计算后覆盖当前的哈希值。
  • Sum(b []byte) []byte 方法会将哈希值添加到b的末尾,添加的字节个数就是Size()返回的大小,一般用于进行校验和的操作验证数据的完整性。
  • Resize()用于重置hash值到初始值
  • Size() 返回Sum方法添加到b数组末尾的字节大小,一般都是固定的,比如Hash32的Size就是4,Hash64的Size就是8.一般也就是哈希值所占的字节数。
  • BlockSize() int哈希算法底层每次操作的块大小,你在一些哈希算法里面可能会看到很多的位操作,如果这些位操作是对int8操作,那么BlockSize()的返回值就是1,如果每次位操作都是对一个int64,那么BlockSIze()的返回值就是8.

Hash32类型的hash值是一个uint32类型,同理,Hash64类型的hash值是一个uint64类型。

fnv

FNV算法主要分为FNV-1 hashFNV-1a hash两者区别不大,唯一的区别就是算法中执行乘法和异或操作的顺序相反。

fnv不是加密hash.

我们先来看看golang的fnv包中的这两种算法的实现,我们以sum32sum32a为例:

func (s *sum32) Write(data []byte) (int, error) {
    hash := *s
    for _, c := range data {
        hash *= prime32
        hash ^= sum32(c)
    }
    *s = hash
    return len(data), nil
}
​
func (s *sum32a) Write(data []byte) (int, error) {
    hash := *s
    for _, c := range data {
        hash ^= sum32a(c)
        hash *= prime32
    }
    *s = hash
    return len(data), nil
}

我们可以清晰地看到,fnv算法非常简单,基本上只有两步操作:异或和乘法

具体来说fnv-1的主要原理就是:

  • fnv算法的hash初始值是一个fnv偏移基值,该初始值在通过New32和New32a的时候赋予
  • 遍历data字节数组
  • 每次遍历时都用当前hash值去乘上一个fnv素数得到新值,并且将其与当前遍历的字节值进行异或操作。

并且由于它是异或一个字节,因此除了低8位以外的都是0,0异或其他数都会得到原数,所以每次异或操作只会影响低8位。

至于fnv-1a算法,就是将异或操作和乘上一个fnv素数操作的顺序反过来。

在这个过程中你可能发现了,在这个算法中有两个比较重要的参数,分别是fnv偏移基值和fnv素数。以下是其参数表:

image-20230815221825008.png

也可以去福勒-诺尔-沃哈希函数 - 维基百科 (wikipedia.org)查看。