How to Use Bcrypt for Password Hashing in Go

Hash passwords in Go using the bcrypt package to securely store and verify user credentials without storing plain text.

The password blender

You are building a signup form. The user types a password and hits submit. The naive implementation saves that string directly into the database. If your database gets leaked, every user's password is exposed. The secure implementation transforms the password into a hash before storage. A hash is a one-way function. You can turn a password into a hash, but you cannot turn the hash back into the password. Bcrypt is the standard tool for this job in Go. It is slow by design, which makes brute-force attacks expensive for attackers.

Hashing is not encryption. Encryption is reversible if you have the key. Hashing is a one-way transformation. Think of a hash function as a specialized blender. You drop a password in, and the blender spits out a fixed-size string of gibberish. If you drop the exact same password in again, you get the exact same gibberish. You can never reverse the process to get the password back.

This creates a problem. If two users pick password123, they get the same hash. Attackers pre-compute hashes for common passwords in rainbow tables and look them up instantly. The fix is a salt. A salt is random data added to the password before hashing. It ensures that even identical passwords produce different hashes. Bcrypt generates a random salt for every password and embeds it inside the resulting hash string. You do not need to manage the salt separately. The hash string contains the salt, the cost parameter, and the actual hash.

Bcrypt is also slow. That is a feature, not a bug. Password hashing should take a noticeable fraction of a second. Slowness makes brute-force attacks impractical. An attacker trying a billion passwords per second on a fast hash function gets stopped cold when the hash function takes 200 milliseconds per attempt. Bcrypt is a key derivation function based on the Blowfish cipher. The cost parameter controls the computational work. Each increment of cost doubles the work. Cost 10 is 1024 iterations. Cost 12 is 4096 iterations. This exponential scaling allows bcrypt to remain secure as hardware improves.

Bcrypt handles the salt. You handle the cost.

Minimal example

Here is the core loop: generate a hash from a password, then verify a password against that hash.

package main

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

func main() {
	// Convert the password string to bytes for the bcrypt API.
	password := []byte("mySecretPassword")

	// GenerateFromPassword handles salt generation and hashing in one call.
	// bcrypt.DefaultCost is currently 10, which balances security and speed.
	hash, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
	if err != nil {
		// This error usually indicates an invalid cost parameter.
		panic(err)
	}

	// CompareHashAndPassword checks the password against the hash.
	// It extracts the salt and cost from the hash automatically.
	err = bcrypt.CompareHashAndPassword(hash, password)
	if err != nil {
		fmt.Println("Password does not match")
	} else {
		fmt.Println("Password matches")
	}
}

Anatomy of a hash

When you call bcrypt.GenerateFromPassword, the function does three things. First, it generates a random 16-byte salt. Second, it runs the bcrypt algorithm with the specified cost factor. Third, it encodes the salt, cost, and result into a single string. The output looks like $2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy.

The prefix $2a$ identifies the algorithm version. The 10 is the cost factor. The next 22 characters are the base64-encoded salt. The final 31 characters are the hash. You store this entire string in your database. You do not store the password. You do not store the salt separately.

The base64 encoding used here is not standard base64. It uses ./ instead of +/. This is a quirk of the original C implementation that Go preserves for compatibility. The string is self-contained. You can pass the hash string to CompareHashAndPassword and it will parse the salt and cost automatically.

Verification uses bcrypt.CompareHashAndPassword. This function parses the hash string to extract the salt and cost. It hashes the provided password with that salt and cost, then compares the result. The comparison is constant-time. It takes the same amount of time regardless of where the mismatch occurs. This prevents timing attacks where an attacker measures response times to guess the hash byte-by-byte.

The hash string is self-contained. Store it as is.

Realistic application flow

In a real application, hashing happens during registration, and verification happens during login. You will wrap these calls in functions that handle errors explicitly. Go's error handling style makes the failure path visible. If hashing fails, you return an internal server error. If verification fails, you return a generic "invalid credentials" message to avoid leaking user existence.

Here is a registration handler that hashes the password before saving.

package main

import (
	"net/http"
	"golang.org/x/crypto/bcrypt"
)

// User represents a user record in the database.
type User struct {
	Email    string
	Password string // Stores the bcrypt hash, not the plaintext.
}

