判断一个文件是否存在是一个相当常见的需求,在golang中也有多种方案实现这一功能。
现在我们介绍其中两种最常用也是最简单的实现,第一种将是跨平台通用的,而第二种则在POSIX平台上通用。
跨平台实现的思路很简单,如果某个文件不存在,那么使用os.Lstat就一定会返回error,只要判断error是否代表文件不存在即可。
也许你注意到了有些代码会使用os.Open来完成上述工作,不过最好不要这么做,因为虽然两者完成的功能没有区别,但open和stat的调用开销是不同的,后者要小于前者,而且对于判断文件是否存在,检查它的元数据要比直接尝试打开它更加合理。
那么来看看实现的代码:
1 2 3 4 |
func FileExist(path string) bool { _, err := os.Lstat(path) return !os.IsNotExist(err) } |
代码很简单,对于Windows/Linux/MacOS等是通用的,一般没有特殊需求我也比较推荐这种实现。
如果你的程序是面向POSIX平台的(例如UNIX、Linux等),那么还有一种更简单的方案——syscall.Access。
syscall.Access提供了用户检查文件元信息的手段,通常它被用来检查文件权限以及文件的存在性。
通过使用syscall.F_OK标志检查文件,如果不存在则会返回和os.Lstat一样的error:
1 2 3 4 |
func FileExist(path string) bool { err := syscall.Access(path, syscall.F_OK) return !os.IsNotExist(err) } |
这种实现的最大优势在于它简单而直观,但是它无法在Windows上使用。
一些提示
首先当我们的FileExist返回true时,其实文件并不一定存在。
当我们对目标path中的某一部分没有可读权限时,os.Lstat和syscall.Access同样会返回error,不过这个error不会让os.IsNotExist返回true。
当文件不存在而你对文件所在的目录或者它的上层目录没有访问权限时,FileExist依旧会返回true,bug就在这时发生了。所以重要的一点是在判断文件是否存在前应该先判断自己对文件及其路径是否有访问权限。
其次syscall.Access只会使用运行程序的用户的uid和gid,这会导致setuid之类的权限失效,通常来说这是没什么问题的,然而posix平台上一般都会考虑euid和egid,因此你可能需要使用syscall.Faccessat做代替。你需要在深思熟虑后使用合适的系统调用。
最后我们看看两个方案的性能,我们以os.Open做为基准,分别测试先文件存在和不存在时的性能表现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
func checkWithOpen(path string) bool { f, err := os.Open(path) if err != nil { return false } f.Close() return true }
func checkWithLstat(path string) bool { _, err := os.Lstat(path) return !os.IsNotExist(err) }
func checkWithAccess(path string) bool { err := syscall.Access(path, syscall.F_OK) return !os.IsNotExist(err) }
func BenchmarkNotExists(b *testing.B) { for range b.N { checkWithOpen("/home/apocelipes/no-") } }
func BenchmarkNotExistsLstat(b *testing.B) { for range b.N { checkWithLstat("/home/apocelipes/no-") } }
func BenchmarkNotExistsAccess(b *testing.B) { for range b.N { checkWithAccess("/home/apocelipes/no-") } }
func BenchmarkExists(b *testing.B) { for range b.N { checkWithOpen("/home/apocelipes/.zshrc") } }
func BenchmarkExistsLstat(b *testing.B) { for range b.N { checkWithLstat("/home/apocelipes/.zshrc") } }
func BenchmarkExistsAccess(b *testing.B) { for range b.N { checkWithAccess("/home/apocelipes/.zshrc") } } |
这是结果:
测试使用的文件系统类型是XFS。
可以看到open是最慢的,lstat比access慢了16%左右。从结果里也可以看到lstat需要额外返回一个os.FileInfo结构导致了额外的内存分配,所以整体上速度更慢。
但考虑到跨平台以及兼容性,使用os.Lstat是更常见的做法。