GoLong的学习之路,进阶,Viper(yaml等配置文件的管理)

本来有今天是继续接着上一章写微服务的。但是这几天有朋友说,再写Web框架的时候,遇到一个问题,就是很多的中间件(redis,微信,mysql,mq)的配置信息写的太杂了,很不好管理。希望我能写一篇有管理配置文件的。所以这篇就放到今天写吧。微服务就放到下一篇来说吧。今天介绍的主角:Viper

文章目录

  • Viper概念
    • 安装
  • 使用VIper
    • 默认值
    • 使用->配置文件
      • 读取配置文件:
      • 写入配置文件
      • 监控并重新读取配置文件(运行时读取配置文件)
      • 从`io.Reader`读取配置
      • 注册和使用别名
    • 环境变量
    • 命令行参数(flag)
    • `key/value`存储(远程`Key/Value`存储支持)(微服务)
      • etcd
      • Consul
      • Firestore
    • 从Viper获取值
      • 访问嵌套的键
      • 提取子树
    • 反序列化
      • 序列化成字符串
      • 使用多个viper实例
  • 使用Viper示例
    • 直接使用viper管理配置
    • 使用结构体变量保存配置信息

Viper概念

Viper是适用于Go应用程序(包括Twelve-Factor App)的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。

它支持以下特性:

  • 设置默认值
  • JSONTOMLYAMLHCLenvfileJava properties格式的配置文件读取配置信息
  • 实时监控和重新读取配置文件(可选)
  • 从环境变量中读取
  • 从远程配置系统(etcdConsul)读取并监控配置变化
  • 从命令行参数读取配置
  • buffer(缓冲)读取配置
  • 显式配置值

在构建现代应用程序时,你无需担心配置文件格式。

Viper能够为你执行下列操作:

  1. 查找,加载,和反序列化JSONTOMLYAMLHCLINIenvfileJava properties格式的配置文件。
  2. 提供一种机制,为不同配置选项设置默认值。
  3. 提供一种机制来通过命令行参数覆盖指定选项的值。
  4. 提供别名系统,以便在不破坏现有代码的情况下轻松重命名参数。
  5. 当用户提供了,与默认值相同的命令行或者配置文件时,可以很容易分辨出他们之间的不通。

Viper会按照下面的优先级,每个项目的优先级都高于它下面的项目:

  • 显示调用Set设置值
  • 命令行参数(flag)
  • 环境变量
  • 配置文件
  • key/value存储
  • 默认值

目前Viper配置的键(Key)是大小写不敏感的。目前正在讨论是否将这一选项设为可选。

Viper让需要重启服务器才能使配置生效的日子一去不复返!!!

这才是VIper最大的魅力。

Viper没有默认的基础配置,所以在使用的过程中我们初始化Viper实例的时候需要告诉Viper你的配置路径配置格式配置名称等等信息。

Viper虽然支持多配置同时使用,但是一个Viper实例只能寻一个配置路径。

安装

go get github.com/spf13/viper

使用VIper

默认值

Viper 支持使用 viper.SetDefault(key, value) 为 key 设置默认值 value

Viper配置的键(Key)是大小写不敏感的


viper.SetDefault("ContentDir", "content")
viper.SetDefault("LayoutDir", "layouts")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})

键不需要默认值,但如果没有通过配置文件、环境变量、远程配置或命令行标志(flag)设置键,则默认值非常有用。

package main
import ("fmt""github.com/spf13/viper"
)
func main() {// 设置默认配置viper.SetDefault("username", "李四")viper.SetDefault("server", map[string]string{"ip": "127.0.0.1", "port": "8080"})// 读取配置值fmt.Printf("username: %s\n", viper.Get("Username")) // key 不区分大小写fmt.Printf("server: %+v\n", viper.Get("server"))
}
username: jianghushinian
server: map[ip:127.0.0.1 port:8080]

使用->配置文件

读取配置文件:

  • Viper需要最少知道在哪里查找配置文件的配置。

  • Viper可以搜索多个路径,但目前单个Viper实例只支持单个配置文件。

  • Viper不默认任何配置搜索路径,将默认决策留给应用程序。

