简介

今天我们来看一个很小,很实用的库go-homedir。顾名思义,go-homedir用来获取用户的主目录。
实际上,使用标准库os/user我们也可以得到这个信息:

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "os/user"
  6. )
  7. func main() {
  8. u, err := user.Current()
  9. if err != nil {
  10. log.Fatal(err)
  11. }
  12. fmt.Println("Home dir:", u.HomeDir)
  13. }

那么为什么还要go-homedir库?

在 Darwin 系统上,标准库os/user的使用需要 cgo。所以,任何使用os/user的代码都不能交叉编译。
但是,大多数人使用os/user的目的仅仅只是想获取主目录。因此,go-homedir库出现了。

快速使用

go-homedir是第三方包,使用前需要先安装:

  1. $ go get github.com/mitchellh/go-homedir

使用非常简单:

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "github.com/mitchellh/go-homedir"
  6. )
  7. func main() {
  8. dir, err := homedir.Dir()
  9. if err != nil {
  10. log.Fatal(err)
  11. }
  12. fmt.Println("Home dir:", dir)
  13. dir = "~/golang/src"
  14. expandedDir, err := homedir.Expand(dir)
  15. if err != nil {
  16. log.Fatal(err)
  17. }
  18. fmt.Printf("Expand of %s is: %s\n", dir, expandedDir)
  19. }

go-homedir有两个功能:

  • Dir:获取用户主目录;
  • Expand:将路径中的第一个~扩展成用户主目录。

高级用法

由于Dir的调用可能涉及一些系统调用和外部执行命令,多次调用费性能。所以go-homedir提供了缓存的功能。默认情况下,缓存是开启的。
我们也可以将DisableCache设置为false来关闭它。

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "github.com/mitchellh/go-homedir"
  6. )
  7. func main() {
  8. homedir.DisableCache = false
  9. dir, err := homedir.Dir()
  10. if err != nil {
  11. log.Fatal(err)
  12. }
  13. fmt.Println("Home dir:", dir)
  14. }

使用缓存时,如果程序运行中修改了主目录,再次调用Dir还是返回之前的目录。如果需要获取最新的主目录,可以先调用Reset清除缓存。

实现

go-homedir源码只有一个文件homedir.go,今天我们大概看一下Dir的实现,去掉缓存相关代码:

  1. func Dir() (string, error) {
  2. var result string
  3. var err error
  4. if runtime.GOOS == "windows" {
  5. result, err = dirWindows()
  6. } else {
  7. // Unix-like system, so just assume Unix
  8. result, err = dirUnix()
  9. }
  10. if err != nil {
  11. return "", err
  12. }
  13. return result, nil
  14. }

判断当前的系统是windows还是类 Unix,分别调用不同的方法。先看 windows 的,比较简单:

  1. func dirWindows() (string, error) {
  2. // First prefer the HOME environmental variable
  3. if home := os.Getenv("HOME"); home != "" {
  4. return home, nil
  5. }
  6. // Prefer standard environment variable USERPROFILE
  7. if home := os.Getenv("USERPROFILE"); home != "" {
  8. return home, nil
  9. }
  10. drive := os.Getenv("HOMEDRIVE")
  11. path := os.Getenv("HOMEPATH")
  12. home := drive + path
  13. if drive == "" || path == "" {
  14. return "", errors.New("HOMEDRIVE, HOMEPATH, or USERPROFILE are blank")
  15. }
  16. return home, nil
  17. }

流程如下:

  • 读取环境变量HOME,如果不为空,返回这个值;
  • 读取环境变量USERPROFILE,如果不为空,返回这个值;
  • 读取环境变量HOMEDRIVEHOMEPATH,如果两者都不为空,拼接这两个值返回。

类 Unix 系统的实现稍微复杂一点:

  1. func dirUnix() (string, error) {
  2. homeEnv := "HOME"
  3. if runtime.GOOS == "plan9" {
  4. // On plan9, env vars are lowercase.
  5. homeEnv = "home"
  6. }
  7. // First prefer the HOME environmental variable
  8. if home := os.Getenv(homeEnv); home != "" {
  9. return home, nil
  10. }
  11. var stdout bytes.Buffer
  12. // If that fails, try OS specific commands
  13. if runtime.GOOS == "darwin" {
  14. cmd := exec.Command("sh", "-c", `dscl -q . -read /Users/"$(whoami)" NFSHomeDirectory | sed 's/^[^ ]*: //'`)
  15. cmd.Stdout = &stdout
  16. if err := cmd.Run(); err == nil {
  17. result := strings.TrimSpace(stdout.String())
  18. if result != "" {
  19. return result, nil
  20. }
  21. }
  22. } else {
  23. cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid()))
  24. cmd.Stdout = &stdout
  25. if err := cmd.Run(); err != nil {
  26. // If the error is ErrNotFound, we ignore it. Otherwise, return it.
  27. if err != exec.ErrNotFound {
  28. return "", err
  29. }
  30. } else {
  31. if passwd := strings.TrimSpace(stdout.String()); passwd != "" {
  32. // username:password:uid:gid:gecos:home:shell
  33. passwdParts := strings.SplitN(passwd, ":", 7)
  34. if len(passwdParts) > 5 {
  35. return passwdParts[5], nil
  36. }
  37. }
  38. }
  39. }
  40. // If all else fails, try the shell
  41. stdout.Reset()
  42. cmd := exec.Command("sh", "-c", "cd && pwd")
  43. cmd.Stdout = &stdout
  44. if err := cmd.Run(); err != nil {
  45. return "", err
  46. }
  47. result := strings.TrimSpace(stdout.String())
  48. if result == "" {
  49. return "", errors.New("blank output when reading home directory")
  50. }
  51. return result, nil
  52. }

流程如下:

  • 先读取环境变量HOME(注意 plan9 系统上为home),如果不为空,返回这个值;
  • 使用getnet命令查看系统的数据库中的相关记录,我们知道passwd文件中存储了用户信息,包括用户的主目录。使用getent命令查看passwd中当前用户的那条记录,然后从中找到主目录部分返回;
  • 如果上一个步骤失败了,我们知道cd后不加参数是直接切换到用户主目录的,而pwd可以显示当前目录。那么就可以结合这两个命令返回主目录。

这里分析源码并不是表示使用任何库都要熟悉它的源码,毕竟使用库就是为了方便开发。
但是源码是我们学习和提高的一个非常重要的途径。我们在使用库遇到问题的时候也要有能力从文档或甚至源码中查找原因。

参考

  1. home-dir GitHub 仓库