How to Work with File Permissions in Go

Use os.Stat to read and os.Chmod to change file permissions in Go.

The file is locked, but you have the key

You write a script that generates a configuration file with database credentials. It works perfectly on your laptop. You deploy it to a shared server, and the script crashes because the user lacks write access to the target directory. Or worse, the script succeeds, but it creates a file that anyone on the system can read. The credentials leak. File permissions are the gatekeepers of your filesystem. Go handles them with a mix of Unix tradition and type safety. You need to understand how to read, set, and inspect permissions to write secure and reliable code.

Permissions are bits, not strings

File permissions in Go are not strings like "rwxr-xr-x". They are bits packed into a number. The type fs.FileMode carries these bits. You will see numbers like 0644 or 0755. Those are octal numbers. Each digit represents a group: owner, group, and others. Each bit in the digit controls read, write, or execute access.

The octal system is the standard because it maps cleanly to the three-bit groups. A digit 7 means 111 in binary, which is read, write, and execute. A digit 6 means 110, which is read and write. A digit 4 means 100, which is read only. When you see 0644, the leading zero tells Go this is octal. The 6 gives the owner read and write. The 4 gives the group read only. The last 4 gives others read only.

Go uses the fs.FileMode type to wrap these bits. This type prevents you from passing a random integer where permissions are expected. It also carries extra bits for file type information, like whether the path is a directory or a symlink.

Read and change permissions

The standard library provides os.Stat to read file metadata and os.Chmod to modify permissions. os.Stat returns a fs.FileInfo interface. You call .Mode() on that interface to get the full mode, then .Perm() to extract just the permission bits.

Here's the core loop: stat the file to read its mode, then chmod to change it.

package main

import (
	"fmt"
	"os"
)

func main() {
	// Check permissions of an existing file.
	info, err := os.Stat("secret.txt")
	if err != nil {
		// Handle missing file or permission denied.
		fmt.Println(err)
		return
	}

	// Perm() extracts just the permission bits from the full mode.
	// The full mode includes type bits like directory or symlink.
	fmt.Println("Permissions:", info.Mode().Perm())

	// Change permissions to owner read/write only.
	// 0600 is octal: owner rw, group none, others none.
	err = os.Chmod("secret.txt", 0600)
	if err != nil {
		fmt.Println(err)
		return
	}
}

Perm() gives you the mask. Chmod applies it.

The anatomy of fs.FileMode

The fs.FileMode value contains more than just permissions. It includes type bits that describe what the file system object is. You can check if a path is a directory, a regular file, or a symbolic link using methods on the mode.

This example shows how to distinguish file types using the same mode value.

// CheckType demonstrates how to inspect file modes beyond permissions.
// It prints whether a path is a directory, a symlink, or a regular file.
func CheckType(path string) {
	info, err := os.Stat(path)
	if err != nil {
		fmt.Println(err)
		return
	}

	// Mode() returns the full fs.FileMode including type bits.
	mode := info.Mode()

	// IsDir() checks the ModeDir bit.
	if mode.IsDir() {
		fmt.Println("Directory")
	} else if mode&os.ModeSymlink != 0 {
		// Bitwise AND checks if the symlink bit is set.
		// os.ModeSymlink is a constant with the symlink bit.
		fmt.Println("Symlink")
	} else {
		fmt.Println("Regular file")
	}
}

Mode carries type and permissions. Check both.

Create files with secure permissions

A common mistake is creating a file with default permissions and then changing them. This creates a race condition. Between the creation and the permission change, another process might read the file. The secure pattern is to set permissions at creation time. os.OpenFile accepts a mode argument that applies immediately.

Real code needs to handle creation securely. This function writes data while locking down permissions from the start.

// WriteConfig creates a config file with restrictive permissions.
// It returns an error if the file cannot be created or written.
func WriteConfig(path string, data []byte) error {
	// 0600 ensures only the owner can read or write the file.
	// This prevents other users on the system from accessing secrets.
	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		return err
	}

	// Close the file explicitly to ensure handles are released.
	// defer is idiomatic for cleanup in Go.
	defer f.Close()

	_, err = f.Write(data)
	return err
}

The community accepts the if err != nil boilerplate because it makes the unhappy path visible. Don't hide errors. Set permissions at creation. Race conditions leak secrets.

The umask trap

You might request 0777 permissions for a directory, expecting full access for everyone. The file ends up with 0755. This is not a bug. The kernel applies a umask to every file creation. The umask is a process-level setting that masks out bits from the mode you request. If the umask is 0022, the kernel removes write permission for group and others.

Go does not provide a portable way to change the process umask. You can use syscall.Umask on Unix systems, but that affects the entire process and is rarely the right solution. The kernel masks your requests. Respect the umask. If you need specific permissions, calculate the mode that survives the umask, or accept that the umask is a security feature.

Pitfalls and errors

The biggest trap is octal literals. If you write 755, Go treats that as decimal seven hundred fifty-five. That is not what you want. You must prefix with 0 to get octal: 0755. If you forget the zero, the compiler won't stop you, but the permissions will be wrong. 755 decimal is 1363 octal, which sets garbage bits. The compiler complains with cannot use "755" (untyped string constant) as fs.FileMode value in argument if you pass a string. If you pass a decimal integer, the conversion happens silently, and you get unexpected permissions.

Another pitfall involves symbolic links. os.Chmod follows symlinks. If you call os.Chmod on a symlink, you change the permissions of the target file, not the link itself. Go's standard library does not include os.Lchmod. If you need to change permissions on a symlink, you must use syscall.Lchmod, which is platform-specific and not available on Windows.

Octal starts with zero. Symlinks follow targets.

Decision matrix

Use os.Stat when you need to read the current permissions of a file or directory. Use os.Chmod when you need to update permissions on an existing file. Use os.OpenFile with a mode argument when you are creating a new file and want to set permissions atomically. Use os.MkdirAll with a mode argument when you need to create a directory tree with specific permissions. Use fs.FileMode constants like fs.ModePerm when you want readable code instead of magic numbers. Use syscall.Lchmod when you must change permissions on a symbolic link itself, though this requires platform-specific code.

Pick the tool that matches the lifecycle. Create with mode, update with chmod.

Where to go next