viper.SetConfigFile("./config.yaml") // 指定配置文件路径
viper.SetConfigName("config") // 配置文件名称(无扩展名)
viper.SetConfigType("yaml") // 如果配置文件的名称中没有扩展名,则需要配置此项
viper.AddConfigPath("/etc/appname/")   // 查找配置文件所在的路径
viper.AddConfigPath("$HOME/.appname")  // 多次调用以添加多个搜索路径
viper.AddConfigPath(".")               // 还可以在工作目录中查找配置
err := viper.ReadInConfig() // 查找并读取配置文件
if err != nil { // 处理读取配置文件的错误panic(fmt.Errorf("Fatal error config file: %s \n", err))
}

在加载配置文件出错时,可以像下面这样处理找不到配置文件的特定情况:

if err := viper.ReadInConfig(); err != nil {if _, ok := err.(viper.ConfigFileNotFoundError); ok {// 配置文件未找到错误;如果需要可以忽略} else {// 配置文件被找到,但产生了另外的错误}
}// 配置文件找到并成功解析
  • 通过 viper.SetConfigFile() 指定配置文件,如果配置文件名中没有扩展名,则需要使用 viper.SetConfigType() 显式指定配置文件的格式。
  • 通过 viper.AddConfigPath() 指定配置文件的搜索路径中,可以通过多次调用,来设置多个配置文件搜索路径。
    • 然后通过 viper.SetConfigName() 指定不带扩展名的配置文件,Viper 会根据所添加的路径顺序查找配置文件,如果找到就停止查找。
package main
import ("errors""flag""fmt""github.com/spf13/viper"
)
var (cfg = flag.String("c", "", "config file.")
)
func main() {flag.Parse()if *cfg != "" {viper.SetConfigFile(*cfg)   // 指定配置文件(路径 + 配置文件名)viper.SetConfigType("yaml") // 如果配置文件名中没有扩展名,则需要显式指定配置文件的格式} else {viper.AddConfigPath(".")             // 把当前目录加入到配置文件的搜索路径中viper.AddConfigPath("$HOME/.config") // 可以多次调用 AddConfigPath 来设置多个配置文件搜索路径viper.SetConfigName("cfg")           // 指定配置文件名(没有扩展名)}// 读取配置文件if err := viper.ReadInConfig(); err != nil {if _, ok := err.(viper.ConfigFileNotFoundError); ok {fmt.Println(errors.New("config file not found"))} else {fmt.Println(errors.New("config file was found but another error was produced"))}return}fmt.Printf("using config file: %s\n", viper.ConfigFileUsed())// 读取配置值fmt.Printf("username: %s\n", viper.Get("username"))
}

如果有多个名称为config的配置文件,viper怎么搜索呢?它会按照如下顺序搜索

  • config.json
  • config.toml
  • config.yaml
  • config.yml
  • config.properties (这种一般是java中的配置文件名)
  • config.props (这种一般是java中的配置文件名)

写入配置文件

从配置文件中读取配置文件是有用的,但是有时你想要存储在运行时所做的所有修改。

所以这个,就是Viper的最大的作用。

viper.WriteConfig() // 将当前配置写入“viper.AddConfigPath()”和“viper.SetConfigName”设置的预定义路径
viper.SafeWriteConfig()
viper.WriteConfigAs("/path/to/my/.config")
viper.SafeWriteConfigAs("/path/to/my/.config") // 因为该配置文件写入过,所以会报错
viper.SafeWriteConfigAs("/path/to/my/.other_config")
  • WriteConfig - 将当前的viper配置写入预定义的路径并覆盖(如果存在的话)。如果没有预定义的路径,则报错。
  • SafeWriteConfig - 将当前的viper配置写入预定义的路径。如果没有预定义的路径,则报错。如果存在,将不会覆盖当前的配置文件。
  • WriteConfigAs - 将当前的viper配置写入给定的文件路径。将覆盖给定的文件(如果它存在的话)。
  • SafeWriteConfigAs - 将当前的viper配置写入给定的文件路径。不会覆盖给定的文件(如果它存在的话)。

监控并重新读取配置文件(运行时读取配置文件)

Viper 支持在应用程序运行过程中实时读取配置文件,即热加载配置。

只需要调用 viper.WatchConfig() 即可开启此功能。

viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {// 配置文件发生变更之后会调用的回调函数fmt.Println("Config file changed:", e.Name)
})

viper驱动的应用程序可以在运行时读取配置文件的更新,而不会错过任何消息。

package main
import ("fmt""time""github.com/fsnotify/fsnotify""github.com/spf13/viper"
)
func main() {viper.SetConfigFile("./config.yaml")viper.ReadInConfig()// 注册每次配置文件发生变更后都会调用的回调函数viper.OnConfigChange(func(e fsnotify.Event) {fmt.Printf("config file changed: %s\n", e.Name)})// 监控并重新读取配置文件,需要确保在调用前添加了所有的配置路径viper.WatchConfig()// 阻塞程序,这个过程中可以手动去修改配置文件内容,观察程序输出变化time.Sleep(time.Second * 10)// 读取配置值fmt.Printf("username: %s\n", viper.Get("username"))
}

调用了set方法,用了Viper中的最高优先级,对原来的配置文件做覆盖。但是要让这个配置文件在运行中启动,就需要调用:viper.WatchConfig()

也就是说:在调用viper.WatchConfig()监控并重新读取配置文件之前,需要确保添加了所有的配置路径。

io.Reader读取配置

Viper 支持从任何实现了io.Reader接口的配置源中读取配置

package main
import ("bytes""fmt""github.com/spf13/viper"
)
func main() {viper.SetConfigType("yaml") // 或者使用 viper.SetConfigType("YAML")var yamlExample = []byte(`username: jianghushinianpassword: 123456server:ip: 127.0.0.1port: 8080`)viper.ReadConfig(bytes.NewBuffer(yamlExample))// 读取配置值fmt.Printf("username: %s\n", viper.Get("username"))
}

这里我们通过 bytes.NewBuffer() 构造了一个 bytes.Buffer 对象,它实现了 io.Reader 接口,所以可以直接传递给 viper.ReadConfig()来从中读取配置。

注册和使用别名

viper.RegisterAlias("loud", "Verbose")  // 注册别名(此处loud和Verbose建立了别名)
viper.Set("verbose", true) // 结果与下一行相同
viper.Set("loud", true)   // 结果与前一行相同
viper.GetBool("loud") // true
viper.GetBool("verbose") // true

环境变量

Viper 还支持从环境变量读取配置,有 5 个方法可以帮助我们使用环境变量:

  • AutomaticEnv():可以绑定全部环境变量(用法上类似 flag 包的 flag.Parse())。调用后,Viper 会自动检测和加载所有环境变量。
  • BindEnv(string...) : error:绑定一个环境变量。需要一个或两个参数,第一个参数是配置项的键名,第二个参数是环境变量的名称。
    • 如果未提供第二个参数,则 Viper 将假定环境变量名为:环境变量前缀_键名,且为全大写形式。
      • 例如环境变量前缀为 ENV,键名为 username,则环境变量名为 ENV_USERNAME
    • 当显式提供第二个参数时,它不会自动添加前缀,也不会自动将其转换为大写。
      • 例如,使用viper.BindEnv("username", "username")绑定键名为 username 的环境变量,应该使用 viper.Get("username") 读取环境变量的值。

在使用环境变量时,需要注意,每次访问它的值时都会去环境变量中读取。当调用 BindEnv 时,Viper 不会固定它的值。

  • SetEnvPrefix(string):可以告诉 Viper 在读取环境变量时使用的前缀。BindEnvAutomaticEnv 都将使用此前缀。
    • 例如,使用 viper.SetEnvPrefix("ENV") 设置了前缀为 ENV,并且使用 viper.BindEnv("username") 绑定了环境变量,在使用 viper.Get("username") 读取环境变量时,实际读取的 keyENV_USERNAME
  • SetEnvKeyReplacer(string...) *strings.Replacer:允许使用 strings.Replacer 对象在一定程度上重写环境变量的键名。
    • 例如,存在 SERVER_IP="127.0.0.1" 环境变量,使用 viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) 将键名中的.- 替换成 _,则通过 viper.Get("server_ip")viper.Get("server.ip")viper.Get("server-ip") 三种方式都可以读取环境变量对应的值。
  • AllowEmptyEnv(bool):当环境变量为空时(有键名而没有值的情况),默认会被认为是未设置的,并且程序将回退到下一个配置来源。要将空环境变量视为已设置,可以使用此方法。
