Verify file integrity with Go and SHA256
To verify a file in Go, stream its bytes into sha256.New() with io.Copy, then compare the digest
with a checksum obtained from a trusted source. The command-line program below can print a file’s
hash or verify an expected one, with a nonzero exit status when verification fails.
Start with a trusted checksum
SHA256 produces a 256-bit digest: 32 bytes, usually written as 64 hexadecimal characters.
Go’s standard library provides it through crypto/sha256.
A digest describes the file’s bytes; it is not a digital signature or a guarantee that the file is
safe to run.
For a download, obtain the expected checksum from the publisher’s trusted HTTPS page or a signed manifest whose signature you have verified with a trusted key. If someone can replace both the file and the expected checksum, they can make the comparison pass. Apache’s release verification guide explains the distinction between checking a hash and authenticating a release.
Build a SHA256 file checker
You need Go installed and a shell. These commands were tested with Go 1.27.1 on Linux using Bash. The program uses only the standard library, so there are no packages to download and no module setup is needed for this single-file build.
Create a new working directory. Continue only if this command succeeds; it refuses to reuse an existing directory:
mkdir sha256-example && cd sha256-example
Save the following as main.go inside that directory:
package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
)
func hashFile(path string) (sum []byte, err error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer func() {
if closeErr := file.Close(); closeErr != nil && err == nil {
sum, err = nil, closeErr
}
}()
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
return nil, err
}
return hash.Sum(nil), nil
}
func run(args []string) int {
if len(args) < 1 || len(args) > 2 {
fmt.Fprintln(os.Stderr, "Usage: sha256file FILE [EXPECTED_SHA256]")
return 2
}
var expected []byte
if len(args) == 2 {
var err error
expected, err = hex.DecodeString(args[1])
if err != nil || len(expected) != sha256.Size {
fmt.Fprintln(os.Stderr, "Expected SHA256 must be exactly 64 hexadecimal characters")
return 2
}
}
actual, err := hashFile(args[0])
if err != nil {
fmt.Fprintln(os.Stderr, "sha256file:", err)
return 2
}
if len(args) == 1 {
fmt.Printf("%x\n", actual)
return 0
}
if !bytes.Equal(actual, expected) {
fmt.Fprintf(os.Stderr, "FAIL: %q (SHA256 mismatch)\n", args[0])
return 1
}
fmt.Printf("OK: %q\n", args[0])
return 0
}
func main() {
os.Exit(run(os.Args[1:]))
}
os.Open opens the input for reading. io.Copy feeds the file’s bytes
to the hash until EOF or an error, and hash.Sum(nil) returns the
digest of those bytes. The deferred close runs before hashFile returns, including on read errors.
Only after run returns does main call os.Exit, which would otherwise
skip pending deferred calls.
The expected value is decoded and checked for the required length before reading the file.
Decoding also lets uppercase and lowercase hexadecimal checksums compare equally. This CLI compares
public checksums with bytes.Equal; it has no secret comparison value to protect. For keyed message
authentication, use a separate HMAC design and hmac.Equal.
Build the executable in the same directory. Rebuilding replaces an existing Go executable named
sha256file; choose a different output name if that path is already used for an unrelated file:
go build -o sha256file main.go
Verify a file and inspect the exit status
Create a small input containing exactly the three bytes abc, without a trailing newline, and hash
it. This command creates or overwrites the disposable example.txt in your working directory:
printf 'abc' > example.txt && ./sha256file example.txt
The output is:
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
Now verify it against that known checksum:
./sha256file example.txt ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
The program exits with status 0 and prints:
OK: "example.txt"
This known input demonstrates the mechanics. For a real download, pass its path and the publisher’s
trusted checksum; computing a new checksum from the downloaded file and comparing it with itself
cannot establish integrity. Quote paths that contain spaces. The second argument accepts only the
64-character digest, without a filename, sha256: prefix, or surrounding whitespace.
Replace the sample’s contents and check it against the original checksum. In an interactive Bash
shell without set -e, run these lines together so $? captures the checker’s status immediately:
printf 'changed\n' > example.txt &&
./sha256file example.txt ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
printf 'Exit status: %s\n' "$?"
You should see:
FAIL: "example.txt" (SHA256 mismatch)
Exit status: 1
The checker sends failures to standard error and successful results to standard output. Its exit codes let a script decide whether to continue:
| Status | Meaning |
|---|---|
0 | Hash printed, or the file matches the supplied checksum |
1 | File was read successfully, but its checksum differs |
2 | Invalid arguments, malformed checksum, or a file open, read, or close error |
Run the built executable when checking these codes; go run reports a child program’s failure
through the Go tool and does not preserve its exact exit status. The checker opens existing inputs
without modifying them and reports missing files instead of creating replacements.
Stream large files and keep the input stable
The same program handles an empty file, binary data, or a large archive. It processes bytes
incrementally instead of allocating a slice as large as the file. Every byte still has to be read,
so storage speed and hashing throughput affect the elapsed time. There is no universal buffer size
that makes this task fastest. Measure your actual workload before replacing io.Copy with custom
buffering, and account for filesystem caching when repeating a measurement.
Finish downloading or writing the file before hashing it, and keep it unchanged until the consumer uses it. This program does not lock the file or create a snapshot. A successful comparison checks the bytes that were read; it cannot stop another process from changing them afterward.
Common pitfalls
A mismatch can mean corruption, but it can also mean you used a checksum for another release, platform, or archive. Hash the exact file the publisher names: an archive and its extracted contents have different digests. Adding a newline or converting line endings also changes the bytes.
An empty file has a valid SHA256 digest and is not a read failure. It will still fail verification against the checksum of a nonempty download. If reading fails partway through, the program reports an error instead of comparing a partial digest. For a missing or inaccessible input, check the path and permissions before retrying; an I/O error does not establish whether the file matches.
