引言:
在 Web 开发中,处理 URL 路径是一项常见的任务。但由于用户输入、路由规则等原因,URL 路径可能会包含多余的斜杠、点号.和点点…等元素,导致路径不规范。为了确保正确的路径解析和处理,httprouter 实现了 CleanPath
方法。本文将深入解析 CleanPath
方法的实现原理,重点关注其处理逻辑和处理边界情况的技巧。
CleanPath
函数是 httprouter
包中的一部分,用于消除 URL 路径中的多余元素。它通过逐个字符遍历路径,根据不同的字符情况,对路径进行规范化和清理。 CleanPath 函数定义。
// CleanPath is the URL version of path.Clean, it returns a canonical URL path
// for p, eliminating . and .. elements.
func CleanPath(p string) string {
// ... CleanPath 的实现代码 ...
}
CleanPath 函数的处理逻辑可以拆分为以下几个步骤:
2.1 处理多个连续斜杠
如果路径中存在多个连续的斜杠(),将它们替换为单个斜杠(/)。例如:/path//to///resource -> /path/to/resource
for r < n {
switch {
case p[r] == '/':
// 处理多个连续斜杠,将它们替换为单个斜杠
r++
// 其他分支...
}
}
2.2 消除当前目录路径
将路径中的 . 删除,表示当前目录。例如:/path/./to/resource -> /path/to/resource
for r < n {
switch {
// .是最后一个元素时,要在链接最后的链接添加/。例如:/foot/tar/.-> /foot/tar/
case p[r] == '.' && r+1 == n:
// 消除当前目录路径,将 `.` 删除
trailing = true
r++
// 其他分支...
}
}
2.3. 消除上级目录路径
将路径中的 … 及其前面的路径元素删除,表示上级目录。例如:/path/to/…/resource -> /path/resource
for r < n {
switch {
// 其他分支...
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
// 消除上级目录路径,将 `..` 及其前面的路径元素删除
r += 3
if w > 1 {
// 可以回退,将 `w` 向前移动一位,直到找到前一个 `/` 为止
w--
//buf空间未缓冲字符是,此时相当于用p[:w]这个空间缓存着字符
if len(buf) == 0 {
for w > 1 && p[w] != '/' {
w--
}
} else {
for w > 1 && buf[w] != '/' {
w--
}
}
}
// 其他分支...
}
}
2.4. 处理空路径
如果输入路径为空字符串,则 CleanPath 函数会将其转换为根路径(/)。
例如: -> /
// 处理空路径,将其转换为根路径 `/`
if p == "" {
return "/"
}
2.5 处理路径不以斜杠(/)开头
如果路径不以斜杠(/)开头,函数会自动添加斜杠,确保输出的路径以斜杠开头。
例如:path/to ->/path/to
// 只有当遇到待处理字符时,如./ // ../等是,才会使用到buf,其他情况,不使用buf
// r 是读取索引,用于指示当前正在处理的 URL 路径的下一个字节的位置。
// w 是写入索引,用于指示要写入到 buf 切片中的下一个字节的位置,用途是buf中没有内容时,如果p[:w]字符串与p待写入的字符串相同时,就不用向buf空间中写入内容一值标记着p[:w]位置。
// 为何从1开始,而不是从0开始?认为第一个通过将 r 和 w 从 1 开始,我们可以在处理路径时更方便地处理第一个路径元素,而不需要特殊处理开头的斜杠。这样可以简化代码逻辑,并且在处理后续路径元素时也能保持一致的处理方式。
r:=1
w:=1
// 处理路径不以 `/` 开头,添加 `/` 到输出路径的开头
if p[0] != '/' {
// r对应执行的是处理路径中的字符,最初时赋值为1,表示跳过了/,此时第一个字符不是/,所以需要将r赋值为0
// 让r每次都对应着字符的处理,而不包含/处理,可以使逻辑更为简化
r = 0
// 长度超过buff,重新分配空间
if n+1 > stackBufSize {
buf = make([]byte, n+1)
} else {
// buff大小是当前字符串长度+1就足以。通过buf[:n+1]来分配空间,相比make
// 避免额外的内存分配: 切片切取的方式不会创建新的底层数组,而是调整现有底层数组的长度。这意味着不需要额外的内存分配,避免了内存的浪费。注意:buff原有空间的长度要>=n+1时,才可如此操作
buf = buf[:n+1]
}
buf[0] = '/'
}
2.6 将字符存储于惰性缓冲区
// Internal helper to lazily create a buffer if necessary.
// Calls to this function get inlined.
func bufApp(buf *[]byte, s string, w int, c byte) {
b := *buf
if len(b) == 0 {
// 如果原始字符串未被修改过
// 如果下一个字符与原始字符串中的字符相同,表示无需修改,直接返回
// 相当于s[:w]这个空间充当了一个临时缓冲区,存储相应字符
if s[w] == c {
return
}
// 否则,根据需要使用栈缓冲区或在堆上分配新缓冲区,并将之前的字符复制进去
if l := len(s); l > cap(b) {
*buf = make([]byte, len(s))
} else {
*buf = (*buf)[:l]
}
b = *buf
// 将之前的字符复制进新缓冲区
copy(b, s[:w])
}
// 将新字符追加到缓冲区
b[w] = c
}
CleanPath 函数还需考虑一些特殊情况和边界条件,以确保输出的路径正确和合理。
3.1 处理路径结尾的斜杠
如果输入路径结尾有斜杠(/path/to/resource/),则在输出路径结尾也添加一个斜杠,以保持一致。例如:/path/to/resource/ -> /path/to/resource/
// Re-append trailing slash
if trailing && w > 1 {
bufApp(&buf, p, w, '/')
w++
}
3.2. 消除以 … 开头的路径元素
如果输入路径以 … 开头(…/path/to/resource),表示位于上级目录之外,将其删除。例如:…/path/to/resource -> /path/to/resource
// 其他分支...
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
// 消除以 `..` 开头的路径元素
r += 3
if w > 1 {
// 可以回退,将 `w` 向前移动一位,直到找到前一个 `/` 为止
w--
if len(buf) == 0 {
for w > 1 && p[w] != '/' {
w--
}
} else {
for w > 1 && buf[w] != '/' {
w--
}
}
}
// 其他分支...
CleanPath 函数在实现中采用了一些性能优化技巧,使用缓冲区来减少内存分配。避免不必要的字符串拼接,从而提高处理效率。我们将深入探讨这些优化措施,并了解它们如何提升 CleanPath 函数的性能。
在 Go 语言标准库中,也提供了 path.Clean 函数用于清理路径。虽然 CleanPath 和 path.Clean 都用于路径规范化,但它们之间存在一些细微的差异。
path.Clean 是标准库中的函数,用于规范化文件路径。而 CleanPath 是 httprouter 包中的函数,用于规范化 URL 路径。它们的功能相似,但适用的场景不同。
path.Clean 是对文件路径进行规范化的函数,它主要用于处理文件系统中的路径,比如读取文件、写入文件等操作。
CleanPath 则是专门用于处理 URL 路径的规范化函数,它适用于 Web 开发中的路由处理和 URL 解析等场景。
我们深入解析了 CleanPath 方法,它作为一个简单的 URL 路径规范化工具,背后却涉及较多的处理逻辑。在 Web 开发中,处理 URL 路径可能看似简单,但实际上涉及到许多细节和边界条件的考虑。写代码就是如此,我们必须仔细思考各种场景和条件,确保代码的正确性和稳定性。
写代码不仅仅是实现功能,更重要的是要考虑到各种场景和条件。在 Web 开发中,URL 路径处理可能只是一个看似简单的环节,但它直接影响着系统的稳定性和性能。因此,我们应该在写代码时注重细节,善于思考和排查各种可能出现的情况,保证代码的鲁棒性和可靠性。
httprouter