package main
import ("fmt""strings""github.com/spf13/viper"
)
func main() {viper.SetEnvPrefix("env") // 设置读取环境变量前缀,会自动转为大写 ENVviper.AllowEmptyEnv(true) // 将空环境变量视为已设置viper.AutomaticEnv()      // 可以绑定全部环境变量viper.BindEnv("username") // 也可以单独绑定某一个环境变量viper.BindEnv("password")// 将键名中的 . 或 - 替换成 _viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))// 读取配置fmt.Printf("username: %v\n", viper.Get("username"))fmt.Printf("password: %v\n", viper.Get("password"))fmt.Printf("server.ip: %v\n", viper.Get("server.ip"))// 读取全部配置,只能获取到通过 BindEnv 绑定的环境变量,无法获取到通过 AutomaticEnv 绑定的环境变量fmt.Println(viper.AllSettings())
}

命令行参数(flag)

安装pflag

go get github.com/spf13/pflag

Viper 支持 pflag 包(它们其实都在 spf13 仓库下),能够绑定命令行标志,从而读取命令行参数。

BindEnv 类似,在调用绑定方法时,不会设置值,而是在每次访问时设置。

这意味着我们可以随时绑定它,例如可以在 init() 函数中。

  • BindPFlag:对于单个标志,可以调用此方法进行绑定。
  • BindPFlags:可以绑定一组现有的标志集 pflag.FlagSet
