← Debugging Guides

阅读中文版本

Go filepath vs path — Windows Path Breaks on Linux

Fix path handling that breaks across operating systems by using path/filepath for filesystem paths and path for URL or slash-delimited keys.

beginnerdebugginggofilesystemcross-platform

Symptoms

  • A path built with \ works on Windows but not on Linux
  • filepath returned by one OS fails to open on another
  • Tests pass locally and fail in a Linux CI container

Possible causes

  • Hardcoded separators instead of filepath.Join
  • Using path.Join for filesystem paths, which always uses forward slashes on every OS
  • Comparing paths as strings across platforms where drive letters differ

Diagnosis steps

Print the produced path with quoting

fmt.Printf("%q\n", filepath.Join("a", "b", "c"))

Interpretation

The output uses the OS-specific separator; on Windows it contains a backslash.

Compare path versus filepath

go run - <<'EOF'
package main
import ("fmt"; "path"; "path/filepath")
func main(){ fmt.Println(path.Join("a","b")); fmt.Println(filepath.Join("a","b")) }
EOF

Interpretation

On Windows path.Join yields a/b while filepath.Join yields a\b, proving they are not interchangeable.

Root cause

path always uses "/" and is intended for URL or slash-delimited identifiers; path/filepath uses the separator of the build OS. Mixing them produces paths that only resolve on one platform.

Fix

Use path/filepath for all filesystem work (Join, Clean, Base, Rel) and reserve path for URL or slash-delimited keys; never concatenate separators by hand.

Risk note

Converting separators with Replace can corrupt paths containing legitimately odd names; let filepath handle it.

Verify the fix

Build and test on both platforms (native plus GOOS=linux go test with cross-compile) and confirm paths resolve; keep path handling in shared helpers.

Symptoms

This debugging guide addresses “Go filepath vs path — Windows Path Breaks on Linux”. Start from the symptoms, follow the ordered diagnostics, and confirm each hypothesis before moving on; the fix and verification steps then close the loop.

Observed symptoms include:

  • A path built with \ works on Windows but not on Linux
  • filepath returned by one OS fails to open on another
  • Tests pass locally and fail in a Linux CI container

Reproduce and Diagnose

Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.

Step 1: Print the produced path with quoting

fmt.Printf("%q\n", filepath.Join("a", "b", "c"))

The output uses the OS-specific separator; on Windows it contains a backslash.

Step 2: Compare path versus filepath

go run - <<'EOF'
package main
import ("fmt"; "path"; "path/filepath")
func main(){ fmt.Println(path.Join("a","b")); fmt.Println(filepath.Join("a","b")) }
EOF

On Windows path.Join yields a/b while filepath.Join yields a\b, proving they are not interchangeable.

Likely contributors, in order of frequency:

  • Hardcoded separators instead of filepath.Join
  • Using path.Join for filesystem paths, which always uses forward slashes on every OS
  • Comparing paths as strings across platforms where drive letters differ

Root Cause

path always uses “/” and is intended for URL or slash-delimited identifiers; path/filepath uses the separator of the build OS. Mixing them produces paths that only resolve on one platform.

The Fix

Use path/filepath for all filesystem work (Join, Clean, Base, Rel) and reserve path for URL or slash-delimited keys; never concatenate separators by hand.

Watch out: Converting separators with Replace can corrupt paths containing legitimately odd names; let filepath handle it.

import (
	"net/url"
	"path"
	"path/filepath"
)

// filesystem path
p := filepath.Join("data", "users", "alice.json")
f, _ := os.Open(p)

// URL path (always forward slash, OS independent)
u := &url.URL{Scheme: "https", Host: "api.example.com", Path: path.Join("v1", "users")}
fmt.Println(u.String()) // https://api.example.com/v1/users

Verify and Prevent

Build and test on both platforms (native plus GOOS=linux go test with cross-compile) and confirm paths resolve; keep path handling in shared helpers.

Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.

This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.