在编写 Go 程序的代码时,总是有很多变量时需要我们不断调整的,例如定时任务的轮询实践,API 的调用超时时间,还有一些 Flag 的打开关闭设置等,我们有很多方式可以设置它们,也有很多方式在代码中传递,有些方式挺好,但是有些方式却不是那么令人满意。在我的实践中,我发现有一些可以拿来借鉴的东西,所以这篇文章就总结一下关于代码环境参数变量传递的一些实践总结。

在开始之前,有一点需要说明一下,虽然后面我介绍的内容都是基于原始的 flags 和 main 函数的,但是,实际上即使你使用了类似于 Cobra 之类的 CMD 框架也无所谓,因为可以很简单得转换成 flags。可能有其他语言经验的同学会想要去区分一下 argumentsoptionsflags,这个在其他语言中有这种区分方式:

然而,Go 程序是不仔细区分这些的,通通都是通过 flags 包处理。这里先来介绍一个简单得 CMD 程序,这段程序很简单,简单到只会打印出你输入的参数:

这里有个小 tips 就是指定默认值,可以看到,这里既让 name 作为参数的值接收变量,同时,默认值也让 name 来提供。这是一个非常简单的传递 string 类型的示例,除了 string 之外,flags 还支持很多类型,下面就列举一下:

这里有个 Duration 很好用,因为如果是自己解析的话,得让用户传递个 string 过来,然后再自己解析,而且 Duration 在大多数情况下用得比较多,例如 timeout 和 interval,下面就对 Duration 的使用也来个小例子:

然后执行一下:

[root@liqiang.io]# go run simple-01.go -sleep.duration 2s
2019/05/11 14:54:23 simple-01.go:17: Ready to sleep
2019/05/11 14:54:25 simple-01.go:19: Sleep done!

运行良好,和我们预想的一样,而且可以很方便得使用。

传递参数

OK,现在参数已经从用户传入到我们的代码之中了,下一步很需要从 main 里面传递给具体的业务代码,当只有一两个参数的时候还可以很简单的传递,但是例如 prometheus 这样的包含大量参数的应用,就不能简单得传递参数了:

 [root@liqiang.io]# prometheus -h 
usage: prometheus [<flags>]

The Prometheus monitoring server

Flags:
  -h, --help                     Show context-sensitive help (also try --help-long and --help-man).
      --version                  Show application version.
      --config.file="prometheus.yml"  
                                 Prometheus configuration file path.
      --web.listen-address="0.0.0.0:9090"  
                                 Address to listen on for UI, API, and telemetry.
      --web.read-timeout=5m      Maximum duration before timing out read of the request, and closing idle connections.
      --web.max-connections=512  Maximum number of simultaneous connections.
      --web.external-url=<URL>   The URL under which Prometheus is externally reachable (for example, if Prometheus is served via a reverse proxy). Used for
                                 generating relative and absolute links back to Prometheus itself. If the URL has a path portion, it will be used to prefix all
                                 HTTP endpoints served by Prometheus. If omitted, relevant URL components will be derived automatically.
      --web.route-prefix=<path>  Prefix for the internal routes of web endpoints. Defaults to path of --web.external-url.
      --web.user-assets=<path>   Path to static asset directory, available at /user.
      --web.enable-lifecycle     Enable shutdown and reload via HTTP request.
      --web.enable-admin-api     Enables API endpoints for admin control actions.
      --web.console.templates="consoles"  
                                 Path to the console template directory, available at /consoles.
      --web.console.libraries="console_libraries"  
                                 Path to the console library directory.
      --storage.tsdb.path="data/"  
                                 Base path for metrics storage.
      --storage.tsdb.retention=15d  
                                 How long to retain samples in the storage.
      --storage.tsdb.no-lockfile  
                                 Do not create lockfile in data directory.
      --alertmanager.notification-queue-capacity=10000  
                                 The capacity of the queue for pending alert manager notifications.
      --alertmanager.timeout=10s  
                                 Timeout for sending alerts to Alertmanager.
      --query.lookback-delta=5m  The delta difference allowed for retrieving metrics during expression evaluations.
      --query.timeout=2m         Maximum time a query may take before being aborted.
      --query.max-concurrency=20  
                                 Maximum number of queries executed concurrently.
      --log.level=info           Only log messages with the given severity or above. One of: [debug, info, warn, error]

所以我们不妨来学习一下 prometheus 是如何传递参数的:

从这里可以看到,prometheus 对于参数的管理不是直接传递变量,而是赋值的结构体的值,我下面用一个简单的 demo 来简化一下:

这里就用一个结构体作为参数的接收者,然后直接将结构体作为参数传递给其他业务,这样无论是参数的分类还是参数的管理都会比较简单和清晰。

不推荐的实践

当然,在 promtheus 中还有一种参数传递方式,那就是全局变量赋值,例如:

而这里的 version 是另外一个包:github.com/prometheus/common/version 中的全局变量:

这虽然是一种实践,但是我个人不太推荐,因为这会让变量的管理失控,你没法知道变量是从何处来的。反之,如果我将这些参数作为 NewTsdb(version Version) 传递进来,那么我就很清楚,这些参数都是在创建的时候传进来的,而不是被某个包设置了全局变量来的,而且这个参数是什么值我也可以把握,不会在运行的时候突然被某个业务修改掉。

小结

本文就 Go 命令行应用的参数传递和使用做了一个简单的实践介绍,同时也举了两个例子用于介绍实际中是如何使用的,希望对你有所帮助,如果你有更好的实践,欢迎留言大家一起交流。