Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
188: path.Join and path.Clean
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
rootstring and auserInputstring. - It must use
path.Jointo combine them. - The resulting path must be cleaned (no double slashes, no
..segments). - If the
userInputattempts to "break out" of the root (e.g., by using../../../etc/passwd),path.Joinwill 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 ofpath.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
There are no comments for now.