diff --git a/.gitignore b/.gitignore
index f2dd9554a12fd7acdc62e60e8eccae086f718be2..9ef7b5e10b431d85bfd4f10801a83f119e2b5e90 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
*.dll
*.so
*.dylib
+.idea
# Test binary, built with `go test -c`
*.test
diff --git a/chap-33-application-configuration/33.md b/chap-33-application-configuration/33.md
new file mode 100644
index 0000000000000000000000000000000000000000..0082d70e207376bcb79794158de14e4facf2fbe9
--- /dev/null
+++ b/chap-33-application-configuration/33.md
@@ -0,0 +1,468 @@
+# 第 33 章:应用程序配置
+
+
+
+## 1 您将在本章学到什么?
+
+- 如何配置一个应用程序?
+- 如何给程序加一个命令行选项?
+- 如何创建和解析环境变量?
+
+## 2 涉及到的技术概念
+
+- 环境变量
+- 解构
+- 命令行选项
+
+## 3 介绍
+
+当我们写程序时,(经常)会将一些重要的选项写进源码里,例如:服务监听的端口,使用的数据库名称等等。但是如果您尝试将您写的程序分享给别人呢?很明显在开发阶段您的用户不会和您使用同样的数据库名称;他们也想给自己的程序使用不同的端口号!
+
+并且还有安全问题呢!您想让每个人都知道您的数据库密码吗?您不应该提交这些凭证到您的代码中。
+
+## 4 它是什么?
+
+当应用程序向用户提供控制执行方面的可能性时,它是可配置的[@rabkin2011static]。例如,用户可以自定义服务监听的端口,数据库凭证,HTTP 请求的超时时间等等。
+
+## 5 该如何做呢?
+
+在许多开源应用程序中,配置项都是通过 键-值 数据结构处理的[@rabkin2011static]。配置经常通过一个独一无二的名字标识的。Unix 系统使用字符串作为选项名字。Windows 使用一种树状结构。
+
+在实际的应用中,您总是会找到一个 class(或一个类型结构体)将配置选项暴露出去。这个 class 经常解析一个文件将值赋给每个选项。应用程序通常建议使用命令行选项来调整配置。
+
+## 6 命令行选项
+
+为了定义一个命令行选项的配置名称,我们可以使用标准库 `flag` 。
+
+举个例子。您在构建一个 REST API 服务。您的应用将会暴露一个网络服务;您想让用户自己配置监听端口。
+
+第一步,先定义一个新标识:
+
+``` Go
+// configuration/cli/main.go
+
+port := flag.Int("port", 4242, "the port on which the server will listen")
+```
+
+第一个参数是标识名;第二个参数是默认值;第三个是帮助文本。返回值是一个 integer 型的指针。
+
+``` Go
+var port int
+flag.IntVar(&port, "port", 4242, "the port on which the server will listen")
+```
+
+下一步就是解析这些标识:
+
+``` Go
+flag.Parse()
+```
+
+在您使用自己定义的这些标识前,`Parse` 函数必须被调用。在函数内部,它将遍历命令行中给出的所有参数,并为您定义的标识变量赋值。
+
+在第一版(`flag.Int`)中,变量将成为一个数字型指针(**`*int`**)。在第二版(`flag.Intvar`)将成为数字类型(**`int`**)。这种特殊性改变了您以后使用变量的方式:
+
+``` Go
+// version 1 : Int
+
+port := flag.Int("port", 4242, "the port on which the server will listen")
+flag.Parse()
+fmt.Printf("%d",*port)
+
+// version 2 : IntVar
+
+var port2 int flag.IntVar(&port2, "port2", 4242, "the port on which the server will listen") flag.Parse()
+fmt.Printf("%d\n",port2)
+```
+
+### 6.0.1 其他类型
+
+`flag` 包提供了一些函数来添加不同类型的标识。下面左边是一些类型和 flag 包的相关函数。
+
+Int64, Int64Var
+
+String, StringVar
+
+Uint, UintVar
+
+Uint64, Uint64Var
+
+Float64, Float64Var
+
+Duration, DurationVar
+
+Bool, BoolVar
+
+### 6.0.2 在命令行中的用法
+
+有两种方法将指定的标识传给程序
+
+``` Bash
+$ myCompiledGoProgram -port=4242
+```
+或者
+``` Bash
+$ myCompiledGoProgram -port 4242
+```
+
+bool 标识类型禁止使用最后一种用法。您的应用可以简单的写成这样来设置标识为 true:
+
+``` Bash
+$ myCompiledGoProgram -myBoolFlag
+```
+
+如此做,标识背后的变量将会被设置为 true。
+
+您可以使用 help flag 来查看所有可用的标识(这是默认添加到您的程序中的)
+
+``` Bash
+$ ./myCompiledGoProgram -help
+Usage of ./myCompiledProgram:
+ -myBool
+ a test boolean
+ -port int
+ the port on which the server will listen (default 4242)
+ -port2 int
+ the port on which the server will listen (default 4242)
+```
+
+## 7 环境变量
+
+一些云服务通过环境变量支持配置。环境变量使用 export 命令很容易配置。只需要在您的终端输入以下命令即可创建一个名为 MYVAR 的环境变量1:
+
+``` Bash
+$ export MYVAR=value
+```
+
+如果您想将环境变量传递给应用程序,使用以下语句:
+
+``` Bash
+$ export MYVAR=test && export MYVAR2=test2 && ./myCompiledProgram
+```
+
+这里我们设置了两个环境变量,并启动了程序 `myCompiledProgram`。
+
+### 7.0.1 如何获取环境变量的值
+
+您可以使用 `os` 标准包获取环境变量的值。该包暴露了一个名为 `Getenv` 的方法。这个方法使用变量的名字作为参数并且返回它的值(以字符串的形式):
+
+``` Go
+// configuration/env/main.go
+myvar := os.Getenv("MYVAR")
+myvar2 := os.Getenv("MYVAR2")
+fmt.Printf("myvar : '%s'\n", myvar)
+fmt.Printf("myvar2 :'%s'\n", myvar2)
+```
+
+您可以使用其他的方法获取环境变量的值:`LookupEnv`。它比前一种方法要好,因为如果变量不存在的话它会通知您:
+
+``` Go
+// configuration/env/main.go
+port, found := os.LookupEnv("DB_PORT")
+if !found {
+ log.Fatal("impossible to start up, DB_PORT env var is mandatory")
+}
+portParsed, err := strconv.ParseUint(port, 10, 8)
+if err != nil {
+ log.Fatalf("impossible to parse db port: %s", err)
+}
+log.Println(portParsed)
+```
+
+`LookupEnv` 将会检查环境变量是否存在:
+
+- 如果不存在,第二个结果(`found`)将等于 `false`
+- 在上面的例子中,我们使用 `strconv.ParseUint` 方法将端口值解析为 uint16(基于十进制)
+
+### 7.0.2 如何获取所有的环境变量
+
+您可以使用 Environ() 方法获取所有的环境变量,其返回一个字符串类型的 slice:
+
+``` Go
+fmt.Println(os.Environ())
+```
+
+返回的 slice 以『键=值』的格式组成所有的变量。slice 的每个元素都是一个 键-值 对。包并不会从键中分割值,您必须解析 slice 的值。
+
+### 7.0.3 如何设置一个环境变量
+
+os 包暴露了 `os.Setenv` 方法:
+
+``` Go
+err := os.Setenv("MYVAR3","test3")
+if err != nil {
+ panic(err)
+}
+```
+
+`os.Setenv` 方法接收两个参数:
+
+- 设置的变量名
+- 它的值
+
+注意,它会产生错误,您要记得处理错误。
+
+## 8 基于文件的配置
+
+应用可以保存为文件并且被应用自动解析。
+
+文件的格式依赖于应用的需求。如果您需要一个带着等级和层级(以 Windows Registry 的方式)的复杂配置,可以考虑使用 JSON,YAML,或者 XML 格式。
+
+我们看一下 JSON 配置文件的例子:
+
+``` Json
+{
+ "server": {
+ "host": "localhost",
+ "port": 80
+ },
+ "database": {
+ "host": "localhost",
+ "username": "myUsername",
+ "password": "abcdefgh"
+ }
+}
+```
+
+我们把这个文件放置在托管应用程序的某个地方。在应用程序里解析这些文件以获取配置值。
+
+为了解析它,首先在程序中创建这样一个结构体:
+
+``` Go
+// configuration/file/main.go
+type Configuration struct {
+ Server Server `json:"server"`
+ Database DB `json:"database"`
+}
+type Server struct {
+ Host string `json:"host"`
+ Port int `json:"port"`
+}
+type DB struct {
+ Host string `json:"host"`
+ Username string `json:"username"`
+ Password string `json:"password"`
+}
+```
+
+我们创建了三种类型的结构体:
+
+- `Configuration` 包括了其它两种类型
+- `Server` 代表了 JSON 对象 **server**
+- `DB` 存储了 JSON 对象 **database**
+
+您可以使用这些类型的结构体解析这个 JSON 配置文件。下一步就是打开文件读取内容:
+
+``` Go
+// configuration/file/main.go
+
+confFile, err := os.Open("myConf.json")
+if err != nil {
+ panic(err)
+}
+defer confFile.Close()
+conf, err := ioutil.ReadAll(confFile)
+if err != nil {
+ panic(err)
+}
+```
+
+这里我们调用 `os.Open` 函数打开位于可执行文件同级目录中的 `myConf.json` 文件。在实际的环境中,您应该把文件上传到机器的合适位置。
+
+通过使用 `ioutil.ReadAll` 函数获取整个文件的内容。该函数返回一个 byte 类型的 slice。下一步就是使用 `json.Unmarshal` 函数处理这个 slice:
+
+``` Go
+// configuration/file/main.go
+
+myConf := Configuration{}
+err = json.Unmarshal(conf, &myConf)
+if err != nil {
+ panic(err)
+}
+fmt.Printf("%+v",myConf)
+```
+
+我们首先创建了一个变量 `myConf` 初始化为一个空结构体 `Configuration`。然后我们把从文件中提取出的 byte 类型的 slice 传给 `json.Unmarshal` 函数,和(第二个参数)一个 `myConf` 指针地址。
+
+最后,我们的程序会输出如下:
+
+``` Go
+{Server:{Host:localhost Port:80} Database:{Host:localhost Username:myUsername Password:abcdefgh}}
+```
+
+您可以在整个应用程序中共享这个变量。
+
+## 9 github.com/spf13/viper 模块
+
+`github.com/spf13/viper` 是非常受当前 Go 社区欢迎的配置模块,其拥有超过 14,000 星和超过 1.3k forks2。
+
+这个包允许您通过文件、环境变量、命令行、buffers 等等简单定义一个配置。它亦支持一个有趣的特性:如果您的配置项在应用程序的生命周期中改变了,它将会重载。
+
+### 9.1 从文件中读取
+
+尝试一下:
+
+``` Go
+// configuration/viper/main.go
+//...
+// import "github.com/spf13/viper"
+viper.SetConfigName("myConf")
+viper.(".")
+err := viper.ReadInConfig()
+if err != nil {
+ log.Panicf("Fatal error config file: %s\n", err)
+}
+```
+
+首先,您必须定义您想加载的配置文件的名字:`viper.SetConfigName("myConf")`。这里我们加载了名为 `myConf` 的文件,注意不能给 viper 文件的扩展名。其将会检索和解析正确的文件。
+
+在下一行,必须告诉 viper 使用函数 `AddConfigPath` 搜索。点意味着『在当前文件夹下查找』名为 myConf 的文件。当前支持的扩展名如下:
+
+- json
+- toml
+- yaml
+- yml
+- properties
+- props
+- prop
+- hcl
+
+注意应该为配置添加其它的路径。`AddConfig` 函数将会添加这些路径到一个 slice 中。
+
+当我们调用 `ReadInConfig` 函数时,包将会搜寻这个特定的文件。
+
+可以加载使用如下配置:
+
+``` Go
+fmt.Println(viper.AllKeys())
+//[database.username database.password server.host server.port database.host]
+fmt.Println(viper.GetString("database.username"))
+// myUsername
+```
+
+使用 `AllKeys` 函数,可以检索应用程序的所有存在的配置键。使用 `GetString` 函数获取一个特定的配置变量。
+
+`GetString` 函数接收配置项的键作为参数,键是配置文件层次结构的反映。这里的 `"database.username"` 意思是我们从 **database** 对象中获取 **username** 属性。
+
+database 对象也有 host 熟悉,可以简单的通过 `"database.host"` 获取。
+
+viper 暴露 `GetXXX` 函数这些类型 **`bool`**,**`float64`**,**`int`**,**`string`**,**`map[string]interface{}`**,**`map[string]string`**,slice of strings,time 和 duration。也可以简单地使用 Get 获取配置项的值,返回的类型是空接口。我并不推荐这么做,因为其可能导致类型错误。在我们的例子中,我们期望字符串,但是如果一个用户把数字填入配置文件中,它就会返回一个数字。您的程序将会出现预期外的罢工和 panic。
+
+### 9.2 从环境变量中读取
+
+Viper 可以使用 prefix 自动解析环境变量:
+
+``` Go
+viper.SetEnvPrefix("myapp")
+viper.AutomaticEnv()
+```
+
+有了这两行之后,您每次调用一个方法(`GetString`,`GetBool` 等等)viper 将会查找带着 MYAPP_ 前缀的环境变量:
+
+``` Go
+fmt.Println(viper.GetInt("timeout"))
+```
+
+将会查找名为 `MYAPP_TIMEOUT` 的环境变量。
+
+## 10 问题以及如何避免
+
+### 10.1 文档缺失
+
+许多项目都缺失关于配置项的文档。
+
+通过在一个单独的文件中记录每一个可用配置选项,可以降低软件的采用门槛。
+
+在公司内部,您可能不是处理应用程序部署的人,因此,您必须在您的估计中包括文档时间。更好的文档意味着减少了解决方案的上市时间。
+
+来自 Berkeley University[@rabkin2011static] 的 Ariel Rabkin 和 Randy Katz 的一项有趣的研究表明,甚至一些大型开源项目(他们研究了 Cassandra,Hadoop,HBase 等七个大型项目)的配置文档都是不准确的:
+
+- 其中提到了应用程序源代码中不存在的配置项。他们指出,有时这些选项只是简单地注释到应用程序源代码中。
+- 应用程序源代码中存在一些甚至没有文档记录的选项。
+
+解决方法很简单:为您的配置选项创建精确的文档,并在项目开发时保持最新。
+
+### 10.2 配置错误
+
+在一项研究中[@nagaraja2004understanding]技术操作人员被要求在一个真实的互联网系统(三层拍卖应用程序)上执行操作。据观察,50%的错误是配置错误!
+
+这个数字令人印象深刻,也非常有趣。系统可能会因为配置错误而失败。您无法阻止用户在配置中输入错误的端口号。但是应用程序可以保护自己不受错误配置的影响。
+
+- 通过检查和警告用户。
+- 不使用默认值替换错误值。
+
+``` Go
+// configuration/error-detection/main.go
+//...
+dbPortRaw := os.Getenv("DATABASE_PORT")
+dbPort, err := strconv.ParseUint(port, 10, 16)
+if err != nil {
+ log.Panicf("Impossible to parse database port number '%s'. Please double check the env variable DATABASE_PORT",dbPortRaw)
+}
+```
+
+在前面的代码里,我们检索环境变量 `DATABASE_PORT` 的值。接着尝试把它转换为 **`uint16`** 类型。如果出现错误,我们拒绝启动应用程序并且通知用户错误原因。
+
+注意这里应该通知用户如何修复这个错误。这是一个好的例子,仅仅花费您十秒钟的时间写,但是可能省去了用户一个小时的查找错误时间。
+
+再次检查程序是否有效地使用了所有配置变量。
+
+## 11 安全
+
+配置变量通常由这些组成:密码、访问令牌、加密密钥等等。泄露这些机密可能会造成损失。以上所有解决方案都不能完美地存储和保护生产机密。频繁变换这些机密信息也不是一个很好地解决方案。
+
+一些开源解决方案可以处理这个具体的问题。它们提供了一系列功能来保护、监视和审计您的机密信息的使用。在写作的时候,似乎由 Hashicorp 编辑的 Vault 是最流行的3。而且它是用 Go 写的!
+
+## 12 自我测试
+
+### 12.1 问题
+
+1. 如何给程序添加一个字符串类型的命令行选项?
+2. 如何检查一个环境变量是否存在,并且在同一个调用中获取它的值?
+3. 如何设置一个环境变量?
+4. 然后使用文件配置应用程序?
+
+### 12.2 答案
+
+1. 如何给程序添加一个字符串类型的命令行选项?
+ 1. ``` var password stringflag.StringVar(&password,"password", "default","the db password")// define other flags// parse input flagsflag.Parse() ```
+ 2. ``` password2 := flag.String(“password2”,“default”, “the db password”) ```
+2. 如何检查一个环境变量是否存在,并且在同一个调用中获取它的值?
+ 1. 使用 `os` 包中的 `LookupEnv` 函数
+ 2. ``` port, ok := os.LookupEnv(“DB_PORT”) if !ok { log.Fatal(“impossible to start up, DB_PORT env var is mandatory”) } ```
+3. 如何设置一个环境变量?
+ 1. 使用 `os` 包中的 `SetEnv` 函数
+ 2. ``` err := os.Setenv(“MYVAR3”, “test3”) if err != nil { panic(err) } ```
+4. 然后使用 JSON 文件配置应用程序?
+ 1. 使用所有配置变量创建一个特定类型的文件
+ 2. 在引导程序里,打开 JSON 文件
+ 3. 解析文件。
+
+## 13 关键要点
+
+- 应用程序配置可以通过命令行选项配置。
+- `flag` 标准包提供了添加这些选项的方法。
+``` Go
+var password string
+flag.StringVar(&password,"password", "default","the db password")// define other flags// parse input flagsflag.Parse()
+```
+- 配置项在程序启动的时候通过命令行给出:```$ myCompiledGoProgram -password insecuredPass```
+- 配置项通常在应用程序启动的时候加载(在 main 函数中)
+- 应用程序配置项也能通过环境变量设置
+- 可以使用 `os` 包检索环境变量
+ - ```dbHost := os.GetEnv(“DB_HOST”)```
+ - ```dbHost, found := os.LookupEnv(“DB_HOST”)```
+- 也可以通过文件(JSON,YAML...)处理配置项。做法就是创建对应的结构体并解构文件内容
+- `github.com/spf13/viper` 是处理应用程序配置的常用参考模块
+- 配置项应该被开发者认真记录为文档。及时更新的文档也是相当有竞争力的
+- 使用专用的机密信息管理解决方案应处理应用程序机密信息。
+
+---
+1. 在Unix系统中,环境变量是在其上定义的进程的本地变量↩︎
+2. 截止到2021年2月22日
+3. 数据源自:https://github.com/search?o=desc&q=secrets+management&s=stars&type=Repositories 截止到2021年2月22日
+
+## 参考文献
+- [rabkin2011static] Rabkin, Ariel, and Randy Katz. 2011. “Static Extraction of Program Configuration Options.” In Proceedings of the 33rd International Conference on Software Engineering, 131–40. ACM.
+- [rabkin2011static] Rabkin, Ariel, and Randy Katz. 2011. “Static Extraction of Program Configuration Options.” In Proceedings of the 33rd International Conference on Software Engineering, 131–40. ACM.
+- [rabkin2011static] Rabkin, Ariel, and Randy Katz. 2011. “Static Extraction of Program Configuration Options.” In Proceedings of the 33rd International Conference on Software Engineering, 131–40. ACM.
+- [nagaraja2004understanding] Nagaraja, Kiran, Fábio Oliveira, Ricardo Bianchini, Richard P Martin, and Thu D Nguyen. 2004. “Understanding and Dealing with Operator Mistakes in Internet Services.” In OSDI, 4:61–76.
diff --git a/chap-33-application-configuration/imgs/33-1-application.jpg b/chap-33-application-configuration/imgs/33-1-application.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..9df4fdc825a04f96d0daf6e38499371fdabe0f14
Binary files /dev/null and b/chap-33-application-configuration/imgs/33-1-application.jpg differ