Sooner or later, every backend developer needs a hash. You want to verify that a downloaded backup was not corrupted, sign a webhook payload so the receiver can trust it, or spread cache keys evenly across a few Redis shards. All three of these problems are solved with hashing, and Go ships everything you need in its standard library, no third-party packages required.
The interesting part is that Go does not treat hashing as a bag of unrelated functions. Every hash in the standard library, from SHA-256 down to CRC-32, implements one small interface called hash.Hash. Once you understand that interface, you understand the whole mechanism, and every algorithm works the same way.
This tutorial is for developers who know basic Go syntax and want to use hashing correctly in real projects. You will learn how the hash.Hash interface works, how to compute a one-shot SHA-256 digest, how to hash a multi-gigabyte file without loading it into memory, how to sign and verify messages with HMAC, and when a fast non-cryptographic hash like FNV is the right tool. Everything runs on Ubuntu with the Go standard library only.
I wrote a similar guide for Node.js a while back, so if you work in both languages you can compare the two approaches: Various Types of Hashes Cryptography in NodeJS.
How Hashing Works in Go
A hash function takes an input of any size and produces a fixed-size output called a digest. The same input always produces the same digest, and for a cryptographic hash like SHA-256, you cannot practically reverse the digest back into the input or find two different inputs that produce the same digest.
In Go, the mechanism behind all of this is the hash.Hash interface from the hash package. It looks like this:
type Hash interface {
io.Writer
Sum(b []byte) []byte
Reset()
Size() int
BlockSize() int
}
Three things matter here:
- It embeds
io.Writer. A hash in Go is just something you write bytes into, exactly like a file or a network connection. This is the key design decision, because it means anything that can copy data to a writer (such asio.Copy) can feed a hash. That is how Go hashes huge files with constant memory. Sum(b)returns the digest. It appends the digest to the slice you pass in and returns the result. In practice you almost always callSum(nil)to get just the digest. CallingSumdoes not reset the hash, so you can keep writing more data afterwards if you want a running digest.Reset()puts the hash back to its initial state, so you can reuse one hash object for many inputs instead of allocating a new one each time.
The standard library then provides constructors that return this interface: sha256.New(), sha512.New(), fnv.New32a(), crc32.NewIEEE(), and so on. Swap the constructor and the rest of your code stays identical.
One important distinction before we start: cryptographic hashes (SHA-256, SHA-512) are designed to resist attackers and are what you use for integrity checks and signatures. Non-cryptographic hashes (FNV, CRC-32) are much faster but offer no security at all, and are meant for things like sharding and hash tables. We will use both in this tutorial, in the right places.
Prerequisites
- Ubuntu 20.04, 22.04, or 24.04 (any Linux works, but commands are tested on Ubuntu)
- Go 1.21 or newer installed
- Basic Go knowledge: packages, functions, slices
- Basic command-line knowledge
If you do not have Go yet, the quickest way on Ubuntu is apt:
sudo apt update
sudo apt install golang-go -y
Verify the installation:
go version
go version go1.26.4 linux/amd64
Any version from the last few years is fine, since everything in this tutorial has been in the standard library for a long time.
Step 1: Create the Project
Create a working directory and initialize a Go module. A module is required by modern Go tooling even when you only use the standard library:
mkdir ~/hashdemo && cd ~/hashdemo
go mod init hashdemo
go: creating new go.mod: module hashdemo
We will put each example in its own subfolder so you can run them independently with go run ./<folder>.
Step 2: Your First Hash, One-Shot SHA-256
When the data already fits in memory (an API token, a small JSON body), the simplest option is the one-shot helper sha256.Sum256. Create the file:
mkdir basic
nano basic/main.go
package main
import (
"crypto/sha256"
"fmt"
)
func main() {
data := []byte("hello world")
digest := sha256.Sum256(data)
fmt.Printf("%x\n", digest)
}
Run it:
go run ./basic
b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
Two details are worth explaining:
sha256.Sum256returns a[32]bytearray, not a slice. 32 bytes is the fixed output size of SHA-256, which is where the name comes from (256 bits).- The
%xverb formats the bytes as lowercase hexadecimal. Raw digest bytes are not printable, so hex encoding is the standard way to display or store them.
You can confirm Go agrees with the rest of the world using the sha256sum tool that ships with Ubuntu:
printf 'hello world' | sha256sum
b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 -
Same digest. Note the use of printf instead of echo, because echo appends a newline and a single extra byte produces a completely different digest. This property is called the avalanche effect, and it is exactly what makes hashes useful for integrity checking.
Step 3: Hash a Large File with Streaming
The one-shot helper needs all the data in memory, which is a bad idea for a 4 GB backup archive. This is where the hash.Hash interface earns its keep: because a hash is an io.Writer, you can stream a file through it with io.Copy and memory usage stays flat no matter how big the file is.
First, create a test file. We will use dd to make a 500 MB file so the result is reproducible on your machine:
dd if=/dev/zero of=backup-2026-07-04.tar.gz bs=1M count=500 status=none
Now the program:
mkdir filehash
nano filehash/main.go
package main
import (
"crypto/sha256"
"fmt"
"io"
"log"
"os"
)
func main() {
if len(os.Args) < 2 {
log.Fatal("usage: filehash <path>")
}
f, err := os.Open(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
log.Fatal(err)
}
fmt.Printf("%x %s\n", h.Sum(nil), os.Args[1])
}
The flow is: sha256.New() gives us a hash.Hash, io.Copy reads the file in chunks (32 KB by default) and writes each chunk into the hash, and h.Sum(nil) returns the final 32-byte digest. At no point is more than one chunk in memory.
Run it and compare against sha256sum:
go run ./filehash backup-2026-07-04.tar.gz
sha256sum backup-2026-07-04.tar.gz
a08a92258f621b55d08ad1e84c90c2ea6286fc6b6c9a4dfa7156afb16c190170 backup-2026-07-04.tar.gz
a08a92258f621b55d08ad1e84c90c2ea6286fc6b6c9a4dfa7156afb16c190170 backup-2026-07-04.tar.gz
Identical output, and the 500 MB file hashes in well under a second. This exact pattern is what you use in production to verify downloaded artifacts, deduplicate uploads, or generate content-addressed storage keys. If you build automated backups, publishing the SHA-256 digest next to each archive lets anyone verify the file later.
Step 4: Sign and Verify Messages with HMAC
A plain hash proves integrity, but not authenticity. Anyone can recompute SHA-256 over a tampered message and replace the digest. When two parties share a secret key, HMAC (Hash-based Message Authentication Code) solves this: the digest is computed from both the message and the key, so only someone holding the key can produce a valid signature. This is how webhook signatures from GitHub, Stripe, and most payment gateways work.
mkdir hmacdemo
nano hmacdemo/main.go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func sign(secret, message []byte) string {
mac := hmac.New(sha256.New, secret)
mac.Write(message)
return hex.EncodeToString(mac.Sum(nil))
}
func verify(secret, message []byte, signature string) bool {
expected, err := hex.DecodeString(signature)
if err != nil {
return false
}
mac := hmac.New(sha256.New, secret)
mac.Write(message)
return hmac.Equal(mac.Sum(nil), expected)
}
func main() {
secret := []byte("mysupersecret")
message := []byte(`{"order_id":1234,"amount":50000}`)
signature := sign(secret, message)
fmt.Println("signature:", signature)
fmt.Println("valid message:", verify(secret, message, signature))
tampered := []byte(`{"order_id":1234,"amount":99999}`)
fmt.Println("tampered message:", verify(secret, tampered, signature))
}
Run it:
go run ./hmacdemo
signature: 8a9ab778c9022f79dd76587e51b8fbbae0e0e2d16298b23c0146e40d65955a36
valid message: true
tampered message: false
Notice that hmac.New takes the hash constructor sha256.New as an argument, not a hash instance. This is the hash.Hash mechanism again: HMAC is built on top of any hash that implements the interface, so HMAC-SHA512 is just hmac.New(sha512.New, secret).
The other critical detail is hmac.Equal. It compares the two MACs in constant time, meaning the comparison takes the same amount of time whether the first byte differs or the last one. A plain == or bytes.Equal comparison returns early on the first mismatch, and attackers can measure that timing difference over many requests to recover a valid signature byte by byte. Always use hmac.Equal for signature checks.
You can cross-check the signature with OpenSSL to prove Go is doing standard HMAC-SHA256:
printf '{"order_id":1234,"amount":50000}' | openssl dgst -sha256 -hmac "mysupersecret"
SHA2-256(stdin)= 8a9ab778c9022f79dd76587e51b8fbbae0e0e2d16298b23c0146e40d65955a36
Step 5: Fast Non-Cryptographic Hashing with FNV
Not every hashing problem involves an attacker. If you want to assign cache keys to one of four Redis shards, you need speed and even distribution, not cryptographic strength. Go’s hash/fnv package implements the FNV-1a algorithm, which is tiny, fast, and returns an integer directly, so there is no digest slice to convert.
mkdir fnvdemo
nano fnvdemo/main.go
package main
import (
"fmt"
"hash/fnv"
)
func shardFor(key string, shards uint32) uint32 {
h := fnv.New32a()
h.Write([]byte(key))
return h.Sum32() % shards
}
func main() {
keys := []string{"user:1001", "user:1002", "user:1003", "user:1004", "user:1005"}
for _, key := range keys {
fmt.Printf("%s -> shard %d\n", key, shardFor(key, 4))
}
}
Run it:
go run ./fnvdemo
user:1001 -> shard 2
user:1002 -> shard 3
user:1003 -> shard 0
user:1004 -> shard 1
user:1005 -> shard 2
Every run, on every machine, produces the same mapping, which is exactly what you need for sharding: any instance of your application computes the same shard for the same key. Note that fnv.New32a() still returns a writer you call Write on, the same mechanism as SHA-256, just with a Sum32() convenience method on top.
Do not use FNV for anything security-related. It is trivial for an attacker to craft keys that all land in one shard (a hash-flooding attack). If untrusted users control the keys and that worries you, the standard library also has hash/maphash, which is seeded randomly per process for exactly this reason. The trade-off is that maphash values are not stable across restarts, so it fits in-memory hash tables, while FNV fits stable sharding of trusted keys.
Common Mistakes and Troubleshooting
Comparing signatures with == instead of hmac.Equal. The code works, the tests pass, and you have a timing side channel in production. Any time you compare a secret-derived value against user input, use hmac.Equal (it works for any two byte slices, not just HMACs).
Hashing the hex string instead of the raw bytes. A classic bug: one side compares raw digest bytes while the other compares a hex string, or worse, hashes the hex representation again. Decide on one encoding at your API boundary (hex is the common choice) and decode back to bytes before comparing.
Expecting Sum() to reset the hash. Sum(nil) returns the digest of everything written so far but does not clear the state. If you reuse a hash for a second input without calling Reset(), the second digest silently covers both inputs concatenated. If you see “wrong digest” bugs in a loop, a missing Reset() is the first thing to check.
Reading whole files with os.ReadFile before hashing. It works in development with 10 MB files, then your container gets OOM-killed in production on a 6 GB archive. Use the io.Copy streaming pattern from Step 3 for anything that comes from disk or the network.
Using SHA-256 to store passwords. A GPU can attempt billions of SHA-256 guesses per second, so fast hashes are exactly the wrong tool for passwords, even with a salt. Use golang.org/x/crypto/bcrypt or golang.org/x/crypto/argon2, which are deliberately slow and memory-hard. That topic deserves its own article, so here just remember: integrity and signatures use SHA-256, passwords never do.
A different digest than expected from shell tests. Nine times out of ten it is a trailing newline added by echo, or a Windows-edited file with \r\n line endings. Compare byte counts first (wc -c) before suspecting the hash code.
Best Practices
- Default to SHA-256. It is fast, universally supported, and has no known practical attacks. Avoid MD5 and SHA-1 in new code, since both have real collision attacks, and their presence tends to trip security scanners like Trivy even when the usage is harmless.
- Pick the hash by threat model, not by speed. Attacker involved: SHA-256 or HMAC-SHA256. No attacker, stable output needed: FNV. No attacker, in-memory only:
hash/maphash. - Keep HMAC secrets out of the source code. Load them from an environment variable or a secrets manager, and plan for rotation by accepting two valid keys during a rotation window.
- Hash before you encode. Internally, work with
[]bytedigests, and convert to hex only at the edges (logs, JSON, headers). This avoids the encoding-mismatch bugs described above. - Publish checksums with your artifacts. If you ship backups or release binaries, generate a
SHA256SUMSfile next to them. It costs one command and turns “the download looks corrupted” tickets into a one-line verification. - Reuse hash objects in hot paths. In a tight loop, call
Reset()and reuse the samehash.Hashinstead of allocating a new one per item. The interface is designed for exactly this.
Conclusion
You now know the mechanism behind every hash in Go: one small hash.Hash interface that embeds io.Writer, so hashing is just writing bytes. On top of that mechanism you built four real tools. A one-shot SHA-256 digest for small data, a streaming file checksummer that matches sha256sum and uses constant memory, an HMAC signer and verifier with constant-time comparison for webhook-style authentication, and an FNV-based shard router for cache keys.
The same interface extends further than this tutorial. crypto/sha512, hash/crc32, and third-party hashes like BLAKE2 all plug into the identical New, Write, Sum pattern, so you already know how to use them. The natural next step is storing passwords, which needs deliberately slow algorithms instead of the fast hashes covered here, and I wrote a dedicated follow-up for exactly that: Password Hashing in Golang with Bcrypt and Argon2. And if you also work with Node.js, my earlier article Various Types of Hashes Cryptography in NodeJS covers the same ideas from the JavaScript side.