// RegisterUser handles the signup request and hashes the password.
// RegisterUser validates input, hashes the password, and saves the user.
func RegisterUser(w http.ResponseWriter, r *http.Request) {
	// Parse the request body to get the password.
	// In production, use a struct and json.NewDecoder.
	password := r.FormValue("password")

	// Hash the password with the default cost factor.
	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		// Hashing errors are rare and usually indicate a bug.
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// Save the hash to the database.
	// The database column should be VARCHAR(60) to hold the full hash string.
	user := User{
		Email:    r.FormValue("email"),
		Password: string(hash),
	}

	// db.Save(&user) would go here.
	w.WriteHeader(http.StatusCreated)
}

The login handler verifies the password. Notice the error handling. CompareHashAndPassword returns bcrypt.ErrMismatch if the password is wrong. You should treat this the same as any other authentication failure.

// LoginUser verifies credentials and returns an error if they don't match.
// LoginUser retrieves the user, compares the password, and returns a generic error on failure.
func LoginUser(w http.ResponseWriter, r *http.Request) {
	password := r.FormValue("password")
	email := r.FormValue("email")

	// Retrieve the user from the database.
	// user := db.FindByEmail(email)
	// For this example, assume we have the stored hash.
	storedHash := []byte("$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy")

	// Compare the provided password against the stored hash.
	err := bcrypt.CompareHashAndPassword(storedHash, []byte(password))
	if err != nil {
		// Return a generic error. Don't reveal whether the user exists.
		http.Error(w, "Invalid credentials", http.StatusUnauthorized)
		return
	}

	// Password matches. Create a session or token.
	w.WriteHeader(http.StatusOK)
}

Upgrading the cost over time

Hardware gets faster. A cost of 10 that took 200 milliseconds in 2015 might take 20 milliseconds in 2025. You need a strategy to upgrade costs. The standard pattern is lazy upgrade. During login, after verifying the password, check the cost of the stored hash. If the cost is lower than your current threshold, generate a new hash with the higher cost and update the database. This spreads the computational load across login events. Users with weak costs get upgraded when they log in. You do not need a background migration job.

Here is how to inspect the cost of a stored hash.

// CheckCost demonstrates how to inspect the cost of a stored hash.
// CheckCost extracts the cost parameter from a bcrypt hash string.
func CheckCost(hash []byte) int {
	// Cost returns the cost parameter used to generate the hash.
	// It returns -1 if the hash is invalid.
	return bcrypt.Cost(hash)
}

In your login handler, you would call bcrypt.Cost(storedHash). If the result is less than your desired cost, you call GenerateFromPassword again and update the user record. This keeps your security current without downtime.

Lazy upgrades keep your security current without downtime.

Pitfalls and errors

The most common mistake is comparing hashes with ==. Never do this. String comparison in Go is not constant-time. An attacker can measure how long the comparison takes to determine how many leading bytes match. Always use bcrypt.CompareHashAndPassword.

Another pitfall is choosing the wrong cost factor. bcrypt.DefaultCost is a safe starting point. If your server is very fast, you might need to increase the cost to maintain a hashing time of around 200 to 300 milliseconds. If you set the cost too high, your login endpoint will become a denial-of-service vector. Users will time out waiting for authentication. Test the cost on your target hardware.

You might see the compiler complain with cannot use password (type string) as []byte value in argument if you forget to convert the password to bytes. The bcrypt API requires byte slices. Convert strings with []byte(password).

If you pass a cost factor outside the valid range, GenerateFromPassword returns an error. The valid range is 4 to 31. Passing 0 or 100 triggers a runtime error. The function returns bcrypt.InvalidCost in this case. Check the error before proceeding.

Database schema errors are also common. The bcrypt hash string is exactly 60 characters long. If your database column is VARCHAR(50), the hash gets truncated. Verification will fail silently because the truncated hash will not match. Use VARCHAR(60) or TEXT for the password column.

Never compare hashes with ==.

Decision matrix

Use bcrypt when you need a battle-tested, memory-hard-ish password hashing function with minimal configuration. It is the standard in Go and works well for most web applications.

Use argon2 when you require resistance against GPU and ASIC attacks. Argon2 is the winner of the Password Hashing Competition and uses more memory, making hardware attacks harder. The golang.org/x/crypto/argon2 package provides this algorithm.

Use pbkdf2 when you must support legacy systems that only accept PKCS#5 compliant hashes. PBKDF2 is older and less resistant to parallelization than bcrypt or argon2, but it is widely supported in older databases.

Use sha256 or md5 only for non-security checksums. Never use these for passwords. They are too fast and vulnerable to rainbow tables and brute-force attacks.

Where to go next