原创分享 Go 实战 | 让你的 flag 支持从文件中读取命令行参数

yudotyang · 2021年11月06日 · 82 次阅读

大家好,我是 Go 学堂的渔夫子。今天给大家介绍一个在项目中如何将命令行参数组织到文件中并进行解析的案例

golang 标准库提供了 flag 包来处理命令行参数。常规的使用都是在命令行中启动服务的时候一一的输入,让程序解析。今天给大家介绍一种可以从文件中读取命令行参数的实现方法。

01 flag 的常规应用

下面我们通过代码来演示下 flag 的常规应用。如下代码:

var (
    RedisAddress string
)

func init() {
    flag.StrVar(&RedisAddress, "redis_address", "127.0.0.1", "this is redis address")
}

func main() {
    flag.Parse()
    if RedisAddress != "" {
        //redis初始化操作
    }
    fmt.Printf("redis address:%s\n", RedisAddress)
}

然后在命令行中进行编译或直接运行时要指定-redis_address 参数,如下:

go run main.go -redis_address=redisaddr.goxuetang.com

随着项目规模的增大,需要的命令行参数越来越多,假设有 50 个命令行参数甚至更多,如果我们一个一个指定的话,可想而知会是一件多么可怕的事情:参数多,难以维护,容易出错。下面我们就介绍通过让程序从配置文件中读取的方法。

02 通过文件读取命令行参数的 flag 应用

常规应用中,我们看到,读取并解析命令行参数的逻辑主要在 flag.Parse 中。我们对 flag.Parse() 进一步查看,看到源码包中 flag.Parse() 函数实际上是调用了 CommandLine.Parse(arguments [] string) error 函数,如下:

func Parse() {
    // Ignore errors; CommandLine is set for ExitOnError.
    CommandLine.Parse(os.Args[1:])
}

通过上面代码可知,os.Args[1:] 就是命令行后跟的所有参数的集合(在上面的例子中就是 [-redis_address=redis-test.goxuetang.com]),然后 CommandLine.Parse 对该字符串集合进行实际的解析。

那我们要实现的目标实际上就是将文件中的每一行读取出来,组织成 CommandLine.Parse 函数可接收的参数即可。如下图所示 flag 常规解析和读取文件方式的示意图:

好了,思路讲清楚后,我们来看下代码实现

03 代码实现

我们将实现的函数封装在 flagx 的包中,本文意图是讲解实现的思路,所以在代码中忽略了错误处理,大家在实际项目中自行添加即可。

package flagx

//存储命令行传过来的文件路径
var FlagFile string

func init() {
    //注册命令行的flagfile参数
    flag.Var(&FlagFile, "flagfile", "")
}

//在Parse函数中调用,将解析到的命令行参数打印出来
func visitFlag(f *flag.Flag) {
    fmt.Println(f.Name + "=" + f.Value.String())
}

func Parse() error {
    //先解析命令行中的-flagfile参数
    flag.Parse()

    var validFlagLines []string

    flagContents, _ := ioutil.ReadFile(FlagFile)

    configContent := string(flagContents)
    // 统一使用\n作为换行符,以便后面按分隔符分隔字符串成切片
    configContent = strings.Replace(configContent, "\r\n", "\n", 10000)
    flagLines := strings.Split(configContent, "\n")
    for _, line := range flagLines {
        //忽略掉以 # 开头的注释行
        if len([]rune(line)) != 0 && string([]rune(line)[0]) != "#" {
            //将每一行作为一个有效的命令行参数
            validFlagLines = append(validFlagLines, line)
        }
    }

    //实际执行解析命令行参数的地方,这里就又和常规的flag调用一样了
    _ := flag.CommandLine.Parse(validFlagLines)

    //主动在命令行设置的参数具有更高的优先级,会覆盖掉配置文件中相同的命令行参数
    flag.Parse()

    flag.VisitAll(visitFlag)
    return nil
}

假设命令行参数文件存在于文件/data/conf/prod.gflags 中,内容如下:

# redis地址
-redis_address=redisaddr.goxuetang.com 
# redis端口
-redis_port=9999

# 其他所有的命令行参数

好,写个 main 函数测试一下,main 函数中引入的 gotech.github.com/m/flagfile/flagx 包是我项目下的定义路径,大家在实际开发中根据自己的项目组织包路径即可。

package main
import (
    "flag"
    "fmt"
    "gotech.github.com/m/flagfile/flagx"
)
var (
    RedisAddress string
    RedisPort    int
)

func init() {
    flag.StringVar(&RedisAddress, "redis_address", "127.0.0.1", "this is redis address")
    flag.IntVar(&RedisPort, "redis_port", 6379, "this is redis port")
}

func main() {
    //这里调用我们自定实现的Parse函数
    err := flagx.Parse()
    fmt.Println("err:", err)


    if RedisAddress != "" && RedisPort != 0 {
        //redis初始化操作
    }
    fmt.Printf("redis address:%s,port:%d\n", RedisAddress, RedisPort)
}

执行如下命令

go run main.go -flagfile=/data/conf/prod.gflags

04 总结

和常规的 flag 应用相比,将命令行参数写在配置文件中,可以提高命令行参数的可读性以及可维护性。该方法的实现思路主要是应用了 flag.Parse 解析命令行参数底层的 CommandLine.Parse(arguments [] string) 的函数功能,将文件中的每行内容视为一个命令行参数,并将所有的行组织成一个切片,然后调用 CommandLine.Parse 方法即完成了整个过程的解析。

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册