package main
import ("fmt""github.com/spf13/pflag""github.com/spf13/viper"
)
var (username = pflag.StringP("username", "u", "", "help message for username")password = pflag.StringP("password", "p", "", "help message for password")
)
func main() {pflag.Parse()viper.BindPFlag("username", pflag.Lookup("username")) // 绑定单个标志viper.BindPFlags(pflag.CommandLine)                   // 绑定标志集// 读取配置值fmt.Printf("username: %s\n", viper.Get("username"))fmt.Printf("password: %s\n", viper.Get("password"))
}

因为 pflag 能够兼容标准库的 flag 包,所以我们也可以变相的让 Viper 支持 flag

在 Viper 中使用 pflag 并不阻碍其他包中使用标准库中的 flag 包。pflag 包可以通过导入这些 flags 来处理flag包定义的flags。这是通过调用pflag包提供的便利函数AddGoFlagSet()来实现的。

package main
import ("flag""fmt""github.com/spf13/pflag""github.com/spf13/viper"
)
func main() {flag.String("username", "", "help message for username")pflag.CommandLine.AddGoFlagSet(flag.CommandLine) // 将 flag 命令行参数注册到 pflagpflag.Parse()viper.BindPFlags(pflag.CommandLine)// 读取配置值fmt.Printf("username: %s\n", viper.Get("username"))
}

使用Flags

serverCmd.Flags().Int("port", 1138, "Port to run Application server on")
viper.BindPFlag("port", serverCmd.Flags().Lookup("port"))

如果你不使用PflagViper 提供了两个Go接口来绑定其他 flag 系统。

FlagValue表示单个flag

type myFlag struct {}
func (f myFlag) HasChanged() bool { return false }
func (f myFlag) Name() string { return "my-flag-name" }
func (f myFlag) ValueString() string { return "my-flag-value" }
func (f myFlag) ValueType() string { return "string" }

flag 实现了这个接口,Viper绑定。

