配置
本章介绍如何使用 Go-Sail 进行配置解析与热更新。
引言
配置一直是任何软件服务的灵魂,它决定了程序运行和行为的方式。在 Go-Sail 的具体场景中,配置项十分丰富。完全依赖硬编码(如之前示例所演示)既不规范也不可取,还存在严重的安全隐患。通常,配置项(或配置文件)是与程序源代码分离的,此举能有效提升应用程序的灵活性与安全性。
分离
健壮的软件设计应当具备读取配置项来提供服务的能力,而不是将这些配置硬编码进源码里。因此,需要将源码和配置文件解耦。
Go-Sail 支持三种主流的配置文件格式,分别是:
- YAML
- TOML
- JSON
它们之间不存在优劣之分,开发者可根据项目或团队的风格灵活选择。
从零开始
刚开始时你可能并没有配置文件,或者只有一个空的配置文件。手动逐项声明每个条目显然不现实,因此我们可以借助 Go-Sail 脚手架自动生成配置文件模板。
import (
"testing"
"github.com/keepchen/go-sail/v3/sail/config"
)
func TestGivemeConfigurationTemplate(t *testing.T) {
config.PrintTemplateConfig("yaml", "./go-sail.config.local.yaml")
}
你可能已经注意到,这次我们新建了一个名为 configuration_test.go 的文件,而没有直接写在 main.go 里。这样做是为了方便你在 GoLand、Visual Studio Code 等 IDE 中直接运行。
执行后,脚手架工具会在当前目录下生成一个名为 go-sail.config.local.yaml 的配置文件。该文件包含了 Go-Sail 的核心配置,你无需填写所有字段,只需按需填写即可,甚至可以删除未使用的配置项。
生成内容大致如下:
http_conf:
debug: false
addr: ""
swagger_conf:
enable: false
redoc_ui_path: ""
json_path: ""
favicon_path: ""
prometheus_conf:
enable: false
addr: ""
access_path: ""
disable_system_sample: false
disk_path: ""
sample_interval: ""
websocket_route_path: ""
trusted_proxies: []
logger_conf:
console_output: false
env: ""
level: ""
modules: []
filename: ""
max_size: 0
max_backups: 0
compress: false
exporter:
provider: ""
redis:
list_key: ""
conn_conf:
endpoint:
host: ""
port: 0
username: ""
password: ""
enable: false
database: 0
ssl_enable: false
cluster_conn_conf:
enable: false
ssl_enable: false
endpoints: []
nats:
subject: ""
conn_conf:
enable: false
endpoints: []
username: ""
password: ""
kafka:
topic: ""
conn_conf:
enable: false
endpoints: []
SASLAuthType: ""
username: ""
password: ""
timeout: 0
db_conf:
enable: false
driver_name: ""
auto_migrate: false
disable_foreign_key_constraint_when_migrating: false
disable_nested_transaction: false
allow_global_update: false
skip_default_transaction: false
logger:
level: ""
slow_threshold: 0
skip_caller_lookup: false
ignore_record_not_found_error: false
colorful: false
connection_pool:
max_open_conn_count: 0
max_idle_conn_count: 0
conn_max_life_time_minutes: 0
conn_max_idle_time_minutes: 0
mysql:
read:
host: ""
port: 0
username: ""
password: ""
database: ""
charset: ""
parseTime: false
loc: ""
write:
host: ""
port: 0
username: ""
password: ""
database: ""
charset: ""
parseTime: false
loc: ""
postgres:
read:
host: ""
port: 0
username: ""
password: ""
database: ""
ssl_mode: ""
timezone: ""
write:
host: ""
port: 0
username: ""
password: ""
database: ""
ssl_mode: ""
timezone: ""
sqlserver:
read:
host: ""
port: 0
username: ""
password: ""
database: ""
write:
host: ""
port: 0
username: ""
password: ""
database: ""
sqlite:
read:
file: ""
write:
file: ""
clickhouse:
read:
host: ""
port: 0
username: ""
password: ""
database: ""
read_timeout: 0
write_timeout: 0
write:
host: ""
port: 0
username: ""
password: ""
database: ""
read_timeout: 0
write_timeout: 0
redis_conf:
endpoint:
host: ""
port: 0
username: ""
password: ""
enable: false
database: 0
ssl_enable: false
redis_cluster_conf:
enable: false
ssl_enable: false
endpoints: []
nats_conf:
enable: false
endpoints: []
username: ""
password: ""
jwt_conf: null
email_conf:
workers: 0
worker_throttle_seconds: 0
host: ""
port: 0
username: ""
password: ""
from: ""
subject: ""
params:
variables: []
kafka_conf:
conf:
enable: false
endpoints: []
SASLAuthType: ""
username: ""
password: ""
timeout: 0
topic: ""
groupID: ""
etcd_conf:
enable: false
endpoints: []
username: ""
password: ""
timeout: 0
valkey_conf:
enable: false
username: ""
password: ""
endpoints: []
实际内容可能会随着版本变化而有所不同。
读取与热更新
读取配置
完成了需要的配置项后,我们需要修改源码以读取配置。
首先声明一个配置变量,并使用 Go-Sail 内置的配置读取函数进行加载处理。
package main
import (
"fmt"
"time"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
"github.com/keepchen/go-sail/v3/lib/logger"
"github.com/keepchen/go-sail/v3/http/api"
sailConstants "github.com/keepchen/go-sail/v3/constants"
sailMiddleware "github.com/keepchen/go-sail/v3/http/middleware"
)
type ErrorCode int
func (v ErrorCode) Int() int {
return int(v)
}
const (
ErrUserNotExist ErrorCode = 1000
ErrUserAlreadyExist ErrorCode = 1001
ErrUsernameAndPasswordNotMatch ErrorCode = 1002
)
var codeMsgMap = sailConstants.MMBox{
//en
sailConstants.LanguageEnglish: {
ErrUserNotExist: "User not exist",
ErrUserAlreadyExist: "User already exist",
ErrUsernameAndPasswordNotMatch: "Username and password not match",
},
//zh-CN
sailConstants.LanguageChinesePRC: {
ErrUserNotExist: "用户不存在",
ErrUserAlreadyExist: "用户已经存在",
ErrUsernameAndPasswordNotMatch: "用户名或密码不正确",
},
//ja
sailConstants.LanguageJapanese: {
ErrUserNotExist: "ユーザーが存在しません",
ErrUserAlreadyExist: "ユーザーは既に存在します",
ErrUsernameAndPasswordNotMatch: "ユーザー名またはパスワードが正しくありません",
},
//other langugage
}
var once sync.Once
func init() {
once.Do(func() {
time.AfterFunc(time.Second*2, func() {
for language, msgMap := range codeMsgMap {
for code, msg := range msgMap {
sail.Code().Register(language.String(), code.Int(), msg)
}
}
})
})
}
type LoginRequest struct {
Username string `json:"username" form:"username" query:"username"`
Password string `json:"password" form:"password" query:"password"`
}
func (v LoginRequest) Validator() (sailConstants.ICodeType, error) {
if len(v.Username) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("username can not be empty")
}
if len(v.Password) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("password can not be empty")
}
return sailConstants.ErrorNone, nil
}
var (
privateKey = []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUvUDx+LPQ0S+L
+5UmtD2EJw1L953mVCMWBJktBbqPTIhDmrd33+3cNq0t7rXuALhoqZS/53nDchU1
wsCveieNDR7SsdO4HMS4bnxgyuYCkC1ugAdyvJ2FCv7xUppc7PvyIQ1gQS/nOP0w
...
vplU0p7ayaXuNF2t73k/L5f92+8VBuYECEUOXw2xST5gvkPdKGK1xM1cLT6y8TrF
RIXvUK2duHjDxiaPKtANi2P4
-----END PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1L1A8fiz0NEvi/uVJrQ9
...
KTJQ+GGzUqOGzruYQ5sM3TnU8Avb4OF36uyADBwA4bP944tKSNSET7BC3N0UerRo
QwIDAQAB
-----END PUBLIC KEY-----`)
conf config.Config
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.Use(sailMiddleware.DetectUserAgentLanguage())
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
if err := c.ShouldBind(&loginRequest); err != nil {
sail.LogTrace(c).Warn("bind request parameter failed", zap.Error(err))
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
if code, err := loginRequest.Validator(); err != nil {
sail.Response(c).Wrap(code, nil, err.Error()).Send()
return
}
var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// 用户不存在
if len(loginRequest.Username) == 0 {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
// 密码不匹配
if loginRequest.Password != user.Password {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
headers := map[string]string{
"X-Request-Id": sail.LogTrace(c).RequestID(),
}
sail.Utils().HttpClient().SendRequest("POST", "https://....", nil, headers)
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
ginEngine.POST("/third-party/notify", func(c *gin.Context){
c.JSON(200, ...)
})
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User)
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}
sail.GetLogger("schedule").Info("logging something...")
}
)
func main() {
parseFn := func(content []byte, viaWatch bool) {
fmt.Println("config content: ", string(content))
yaml.Unmarshal(content, &conf)
}
sail.Config(nil).ViaFile("./go-sail.config.local.yaml").Parse(parseFn)
options := &api.Option{
ForceHttpCode200: true,
DetectAcceptLanguage: true,
}
sail.WakeupHttp("go-sail", conf).
SetupApiOption(options).
Hook(registerRoutes, nil, afterFunc).Launch()
}
热更新
通常,我们希望配置变动可以即时生效,而无需重启服务(否则会造成服务不可用,体验不佳)。这时,Go-Sail 提供的配置监控机制正好可以满足需求。
package main
import (
"fmt"
"time"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
"github.com/gin-gonic/gin"
"github.com/keepchen/go-sail/v3/sail"
"github.com/keepchen/go-sail/v3/sail/config"
"github.com/keepchen/go-sail/v3/lib/db"
"github.com/keepchen/go-sail/v3/lib/logger"
"github.com/keepchen/go-sail/v3/http/api"
sailConstants "github.com/keepchen/go-sail/v3/constants"
sailMiddleware "github.com/keepchen/go-sail/v3/http/middleware"
)
type ErrorCode int
func (v ErrorCode) Int() int {
return int(v)
}
const (
ErrUserNotExist ErrorCode = 1000
ErrUserAlreadyExist ErrorCode = 1001
ErrUsernameAndPasswordNotMatch ErrorCode = 1002
)
var codeMsgMap = sailConstants.MMBox{
//en
sailConstants.LanguageEnglish: {
ErrUserNotExist: "User not exist",
ErrUserAlreadyExist: "User already exist",
ErrUsernameAndPasswordNotMatch: "Username and password not match",
},
//zh-CN
sailConstants.LanguageChinesePRC: {
ErrUserNotExist: "用户不存在",
ErrUserAlreadyExist: "用户已经存在",
ErrUsernameAndPasswordNotMatch: "用户名或密码不正确",
},
//ja
sailConstants.LanguageJapanese: {
ErrUserNotExist: "ユーザーが存在しません",
ErrUserAlreadyExist: "ユーザーは既に存在します",
ErrUsernameAndPasswordNotMatch: "ユーザー名またはパスワードが正しくありません",
},
//other langugage
}
var once sync.Once
func init() {
once.Do(func() {
time.AfterFunc(time.Second*2, func() {
for language, msgMap := range codeMsgMap {
for code, msg := range msgMap {
sail.Code().Register(language.String(), code.Int(), msg)
}
}
})
})
}
type LoginRequest struct {
Username string `json:"username" form:"username" query:"username"`
Password string `json:"password" form:"password" query:"password"`
}
func (v LoginRequest) Validator() (sailConstants.ICodeType, error) {
if len(v.Username) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("username can not be empty")
}
if len(v.Password) == 0 {
return sailConstants.ErrRequestParamsInvalid, fmt.Errorf("password can not be empty")
}
return sailConstants.ErrorNone, nil
}
var (
privateKey = []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUvUDx+LPQ0S+L
+5UmtD2EJw1L953mVCMWBJktBbqPTIhDmrd33+3cNq0t7rXuALhoqZS/53nDchU1
wsCveieNDR7SsdO4HMS4bnxgyuYCkC1ugAdyvJ2FCv7xUppc7PvyIQ1gQS/nOP0w
...
vplU0p7ayaXuNF2t73k/L5f92+8VBuYECEUOXw2xST5gvkPdKGK1xM1cLT6y8TrF
RIXvUK2duHjDxiaPKtANi2P4
-----END PRIVATE KEY-----`)
publicKey = []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1L1A8fiz0NEvi/uVJrQ9
...
KTJQ+GGzUqOGzruYQ5sM3TnU8Avb4OF36uyADBwA4bP944tKSNSET7BC3N0UerRo
QwIDAQAB
-----END PUBLIC KEY-----`)
conf config.Config
registerRoutes = func(ginEngine *gin.Engine) {
ginEngine.Use(sailMiddleware.DetectUserAgentLanguage())
ginEngine.POST("/login", func(c *gin.Context){
var loginRequest LoginRequest
if err := c.ShouldBind(&loginRequest); err != nil {
sail.LogTrace(c).Warn("bind request parameter failed", zap.Error(err))
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
if code, err := loginRequest.Validator(); err != nil {
sail.Response(c).Wrap(code, nil, err.Error()).Send()
return
}
var user User
sail.GetDBR().Where(&User{Username: loginRequest.Username}).First(&user)
// 用户不存在
if len(loginRequest.Username) == 0 {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
// 密码不匹配
if loginRequest.Password != user.Password {
sail.Response(c).Wrap(sailConstants.ErrRequestParamsInvalid, nil).Send()
return
}
headers := map[string]string{
"X-Request-Id": sail.LogTrace(c).RequestID(),
}
sail.Utils().HttpClient().SendRequest("POST", "https://....", nil, headers)
token := "this-is-a-valid-token"
sail.Response(c).Data(token)
})
userGroup := ginEngine.Group("/user").Use(ValidateToken())
{
userGroup.GET("/balance", ...).
GET("/info", ...).
GET("/logout", ...)
}
ginEngine.POST("/third-party/notify", func(c *gin.Context){
c.JSON(200, ...)
})
}
afterFunc = func() {
sail.GetDBW().AutoMigrate(&User)
var user User
sail.GetDBW().Where(&User{Username:"go-sail"}).First(&user)
if len(user.Username) == 0 {
passwordEncrypted, err := sail.Utils().RSA().Encrypt("password", publicKey)
sail.GetDBW().Create(&User{Username:"go-sail", password: passwordEncrypted})
}
sail.GetLogger("schedule").Info("logging something...")
}
)
func main() {
parseFn := func(content []byte, viaWatch bool) {
if viaWatch {
// 配置已重新加载,可在这里做点什么...
}
fmt.Println("config content: ", string(content))
yaml.Unmarshal(content, &conf)
}
sail.
Config(parseFn).
ViaFile("./go-sail.config.local.yaml").Parse(parseFn)
options := &api.Option{
ForceHttpCode200: true,
DetectAcceptLanguage: true,
}
sail.WakeupHttp("go-sail", conf).
SetupApiOption(options).
Hook(registerRoutes, nil, afterFunc).Launch()
}
配置提供者
Go-Sail 目前支持三种配置提供者,分别为:
- 文件
- Nacos
- Etcd
文件
parseFn := func(content []byte, viaWatch bool){
fmt.Println("config content: ", string(content))
if viaWatch {
// 重新加载配置...
}
}
filename := "go-sail.config.local.yaml"
sail.Config(parseFn).ViaFile(filename).Parse(parseFn)
文件监控模式基于文件的修改时间。
Nacos
parseFn := func(content []byte, viaWatch bool){
fmt.Println("config content: ", string(content))
if viaWatch {
// 重新加载配置...
}
}
endpoints := "endpoint1,endpoint2"
namspaceID := ""
groupName := ""
dataID := "go-sail.config.local.yaml"
sail.Config(true, parseFn).ViaNacos(endpoints, namespaceID, groupName, dataID).Parse(parseFn)
Etcd
parseFn := func(content []byte, viaWatch bool){
fmt.Println("config content: ", string(content))
if viaWatch {
// 重新加载配置...
}
}
etcdConf := etcd.Conf{
Endpoints: []string{""},
Username: "",
Password: "",
}
key := "go-sail.config.local.yaml"
sail.Config(parseFn).ViaEtcd(etcdConf, key).Parse(parseFn)
作为组件
当使用 Nacos 和 Etcd 方法获取配置时,可以指定是否将其作为组件激活。只需指定一个布尔类型(true/false)参数即可非常简单地激活。
parseFn := func(content []byte, viaWatch bool){
fmt.Println("config content: ", string(content))
if viaWatch {
// 重新加载配置...
}
}
etcdConf := etcd.Conf{
Endpoints: []string{""},
Username: "",
Password: "",
}
key := "go-sail.config.local.yaml"
sail.
Config(parseFn, true).
ViaEtcd(etcdConf, key).Parse(parseFn)
接下来即可通过控制面板获取该组件实例:
sail.GetEtcdInstance()