Skip to Content
Course content

188: path.Join and path.Clean

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this bug a dozen times in production code. A developer is building a simple resource loader or a virtual file system and thinks, "I'll just use string concatenation to build my paths. It's just a few strings and some slashes."

Here is a piece of code that looks perfectly reasonable at a glance. It takes a base directory and a user-provided path to resolve a final resource location:

package main

import "fmt"

func resolveResource(base, userPath string) string {
    // Simple concatenation to build the path
    return base + "/" + userPath
}

func main() {
    baseDir := "/app/static"
    
    // User A provides a clean path
    fmt.Println(resolveResource(baseDir, "images/logo.png")) 
    // Output: /app/static/images/logo.png (Looks great!)

    // User B provides a path with a leading slash
    fmt.Println(resolveResource(baseDir, "/configs/settings.json")) 
    // Output: /app/static//configs/settings.json (Wait, double slash?)

    // User C tries to be clever with relative jumps
    fmt.Println(resolveResource(baseDir, "images/../icons/fav.ico")) 
    // Output: /app/static/images/../icons/fav.ico (The OS might handle this, but our cache won't)
}

The Double-Slash and Dot-Dot Headache

If you're just printing these strings to a console, you might not care. But in a real application, these "dirty" paths cause nightmares. If you use these strings as keys in a map for caching, /app/static/images/logo.png and /app/static//images/logo.png are treated as two different files, even though they point to the same place. Worse, allowing .. (parent directory) references in paths can lead to path traversal vulnerabilities if you aren't careful.

The problem here is that we're treating paths as mere strings. A path isn't just a string; it's a logical structure. When you manually add "/", you're guessing about whether the surrounding strings already have slashes. It's a losing game.

Cleaning the Mess with path.Join and path.Clean

The path package in Go is designed specifically for slash-separated paths (regardless of the operating system). The path.Join function is your best friend here. It doesn't just concatenate; it cleans the result.

Check out how we fix the resolveResource function:

package main

import (
    "fmt"
    "path"
)

func resolveResource(base, userPath string) string {
    // Join handles the slashes and calls Clean() automatically
    return path.Join(base, userPath)
}

func main() {
    baseDir := "/app/static"
    
    fmt.Println(resolveResource(baseDir, "images/logo.png")) 
    // Output: /app/static/images/logo.png

    fmt.Println(resolveResource(baseDir, "/configs/settings.json")) 
    // Output: /app/static/configs/settings.json (Double slash gone!)

    fmt.Println(resolveResource(baseDir, "images/../icons/fav.ico")) 
    // Output: /app/static/icons/fav.ico (The ".." was resolved!)
}

What happened under the hood? path.Join takes all the arguments, joins them with a single slash, and then runs path.Clean on the result. path.Clean applies a set of lexical rules to simplify the path:

  • It replaces multiple slashes with a single one.
  • It eliminates each . (current directory) element.
  • It resolves each .. (parent directory) element by removing the preceding element.
  • It removes trailing slashes unless the path is just /.

I should mention a quick distinction: I used the path package here because we are dealing with generic slash-separated paths (like URLs or internal app paths). If you were dealing with actual files on a local Windows or Linux disk, you'd use path/filepath. The logic is almost identical, but filepath respects the specific separators of the OS you're running on (like backslashes on Windows).




📋 Practical Task

Building a Virtual Asset Route Resolver

You are building a system that maps virtual URLs to an internal asset structure. You need to create a function called SanitizeAssetPath that ensures no matter how messy the input is, the output is a clean, canonical path.

Requirements:

  • The function should take a root string and a userInput string.
  • It must use path.Join to combine them.
  • The resulting path must be cleaned (no double slashes, no .. segments).
  • If the userInput attempts to "break out" of the root (e.g., by using ../../../etc/passwd), path.Join will still resolve the .. elements, but it won't magically stop the path from starting with /. For this exercise, simply ensure the final output is the result of path.Join(root, userInput).

Test your function with these inputs:

root := "/assets"
input1 := "css/main.css"         // Expected: /assets/css/main.css
input2 := "//js///app.js"        // Expected: /assets/js/app.js
input3 := "images/./avatars/../logo.png" // Expected: /assets/images/logo.png
Rating
0 0

There are no comments for now.

to be the first to leave a comment.