viper.BindFlagValue("my-flag-name", myFlag{})

FlagValueSet代表一组 flags

type myFlagSet struct {flags []myFlag
}func (f myFlagSet) VisitAll(fn func(FlagValue)) {for _, flag := range flags {fn(flag)}
}

flag set实现了这个接口,就可以很方便地告诉Viper绑定它

fSet := myFlagSet{flags: []myFlag{myFlag{}, myFlag{}},
}
viper.BindFlagValues("my-flags", fSet)

key/value存储(远程Key/Value存储支持)(微服务)

Viper中启用远程支持,需要在代码中匿名导入viper/remote这个包。

import _ "github.com/spf13/viper/remote"

Viper 支持 etcd、Consul 等远程 key/value 存储,这里以 Consul 为例进行讲解。

而具体什么是Consul 这个会在微服务中体现,也就是说,这个处于微服务使用。

首先需要准备 Consul 环境:文章

Consul Key/Value存储中设置一个Key保存包含所需配置的JSON值。

package main
import ("fmt""github.com/spf13/viper"_ "github.com/spf13/viper/remote" // 必须导入,才能加载远程 key/value 配置
)
func main() {viper.AddRemoteProvider("consul", "localhost:8500", "user/config") // 连接远程 consul 服务viper.SetConfigType("YAML")                                        // 显式设置文件格式文 YAMLviper.ReadRemoteConfig()// 读取配置值fmt.Printf("username: %s\n", viper.Get("username"))fmt.Printf("server.ip: %s\n", viper.Get("server.ip"))
}

官方文档中有还提供这几点:etcd,Consul,Firestore:

etcd

viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001","/config/hugo.json")
viper.SetConfigType("json") // 因为在字节流中没有文件扩展名,所以这里需要设置下类型。
//支持的扩展名有 "json", "toml", "yaml", "yml", "properties", "props", "prop", "env", "dotenv"
err := viper.ReadRemoteConfig()

Consul

创建一个keyMY_CONSUL_KEY将下面的值存入Consul key/value 存储

{"port": 8080,"hostname": "liwenzhou.com"
}viper.AddRemoteProvider("consul", "localhost:8500", "MY_CONSUL_KEY")
viper.SetConfigType("json") // 需要显示设置成json
err := viper.ReadRemoteConfig()fmt.Println(viper.Get("port")) // 8080
fmt.Println(viper.Get("hostname")) // liwenzhou.com

Firestore

viper.AddRemoteProvider("firestore", "google-cloud-project-id", "collection/document")
viper.SetConfigType("json") // 配置的格式: "json", "toml", "yaml", "yml"
err := viper.ReadRemoteConfig()

这里其实还涉及到一个监控加密的问题,这里建议看官方文档

从Viper获取值

在Viper中,有几种方法可以根据值的类型获取值。存在以下功能和方法:Get(key string) : interface{}
GetBool(key string) : bool
GetFloat64(key string) : float64
GetInt(key string) : int
GetIntSlice(key string) : []int
GetString(key string) : string
GetStringMap(key string) : map[string]interface{}
GetStringMapString(key string) : map[string]string
GetStringSlice(key string) : []string
GetTime(key string) : time.Time
GetDuration(key string) : time.Duration
IsSet(key string) : bool
AllSettings() : map[string]interface{}

每一个Get方法在找不到值的时候都会返回零值。为了检查给定的键是否存在,提供了IsSet()方法。

访问嵌套的键

加载下面的JSON文件

{"host": {"address": "localhost","port": 5799},"datastore": {"metric": {"host": "127.0.0.1","port": 3099},"warehouse": {"host": "198.0.0.1","port": 2112}}
}GetString("datastore.metric.host") // (返回 "127.0.0.1")

搜索路径将遍历其余配置注册表,直到找到为止。

译注:因为Viper支持从多种配置来源,例如磁盘上的配置文件>命令行标志位>环境变量>远程Key/Value存储>默认值,我们在查找一个配置的时候如果在当前配置源中没找到,就会继续从后续的配置源查找,直到找到为止。

