深入理解与使用go之配置--实现
目录
引子
构建项目工程的时候,我们经常要提供一些配置
-
数据库: 比如mysql连接的dsn(host/username/password/port/db)
-
Redis: 比如host/username/password/port/d
-
工程服务:服务名称,服务启动端口号
-
日志:日志存放路径,存放大小等
作为gopher,你最快想到的是啥,看看是不是这样
package config
var ConfigMap = map[string]interface{}{
"server.name":"user",
"server.port":8080,
"mysql.host":"localhost",
"mysql.db":"user",
"mysql.user":"aliyun",
// other config here ---
}
然后使用的时候
serverName := config.ConfigMap["server.name"].(string)
serverPort := config.ConfigMap["server.port"].(int)
-
用这么多的断言,合适么
-
如果配置越来越多,这个map是不是越来越大,如果我们想有一条切换到别的语言,重新写一遍么?
配置
定义
配置说白了就是你需要依赖的资源、路径常量,可能随着环境的变化而变化
原理
资源、路径常量, 还要随环境(测试/预发/生产)变化而变化
-
配置文件常量一般都是放在常见格式的 json / yaml / toml 等
-
随环境而变化,环境那就得从 go启动环境变量配置而来
export ENV=test && /data/www/user-service-go
-
那么常量如何解析呢,使用go结构体
简单实现
比如我们设置 app.json
{
"server":{
"name":"user",
"port":8080
}
}
然后解码 config/server.go
type ServerConfig struct {
Name string
Port int
}
func GetServerConfig() *ServerConfig {
fHandle, err := os.Open("config/app.json")
if err != nil {
panic("config file open error")
}
defer fHandle.Close()
content, err := ioutil.ReadAll(fHandle)
if err != nil {
panic("config file read error")
}
type ServerConfig struct {
Name string
Port int
}
sc := &ServerConfig{}
err = json.Unmarshal(content, sc)
if err != nil {
panic("config file unmarshal error")
}
return sc
}
使用
sc := config.GetServerConfig()
fmt.Println(sc.Name)
优化
如上,有两个问题没有解决
-
如果我切换到生产环境,配置还是使用测试环境的
app.json
? -
凡是调用配置的地方,我们每次都要打开文件,关闭文件进行频繁io操作
第一个问题,我们好解决,配置多环境配置,如下
├── config │ └── json │ ├── app-pre.json │ ├── app-prod.json │ └── app-test.json
然后,我们在读取配置文件之前,读取环境变量
env := os.Getenv("ENV")
if env == "" {
env = "test"
}
fHandle, err := os.Open("config/json/app-" + env + ".json")
第二个问题,环境一旦确定,配置不会再更改,那么我们想从根本上解决问题,就要考虑到设计模式--- 单例模式
这里我们直接使用饥饿模式(直接在文件中初始化全局变量),其他还有懒汉模式等等,感兴趣的可以查询相关资料
我们在 config/server.go
顶部添加
var Server = GetServerConfig()
那么后续的调用中可以直接使用, 配置只读取一次
fmt.Println(config.Server.Name)
扩展
使用viper读取配置
这里我们更换一下使用 yaml
配置(也可以使用json
或其他),具体怎么实现,我们不深究,我们讲下怎么使用
-
文件目录
├── config │ ├── conf.go │ ├── mysql.go │ ├── server.go │ └── yaml │ ├── app-pre.yaml │ ├── app-prod.yaml │ └── app-test.yaml
-
配置
app-test.yaml
server: name: "user" port: 8080 mysql: host: "localhost" port: 3306 user: "aliyun" password: "123456" db: "user"
-
初始化配置
conf.go
import ( "fmt" "github.com/spf13/viper" "os" ) var C = initConfig() type config struct { viper *viper.Viper } func initConfig() *config { v := viper.New() conf := &config{viper: v} env := os.Getenv("ENV") if env == "" { env = "test" } workDir, _ := os.Getwd() conf.viper.SetConfigName("app-" + env) // name of config file (without extension) conf.viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name conf.viper.AddConfigPath(workDir + "/config/yaml/") // path to look for the config file in err := conf.viper.ReadInConfig() // Find and read the config file if err != nil { // Handle errors reading the config file panic(fmt.Errorf("fatal error config file: %w", err)) } return conf }
-
初始化服务
server.go
package config var Server = InitServerConfig() type ServerConfig struct { Name string Port int } func InitServerConfig() *ServerConfig { return &ServerConfig{ Name: C.viper.GetString("server.name"), Port: C.viper.GetInt("server.port"), } }
-
初始化
mysql.go
var MySQL = InitMySQLConfig() type MySQLConfig struct { Host string Port int User string Password string Db string } func InitMySQLConfig() *MySQLConfig { return &MySQLConfig{ Host: C.viper.GetString("mysql.host"), Port: C.viper.GetInt("mysql.port"), User: C.viper.GetString("mysql.user"), Password: C.viper.GetString("mysql.password"), Db: C.viper.GetInt("mysql.db"), } }
-
main.go
打印func main() { fmt.Println(config.Server) fmt.Println(config.Redis) }
-
启动
export ENV=test && /data/www/user-service-go
是不是调用有种很清爽的感觉,如果有更好的方式,欢迎评论留言讨论