Golang程序配置方案小結
bigwhite 技術志 Bool,cobra,config,Cpp,Education,flag,getopt,Go,Golang,ini,Java,json,multiconfig,Perl,POSIX,Ruby,Shell,TOML,推ter,viper,Windows, 命令行選項,標準庫,配置文件 No Comments
在推ter上看到一篇關于Golang程序配置方案總結的系列文章(一個mini series,共6篇),原文鏈接:在 這里 。我覺得不錯,這里粗略整理(非全文翻譯)一下,供大家參考。
一、背景
無論使用任何編程語言開發應用,都離不開配置數據。配置數據提供的形式有多樣,不外乎命令行選項(options)、參數(parameters),環境 變量(env vars)以及配置文件等。Golang也不例外。Golang內置flag標準庫,可以用來支持部分命令行選項和參數的解析;Golang通過os包提 供的方法可以獲取當前環境變量;但Golang沒有規定標準配置文件格式(雖說內置支持xml、json),多通過第三方 包來解決配置文件讀取的問題。Golang配置相關的第三方包郵很多,作者在本文中給出的配置方案中就包含了主流的第三方配置數據操作包。
文章作者認為一個良好的應用配置層次應該是這樣的:
1、程序內內置配置項的初始默認值
2、配置文件中的配置項值可以覆蓋(override)程序內配置項的默認值。
3、命令行選項和參數值具有最高優先級,可以override前兩層的配置項值。
下面就按作者的思路循序漸進探討golang程序配置方案。
二、解析命令行選項和參數
這一節關注golang程序如何訪問命令行選項和參數。
golang對訪問到命令行參數提供了內建的支持:
//cmdlineargs.go
package main
import (
// "fmt"
"os"
"path/filepath"
)
func main() {
println("I am ", os.Args[0])
baseName := filepath.Base(os.Args[0])
println("The base name is ", baseName)
// The length of array a can be discovered using the built-in function len
println("Argument # is ", len(os.Args))
// the first command line arguments
if len(os.Args) > 1 {
println("The first command line argument: ", os.Args[1])
}
}
執行結果如下:
$go build cmdlineargs.go
$cmdlineargs test one
I am cmdlineargs
The base name is cmdlineargs
Argument # is 3
The first command line argument: test
對于命令行結構復雜一些的程序,我們最起碼要用到golang標準庫內置的flag包:
//cmdlineflag.go
package main
import (
"flag"
"fmt"
"os"
"strconv"
)
var (
// main operation modes
write = flag.Bool("w", false, "write result back instead of stdout\n\t\tDefault: No write back")
// layout control
tabWidth = flag.Int("tabwidth", 8, "tab width\n\t\tDefault: Standard")
// debugging
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to this file\n\t\tDefault: no default")
)
func usage() {
// Fprintf allows us to print to a specifed file handle or stream
fmt.Fprintf(os.Stderr, "\nUsage: %s [flags] file [path ...]\n\n",
"CommandLineFlag") // os.Args[0]
flag.PrintDefaults()
os.Exit(0)
}
func main() {
fmt.Printf("Before parsing the flags\n")
fmt.Printf("T: %d\nW: %s\nC: '%s'\n",
*tabWidth, strconv.FormatBool(*write), *cpuprofile)
flag.Usage = usage
flag.Parse()
// There is also a mandatory non-flag arguments
if len(flag.Args()) < 1 {
usage()
}
fmt.Printf("Testing the flag package\n")
fmt.Printf("T: %d\nW: %s\nC: '%s'\n",
*tabWidth, strconv.FormatBool(*write), *cpuprofile)
for index, element := range flag.Args() {
fmt.Printf("I: %d C: '%s'\n", index, element)
}
}
這個例子中:
- 說明了三種類型標志的用法:Int、String和Bool。
- 說明了每個標志的定義都由類型、命令行選項文本、默認值以及含義解釋組成。
- 最后說明了如何處理標志選項(flag option)以及非option參數。
不帶參數運行:
$cmdlineflag
Before parsing the flags
T: 8
W: false
C: ''
Usage: CommandLineFlag [flags] file [path ...]
-cpuprofile="": write cpu profile to this file
Default: no default
-tabwidth=8: tab width
Default: Standard
-w=false: write result back instead of stdout
Default: No write back
帶命令行標志以及參數運行(一個沒有flag,一個有兩個flag):
$cmdlineflag aa bb
Before parsing the flags
T: 8
W: false
C: ''
Testing the flag package
T: 8
W: false
C: ''
I: 0 C: 'aa'
I: 1 C: 'bb'
$cmdlineflag -tabwidth=2 -w aa
Before parsing the flags
T: 8
W: false
C: ''
Testing the flag package
T: 2
W: true
C: ''
I: 0 C: 'aa'
從例子可以看出,簡單情形下,你無需編寫自己的命令行parser或使用第三方包,使用go內建的flag包即可以很好的完成工作。但是 golang的 flag包與命令行Parser的事實標準:Posix getopt(C/C++/Perl/Shell腳本都可用)相比,還有較大差距,主要體現在:
1、無法支持區分long option和short option,比如:-h和–help。
2、不支持short options合并,比如:ls -l -h <=> ls -hl
3、命令行標志的位置不能任意放置,比如無法放在non-flag parameter的后面。
不過畢竟flag是golang內置標準庫包,你無須付出任何cost,就能使用它的功能。另外支持bool型的flag也是其一大亮點。
三、TOML,Go配置文件的事實標準(這個可能不能得到認同)
命令行雖然是一種可選的配置方案,但更多的時候,我們使用配置文件來存儲靜態的配置數據。就像Java配xml,ruby配yaml,windows配 ini,Go也有自己的搭配組合,那就是 TOML (Tom's Obvious, Minimal Language)。
初看toml語法有些類似windows ini,但細致研究你會發現它遠比ini強大的多,下面是一個toml配置文件例子:
# This is a TOML document. Boom.
title = "TOML Example"
[owner]
name = "Lance Uppercut"
dob = 1979-05-27T07:32:00-08:00 # First class dates? Why not?
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# You can indent as you please. Tabs or spaces. TOML don't care.
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ]
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]
看起來很強大,也很復雜,但解析起來卻很簡單。以下面這個toml 文件為例:
Age = 25
Cats = [ "Cauchy", "Plato" ]
Pi = 3.14
Perfection = [ 6, 28, 496, 8128 ]
DOB = 1987-07-05T05:45:00Z
和所有其他配置文件parser類似,這個配置文件中的數據可以被直接解析成一個golang struct:
type Config struct {
Age int
Cats []string
Pi float64
Perfection []int
DOB time.Time // requires `import time`
}
其解析的步驟也很簡單:
var conf Config
if _, err := toml.Decode(tomlData, &conf); err != nil {
// handle error
}
是不是簡單的不能簡單了!
不過toml也有其不足之處。想想如果你需要使用命令行選項的參數值來覆蓋這些配置文件中的選項,你應該怎么做?事實上,我們常常會碰到類似下面這種三層配置結構的情況:
1、程序內內置配置項的初始默認值
2、配置文件中的配置項值可以覆蓋(override)程序內配置項的默認值。
3、命令行選項和參數值具有最高優先級,可以override前兩層的配置項值。
在go中,toml映射的結果體字段沒有初始值。而且go內建flag包也沒有將命令行參數值解析為一個go結構體,而是零散的變量。這些可以通過第三方工具來解決,但如果你不想用第三方工具,你也可以像下面這樣自己解決,雖然難看一些。
func ConfigGet() *Config {
var err error
var cf *Config = NewConfig()
// set default values defined in the program
cf.ConfigFromFlag()
//log.Printf("P: %d, B: '%s', F: '%s'\n", cf.MaxProcs, cf.Webapp.Path)
// Load config file, from flag or env (if specified)
_, err = cf.ConfigFromFile(*configFile, os.Getenv("APPCONFIG"))
if err != nil {
log.Fatal(err)
}
//log.Printf("P: %d, B: '%s', F: '%s'\n", cf.MaxProcs, cf.Webapp.Path)
// Override values from command line flags
cf.ConfigToFlag()
flag.Usage = usage
flag.Parse()
cf.ConfigFromFlag()
//log.Printf("P: %d, B: '%s', F: '%s'\n", cf.MaxProcs, cf.Webapp.Path)
cf.ConfigApply()
return cf
}
就像上面代碼中那樣,你需要:
1、用命令行標志默認值設置配置(cf)默認值。
2、接下來加載配置文件
3、用配置值(cf)覆蓋命令行標志變量值
4、解析命令行參數
5、用命令行標志變量值覆蓋配置(cf)值。
少一步你都無法實現三層配置能力。
四、超越TOML
本節將關注如何克服TOML的各種局限。
為了達成這個目標,很多人會說:使用 viper ,不過在介紹viper這一重量級選手 之前,我要為大家介紹另外一位不那么知名的選手: multiconfig 。
有些人總是認為大的就是好的,但我相信適合的還是更好的。因為:
1、viper太重量級,使用viper時你需要pull另外20個viper依賴的第三方包
2、事實上,viper單獨使用還不足以滿足需求,要想得到viper全部功能,你還需要另外一個包配合,而后者又依賴13個外部包
3、與viper相比,multiconfig使用起來更簡單。
好了,我們再來回顧一下我們現在面臨的問題:
1、在程序里定義默認配置,這樣我們就無需再在toml中定義它們了。
2、用toml配置文件中的數據override默認配置
3、用命令行或環境變量的值override從toml中讀取的配置。
下面是一個說明如何使用multiconfig的例子:
func main() {
m := multiconfig.NewWithPath("config.toml") // supports TOML and JSON
// Get an empty struct for your configuration
serverConf := new(Server)
// Populated the serverConf struct
m.MustLoad(serverConf) // Check for error
fmt.Println("After Loading: ")
fmt.Printf("%+v\n", serverConf)
if serverConf.Enabled {
fmt.Println("Enabled field is set to true")
} else {
fmt.Println("Enabled field is set to false")
}
}
這個例子中的toml文件如下:
Name = "koding"
Enabled = false
Port = 6066
Users = ["ankara", "istanbul"]
[Postgres]
Enabled = true
Port = 5432
Hosts = ["192.168.2.1", "192.168.2.2", "192.168.2.3"]
AvailabilityRatio = 8.23
toml映射后的go結構如下:
type (
// Server holds supported types by the multiconfig package
Server struct {
Name string
Port int `default:"6060"`
Enabled bool
Users []string
Postgres Postgres
}
// Postgres is here for embedded struct feature
Postgres struct {
Enabled bool
Port int
Hosts []string
DBName string
AvailabilityRatio float64
}
)
multiconfig的使用是不是很簡單,后續與viper對比后,你會同意我的觀點的。
multiconfig支持默認值,也支持顯式的字段賦值需求。
支持toml、json、結構體標簽(struct tags)以及環境變量。
你可以自定義配置源(例如一個遠程服務器),如果你想這么做的話。
可高度擴展(通過loader接口),你可以創建你自己的loader。
下面是例子的運行結果,首先是usage help:
$cmdlinemulticonfig -help
Usage of cmdlinemulticonfig:
-enabled=false: Change value of Enabled.
-name=koding: Change value of Name.
-port=6066: Change value of Port.
-postgres-availabilityratio=8.23: Change value of Postgres-AvailabilityRatio.
-postgres-dbname=: Change value of Postgres-DBName.
-postgres-enabled=true: Change value of Postgres-Enabled.
-postgres-hosts=[192.168.2.1 192.168.2.2 192.168.2.3]: Change value of Postgres-Hosts.
-postgres-port=5432: Change value of Postgres-Port.
-users=[ankara istanbul]: Change value of Users.
Generated environment variables:
SERVER_NAME
SERVER_PORT
SERVER_ENABLED
SERVER_USERS
SERVER_POSTGRES_ENABLED
SERVER_POSTGRES_PORT
SERVER_POSTGRES_HOSTS
SERVER_POSTGRES_DBNAME
SERVER_POSTGRES_AVAILABILITYRATIO
$cmdlinemulticonfig
After Loading:
&{Name:koding Port:6066 Enabled:false Users:[ankara istanbul] Postgres:{Enabled:true Port:5432 Hosts:[192.168.2.1 192.168.2.2 192.168.2.3] DBName: AvailabilityRatio:8.23}}
Enabled field is set to false
檢查一下輸出結果吧,是不是每項都符合我們之前的預期呢!
五、Viper
我們的重量級選手viper(https://github.com/spf13/viper)該出場了!
毫無疑問,viper非常強大。但如果你想用命令行參數覆蓋預定義的配置項值,viper自己還不足以。要想讓viper爆發,你需要另外一個包配合,它就是cobra(https://github.com/spf13/cobra)。
不同于注重簡化配置處理的multiconfig,viper讓你擁有全面控制力。不幸的是,在得到這種控制力之前,你需要做一些體力活。
我們再來回顧一下使用multiconfig處理配置的代碼:
func main() {
m := multiconfig.NewWithPath("config.toml") // supports TOML and JSON
// Get an empty struct for your configuration
serverConf := new(Server)
// Populated the serverConf struct
m.MustLoad(serverConf) // Check for error
fmt.Println("After Loading: ")
fmt.Printf("%+v\n", serverConf)
if serverConf.Enabled {
fmt.Println("Enabled field is set to true")
} else {
fmt.Println("Enabled field is set to false")
}
}
這就是使用multiconfig時你要做的所有事情。現在我們來看看使用viper和cobra如何來完成同樣的事情:
func init() {
mainCmd.AddCommand(versionCmd)
viper.SetEnvPrefix("DISPATCH")
viper.AutomaticEnv()
/*
When AutomaticEnv called, Viper will check for an environment variable any
time a viper.Get request is made. It will apply the following rules. It
will check for a environment variable with a name matching the key
uppercased and prefixed with the EnvPrefix if set.
*/
flags := mainCmd.Flags()
flags.Bool("debug", false, "Turn on debugging.")
flags.String("addr", "localhost:5002", "Address of the service")
flags.String("smtp-addr", "localhost:25", "Address of the SMTP server")
flags.String("smtp-user", "", "User to authenticate with the SMTP server")
flags.String("smtp-password", "", "Password to authenticate with the SMTP server")
flags.String("email-from", "noreply@example.com", "The from email address.")
viper.BindPFlag("debug", flags.Lookup("debug"))
viper.BindPFlag("addr", flags.Lookup("addr"))
viper.BindPFlag("smtp_addr", flags.Lookup("smtp-addr"))
viper.BindPFlag("smtp_user", flags.Lookup("smtp-user"))
viper.BindPFlag("smtp_password", flags.Lookup("smtp-password"))
viper.BindPFlag("email_from", flags.Lookup("email-from"))
// Viper supports reading from yaml, toml and/or json files. Viper can
// search multiple paths. Paths will be searched in the order they are
// provided. Searches stopped once Config File found.
viper.SetConfigName("CommandLineCV") // name of config file (without extension)
viper.AddConfigPath("/tmp") // path to look for the config file in
viper.AddConfigPath(".") // more path to look for the config files
err := viper.ReadInConfig()
if err != nil {
println("No config file found. Using built-in defaults.")
}
}
可以看出,你需要使用BindPFlag來讓viper和cobra結合一起工作。但這還不算太糟。
cobra的真正威力在于提供了subcommand能力。同時cobra還提供了與posix 全面兼容的命令行標志解析能力,包括長短標志、內嵌命令、為command定義你自己的help或usage等。
下面是定義子命令的例子代碼:
// The main command describes the service and defaults to printing the
// help message.
var mainCmd = &cobra.Command{
Use: "dispatch",
Short: "Event dispatch service.",
Long: `HTTP service that consumes events and dispatches them to subscribers.`,
Run: func(cmd *cobra.Command, args []string) {
serve()
},
}
// The version command prints this service.
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version.",
Long: "The version of the dispatch service.",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(version)
},
}
有了上面subcommand的定義,我們就可以得到如下的help信息了:
Usage:
dispatch [flags]
dispatch [command]
Available Commands:
version Print the version.
help Help about any command
Flags:
–addr="localhost:5002": Address of the service
–debug=false: Turn on debugging.
–email-from="noreply@example.com": The from email address.
-h, –help=false: help for dispatch
–smtp-addr="localhost:25": Address of the SMTP server
–smtp-password="": Password to authenticate with the SMTP server
–smtp-user="": User to authenticate with the SMTP server
Use "dispatch help [command]" for more information about a command.
六、小結
以上例子的完整源碼在作者的 github repository里 可以找到。
關于golang配置文件,我個人用到了toml這一層次,因為不需要太復雜的配置,不需要環境變量或命令行override默認值或配置文件數據。不過 從作者的例子中可以看到multiconfig、viper的確強大,后續在實現復雜的golang應用時會考慮真正應用。
? 2015,bigwhite. 版權所有.