如果datastore.metric被直接赋值覆盖(被flag环境变量set()方法等等…),那么datastore.metric的所有子键都将变为未定义状态,它们被高优先级配置级别“遮蔽”(shadowed)了。

提取子树

app:cache1:max-items: 100item-size: 64cache2:max-items: 200item-size: 80
subv := viper.Sub("app.cache1")

假设我们现在有这么一个函数:

func NewCache(cfg *Viper) *Cache {...}

它基于subv格式的配置信息创建缓存。现在,可以轻松地分别创建这两个缓存。

cfg1 := viper.Sub("app.cache1")
cache1 := NewCache(cfg1)cfg2 := viper.Sub("app.cache2")
cache2 := NewCache(cfg2)

反序列化

这个也是其中最重要的特点。

可以选择将所有或特定的值解析到结构体、map等:

  • Unmarshal(rawVal interface{}) : error
  • UnmarshalKey(key string, rawVal interface{}) : error
type config struct {Port intName stringPathMap string `mapstructure:"path_map"`
}var C configerr := viper.Unmarshal(&C)
if err != nil {t.Fatalf("unable to decode into struct, %v", err)
}

想要解析那些键本身就包含.(默认的键分隔符)的配置,你需要修改分隔符:

v := viper.NewWithOptions(viper.KeyDelimiter("::"))v.SetDefault("chart::values", map[string]interface{}{"ingress": map[string]interface{}{"annotations": map[string]interface{}{"traefik.frontend.rule.type":                 "PathPrefix","traefik.ingress.kubernetes.io/ssl-redirect": "true",},},
})type config struct {Chart struct{Values map[string]interface{}}
}var C configv.Unmarshal(&C)

序列化成字符串

可能需要将viper中保存的所有设置序列化到一个字符串中,而不是将它们写入到一个文件中。

你可以将自己喜欢的格式的序列化器与AllSettings()返回的配置一起使用。

import (yaml "gopkg.in/yaml.v2"// ...
)func yamlStringSettings() string {c := viper.AllSettings()bs, err := yaml.Marshal(c)if err != nil {log.Fatalf("unable to marshal config to YAML: %v", err)}return string(bs)
}

使用多个viper实例

可以在应用程序中创建许多不同的viper实例

  • 每个都有自己独特的一组配置和值。

  • 每个人都可以从不同的配置文件,key value存储区等读取数据。

  • 每个都可以从不同的配置文件、键值存储等中读取。

viper包支持的所有功能都被镜像为viper实例的方法。

x := viper.New()
y := viper.New()x.SetDefault("ContentDir", "content")
y.SetDefault("ContentDir", "foobar")//...

当使用多个viper实例时,由用户来管理不同的viper实例。

使用Viper示例

现在有一个./conf/config.yaml配置文件

port: 8123
version: "v1.2.3"

直接使用viper管理配置

gin框架搭建的web项目中使用viper,使用viper加载配置文件中的信息,并在代码中直接使用viper.GetXXX()方法获取对应的配置值。

package mainimport ("fmt""net/http""github.com/gin-gonic/gin""github.com/spf13/viper"
)func main() {viper.SetConfigFile("./conf/config.yaml") // 指定配置文件路径err := viper.ReadInConfig()        // 读取配置信息if err != nil {                    // 读取配置信息失败panic(fmt.Errorf("Fatal error config file: %s \n", err))}// 监控配置文件变化viper.WatchConfig()r := gin.Default()// 访问/version的返回值会随配置文件的变化而变化r.GET("/version", func(c *gin.Context) {c.String(http.StatusOK, viper.GetString("version"))})if err := r.Run(fmt.Sprintf(":%d", viper.GetInt("port"))); err != nil {panic(err)}
}

使用结构体变量保存配置信息

可以在项目中定义与配置文件对应的结构体,viper加载完配置信息后使用结构体变量保存配置信息。

