Right, let’s talk about making things unreadable on purpose. Hashing is the workhorse of crypto, and Go’s crypto package gives you a solid, if slightly opinionated, toolbox. We’re not encrypting here—we’re taking some data, scrambling it beyond all recognition, and getting a fixed-size fingerprint. The key idea is that you can’t reverse it. You can’t take the fingerprint and get the original data back. This is perfect for checking if a file has been tampered with or, more commonly, for safely storing passwords (though we’ll get to the massive caveats there in a second).

First, a crucial distinction that trips up everyone at least once: hashing vs. HMAC. A regular hash (like SHA-256) is for data integrity. If I give you data and its hash, you can verify the data hasn’t changed. An HMAC (Hash-based Message Authentication Code) is for data integrity and authenticity. It proves the data hasn’t changed and that it came from someone with the same secret key. Use a regular hash for files; use HMAC for API tokens or cookies. If you use a regular hash for something that needs a secret, you’re asking for trouble.

Choosing Your Hash Function

The crypto package provides a smorgasbord of options, from the ancient (SHA1) to the modern (BLAKE2). Here’s the rule: avoid anything in the SHA family with a number smaller than 256. MD5 and SHA1 are broken. They’re museum pieces. Attackers can generate collisions—two different inputs with the same hash—with ease. Your only real choices today are SHA-256 and SHA-512. For most things, SHA-256 is perfectly sufficient.

package main

import (
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "log"
)

func main() {
    data := []byte("our super secret data")

    // The simple way: using sha256.Sum256
    // This returns a fixed-size [32]byte array, not a slice.
    hash := sha256.Sum256(data)
    fmt.Printf("Simple hash: %x\n", hash)

    // The streaming way: for larger data you don't want in memory all at once
    h := sha256.New()
    h.Write([]byte("part one"))
    h.Write([]byte("part two"))
    streamHash := h.Sum(nil) // Sum(nil) gives you the resulting slice
    fmt.Printf("Streamed hash: %s\n", hex.EncodeToString(streamHash))
}

The Right Way to Hash Passwords

This is the part everyone gets wrong, so pay attention. Never, ever use a plain cryptographic hash like SHA-256 for passwords. I mean it. The reason is that they are fast. Attackers love fast hashes because they can brute-force billions of passwords per second on a decent GPU. You need a slow hash, purpose-built for derailing brute-force attacks.

This is where golang.org/x/crypto/bcrypt comes in. It’s not in the standard library, but it’s the de facto standard for a reason. It handles the whole tedious process for you: generating a secure salt, iterating multiple times, and producing a self-contained hash string you can store directly in your database.

package main

import (
    "fmt"
    "log"
    "golang.org/x/crypto/bcrypt"
)

func main() {
    password := "myPassword123!"

    // Hashing the password with a default cost (currently 10)
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Hashed password: %s\n", hashedPassword)
    // Looks like: '$2a$10$azL42s6jTLV7qrb6aUyHee6Qk3uY6Z7H5b.5J5U5n5Y5Y5Y5Y5Y5Y'

    // Verifying a password against a hash
    inputPassword := "myPassword123!"
    err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(inputPassword))
    if err != nil {
        log.Fatal("Invalid password:", err) // This will fail if passwords don't match
    }
    fmt.Println("Password is correct!")
}

The bcrypt.DefaultCost is the work factor. As computers get faster, you bump this number up. It makes the hash slower to compute, which is exactly what you want. Don’t go crazy and set it to 20 on your web server, though, or you’ll DoS yourself during a login rush.

Using HMAC for Authentication

Remember that distinction? When you need to verify that a message is authentic, not just intact, you need an HMAC. It’s a hash that requires a secret key. The design is actually quite elegant, and Go’s interface for it is clean.

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
)

func main() {
    key := []byte("our-secret-key") // In reality, this should be a cryptographically random byte slice, not a string.
    message := []byte("authenticate this message")

    h := hmac.New(sha256.New, key)
    h.Write(message)
    mac := h.Sum(nil)

    fmt.Printf("HMAC: %s\n", hex.EncodeToString(mac))

    // To verify, you compute the HMAC of the received message with the same key
    // and use hmac.Equal to compare them (it's constant-time!).
    receivedMAC, _ := hex.DecodeString("expected_hmac_hex_string_here")
    receivedMessage := []byte("authenticate this message")

    h2 := hmac.New(sha256.New, key)
    h2.Write(receivedMessage)
    expectedMAC := h2.Sum(nil)

    if hmac.Equal(receivedMAC, expectedMAC) {
        fmt.Println("Message is authentic.")
    } else {
        fmt.Println("Message authentication failed!")
    }
}

Notice I used hmac.Equal and not ==. This is critical. A normal comparison might short-circuit and return false as soon as it finds a differing byte, leaking information about where the difference is through timing attacks. hmac.Equal takes the same amount of time no matter what, which makes it safe for crypto. It’s a small detail the library designers got right.

Common Pitfalls and Final Advice

  1. Salting: Bcrypt does it for you. If you ever roll your own (don’t), salting is non-negotiable. A salt prevents attackers from using precomputed “rainbow tables” against your hashes. Each password gets a unique, random salt.
  2. Key Generation for HMAC: Your HMAC key needs to be a cryptographically random value, not a string like “password123”. Use crypto/rand to generate it. make([]byte, 32) and then rand.Read(key) is your friend.
  3. Encoding: Hashes are []byte. You can’t store them nicely in a JSON field or a database text column. You need to encode them to a string. hex or base64 are your standard choices. hex.EncodeToString is your go-to for a readable string.
  4. Don’t Get Clever: The biggest pitfall is thinking you can outsmart decades of cryptographic research. Use bcrypt for passwords. Use HMAC-SHA256 for tokens. Don’t invent your own algorithm by combining three hashes and a partridge in a pear tree. The designers of these packages made choices for a reason, even if the API feels a bit spartan at times. Trust them. Your job is to use the tools correctly, not to become the tool yourself.