package mainimport ("fmt""net/http""github.com/fsnotify/fsnotify""github.com/gin-gonic/gin""github.com/spf13/viper"
)type Config struct {Port    int    `mapstructure:"port"`Version string `mapstructure:"version"`
}var Conf = new(Config)func main() {viper.SetConfigFile("./conf/config.yaml") // 指定配置文件路径err := viper.ReadInConfig()               // 读取配置信息if err != nil {                           // 读取配置信息失败panic(fmt.Errorf("Fatal error config file: %s \n", err))}// 将读取的配置信息保存至全局变量Confif err := viper.Unmarshal(Conf); err != nil {panic(fmt.Errorf("unmarshal conf failed, err:%s \n", err))}// 监控配置文件变化viper.WatchConfig()// 注意!!!配置文件发生变化后要同步到全局变量Confviper.OnConfigChange(func(in fsnotify.Event) {fmt.Println("夭寿啦~配置文件被人修改啦...")if err := viper.Unmarshal(Conf); err != nil {panic(fmt.Errorf("unmarshal conf failed, err:%s \n", err))}})r := gin.Default()// 访问/version的返回值会随配置文件的变化而变化r.GET("/version", func(c *gin.Context) {c.String(http.StatusOK, Conf.Version)})if err := r.Run(fmt.Sprintf(":%d", Conf.Port)); err != nil {panic(err)}
}

这里献上官方文档

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/206805.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【解决办法】Pycharm中新添加或者导入项目文件名红色!

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 一、问题描述二、问题原因三、解决办法 一、问题描述 Pycharm的代码中添加新的文件夹,发现文件夹下的文件名是红色的,如下图: …

jvm-sandbox-repeater 精简版部署之standalone模式

jvm-sandbox-repeater 仅仅提供了录制回放的能力,如果需要完成业务回归、实时监控、压测等平台,后面须要有一个数据中心负责采集数据的加工、存储、搜索,repeater-console提供了简单的demo示例;一个模块管理平台负责管理JVM-Sandb…

设计模式——单例模式(Singleton Pattern)

概述 单例模式确保一个类只有一个实例,而且自行实例化并向整个系统提供整个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。单例模式有三个要点:一是某个类只能有一个实例;二是它必须自行…

矩阵基本操作算法

题目描述: 题目描述 根据主函数和头文件提示,编写子函数void matrixInput(int (*mat)[COL]); void matrixPrint(int *mat[ROW]); void matrixAddT(int *mat); ​ 分别实现矩阵的输入,输出,与自身的转置相加:void matr…

微信小程序开发中的图片缺少后自动居中问题的解决,自动居中样式,自适应图片多少的写法

下面是写的wxml的view样式类似于web开发中的HTML文件 home-grid是我创建的一个父样式,采用子绝父相的定位方式 我在home-grid的父view(类似于web中div)中包含了三个小的子样式分别是下述代码中的class“yellowstar”,class“maint…

Java---线程讲解(二)

文章目录 1. Runnable接口2. 卖票案例3. 同步代码块解决数据安全问题4. 同步方法解决数据安全问题5. 线程安全的类6. Lock锁 1. Runnable接口 1. 创建线程的另一种方法是声明一个实现Runnable接口的类,之后重写run()方法,然后可以分配类的实例&#xff0…

基于remix+metamask+ganache的智能合约部署调用

在我们部署合约时为了让它更接近真实区块链去中心化体验,我们需要调用小狐狸(Metamask)来进行真实交易,而metamask里没有内置虚拟测试币,我们需要进行调用Ganache来添加带有虚拟测试币的账号。以上就是三者的关系&…

从 MLOps 到 LMOps 的关键技术嬗变

本文整理自 2023 年 9 月 3 日 QCon 全球软件开发大会 2023 北京站 —— 从 MLOps 到 LMOps 分论坛的同名主题演讲。 本次分享的内容结构如下: 从 MLOps 到 LMOps; MLOps 概述、挑战与解决方案; LMOps 实施挑战与关键技术(大模…

[FPGA 学习记录] 快速开发的法宝——IP核

快速开发的法宝——IP核 文章目录 1 IP 核是什么2 为什么要使用 IP 核3 IP 核的存在形式4 IP 核的缺点5 Quartus II 软件下 IP 核的调用6 Altera IP 核的分类 在本小节当中,我们来学习一下 IP 核的相关知识。 IP 核在 FPGA 开发当中应用十分广泛,它被称为…

Java最全面试题专题---1、Java基础知识(2)

笔者有七八年的面试者经验,负责公司技术同学的社招和校招,近些年面试过三四百个技术同学,考虑接近年底这个时段,整理并更新一些以往的面试经验,希望同学们跳槽能有个更好的工作,如有需要的同学可以关注下笔…

Jenkins安装

环境 Ubuntu, 其他平台查看官方文档 步骤 安装jdk sudo apt-get install openjdk-8-jdk 安装Jenkins first add the key to your system wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -Then add a Jenkins apt repository …

使用Golang构建高性能网络爬虫

目录 一、Golang的特点 二、构建网络爬虫的步骤 三、关键技术和注意事项 使用协程进行并发处理 使用通道进行协程间的通信 合理控制并发数和处理速度 遵守网站使用协议和法律法规 防止被网站封禁或限制访问 优化网页解析和数据处理 异常处理和错误处理 日志记录和监控…

Flink入门之部署(二)

三种部署模式 standalone集群,会话模式部署:先启动flink集群 web UI提交shell命令提交:bin/flink run -d -m hadoop102:8081 -c com.atguigu.flink.deployment.Flinke1_NordCount./Flink-1.0-SNAPSHOT.jar --hostname hadoop102 --port 8888 …

vmware虚拟机17 安装macos14过程及问题处理亲测

前期准备 1、可引导可虚拟机安装的macOS Sonoma 14 ISO镜像安装文件 我找到得地址,下载自行解决啦 2、VMware虚拟机应用软件 官网下载就好,搜个码搞定 3、解锁工具macOS Unlocker 开始安装: 1、打开VMware软件,新建一个系统…

API集锦:免费好用的API接口

通用文字识别OCR:多场景、多语种、高精度的整图文字检测和识别服务,多项指标行业领先,可识别中、英、日、韩、法、德多种语言。 二维码识别OCR:对图片中的二维码、条形码进行检测和识别,返回存储的文字内容。 手机号…

Bert-vits2新版本V2.1英文模型本地训练以及中英文混合推理(mix)

中英文混合输出是文本转语音(TTS)项目中很常见的需求场景,尤其在技术文章或者技术视频领域里,其中文文本中一定会夹杂着海量的英文单词,我们当然不希望AI口播只会念中文,Bert-vits2老版本(2.0以下版本)并不支持英文训练和推理&…

完整方案开放下载!详解中国移动《通信网络中量子计算应用研究报告》

8月30日,中国移动在第四届科技周暨战略性新兴产业共创发展大会上重磅发布了《通信网络中量子计算应用研究报告》。 玻色量子作为中国移动在光量子计算领域的唯一一家合作企业兼战投企业,在量子计算应用于通信行业达成了深入合作,并在5G天线多…

干货分享|300平米A级机房设计方案

本方案中XXX计算机中心机房建设工程,是XXX的数据中心,机房位于建筑的X层,计算机机房面积300㎡。采购设备以及装修工艺主要用于XXX所属计算机机房装修工程。 考虑到中心机房投资大、使用周期长,而业主业务发展快,现代技…

十二 动手学深度学习v2计算机视觉 ——目标检测

文章目录 锚框目标检测常用算法RCNNFast RCNNFaster RCNNSSD 单发多框检测YOLO 锚框 一类目标检测算法:以图像的每个像素为中心生成不同形状的锚框,并赋予标号,每个锚框作为一个样本进行训练。在预测时,使用NMS来去掉冗余的预测。…

空间地图GIS基础

关注微信公众号掌握更多技术动态 --------------------------------------------------------------- 一、GIS基本概念 地理信息系统(Geographic Informaiton System, GIS)是一个可以建立、浏览、查询、分析地理空间数据的软件系统,其功能小…