最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Go項(xiàng)目配置管理神器之viper的介紹與使用詳解

 更新時(shí)間:2023年02月14日 10:48:30   作者:CK持續(xù)成長(zhǎng)  
viper是一個(gè)完整的?Go應(yīng)用程序的配置解決方案,它被設(shè)計(jì)為在應(yīng)用程序中工作,并能處理所有類型的配置需求和格式,下面這篇文章主要給大家介紹了關(guān)于Go項(xiàng)目配置管理神器之viper的介紹與使用,需要的朋友可以參考下

1. viper的介紹

viper是go一個(gè)強(qiáng)大的流行的配置解決方案的庫(kù)。viper是spf13的另外一個(gè)重量級(jí)庫(kù)。有大量項(xiàng)目都使用該庫(kù),比如hugo, docker等。 它基本上可以處理所有類型的配置需求和格式, viper支持功能

  • 設(shè)置默認(rèn)配置
  • 支持各種配置文件,如JSON,TOML, YAML, HCL, envfile和Java屬性配置文件
  • 支持監(jiān)聽(tīng)文件變化以及重新讀取配置
  • 支持從環(huán)境變量讀取配置
  • 支持從遠(yuǎn)程配置系統(tǒng)(etcd或Consul)讀取配置,并能監(jiān)聽(tīng)遠(yuǎn)程配置修改
  • 支持從命令行標(biāo)志Flag讀取配置,比如搭配cobra使用
  • 支持讀取緩沖區(qū)數(shù)據(jù)

Viper主要為我們做以下工作:

  • 查找、加載和解組JSON、TOML、YAML、HCL、INI、envfile或Java屬性格式的配置文件。
  • 提供一種機(jī)制來(lái)為不同的配置選項(xiàng)設(shè)置默認(rèn)值。
  • 提供一種機(jī)制來(lái)為通過(guò)命令行標(biāo)志指定的選項(xiàng)設(shè)置覆蓋值。
  • 提供別名系統(tǒng),以便在不破壞現(xiàn)有代碼的情況下輕松重命名參數(shù)。
  • 當(dāng)用戶提供了與默認(rèn)值相同的命令行或配置文件時(shí),很容易區(qū)分它們。

viepr的安裝很簡(jiǎn)單,直接再工程中使用go get命令安裝即可

$ go get github.com/spf13/viper

2. viper的使用

2.1  Viper對(duì)象的創(chuàng)建

Viper的是viper庫(kù)的主要實(shí)現(xiàn)對(duì)象, viper提供了下面的方法可以獲取Viper實(shí)例:

func GetViper() *Viper
func New() *Viper
func NewWithOptions(opts ...Option) *Viper
func Sub(key string) *Viper

使用viper.GetViper()獲取的為全局的Viper實(shí)例對(duì)象,默認(rèn)使用viper包使用也是該全局Viper實(shí)例。查看viper的源碼,可以看到viper默認(rèn)提供了一個(gè)全局的Viper實(shí)例:

var v *Viper
 
func init() {
	v = New()
}
 
// New returns an initialized Viper instance.
func New() *Viper {
	v := new(Viper)
	v.keyDelim = "."
	v.configName = "config"
	v.configPermissions = os.FileMode(0o644)
	v.fs = afero.NewOsFs()
	v.config = make(map[string]interface{})
	v.override = make(map[string]interface{})
	v.defaults = make(map[string]interface{})
	v.kvstore = make(map[string]interface{})
	v.pflags = make(map[string]FlagValue)
	v.env = make(map[string][]string)
	v.aliases = make(map[string]string)
	v.typeByDefValue = false
	v.logger = jwwLogger{}
 
	v.resetEncoding()
 
	return v
}

New和NewWithOptions為我們提供了創(chuàng)建實(shí)例的方法

func New1() *viper.Viper {
	return viper.New()
}
 
func New2() *viper.Viper {
	return viper.NewWithOptions()
}

Sub為我們讀取子配置項(xiàng)提供了一個(gè)新的實(shí)例Viper

v := viper.Sub("db")
url := v.Get("url")
log.Printf("mysql url:%s\n", url)

2.2 預(yù)設(shè)一些默認(rèn)配置

viper.SetDefault("ContentDir", "content")
viper.SetDefault("Taxonomies", map[string]string{"tag": "tags", "category": "categories"})
 
viper.SetDefault("redis.port", 6379)
viper.SetDefault("mysql.url", "root:root@tcp(127.0.0.1:3306)/stock?charset=utf8mb4&parseTime=True&loc=Local")

2.3 從命令行工具的選項(xiàng)參數(shù)Flags讀取

viper主要提供了以下四個(gè)方法,可以綁定行參數(shù)的輸出的選項(xiàng)值:

func (v *Viper) BindFlagValue(key string, flag FlagValue) error
func (v *Viper) BindFlagValues(flags FlagValueSet) (err error)
func (v *Viper) BindPFlag(key string, flag *pflag.Flag) error
func (v *Viper) BindPFlags(flags *pflag.FlagSet) error

這里我們主要結(jié)合之前講的cobra庫(kù)中的pflag來(lái)講解一下viper對(duì)Flags選項(xiàng)參數(shù)的綁定。
在cobra中,我們主要通過(guò)cobra.Command來(lái)組織不同的命令和子命令,這里我們我通過(guò)在root根命令來(lái)做測(cè)試。代碼如下:

func init(){
	rootCmd.Flags().String("author", "YOUR NAME", "Author name for copyright attribution")
	rootCmd.Flags().String("email", "YOUR EMAIL", "Author email for contact")
 
	// 綁定多個(gè)key-value值
	viper.BindPFlags(rootCmd.Flags())
 
  // 單個(gè)綁定不同的key
	viper.BindPFlag("author", rootCmd.Flags().Lookup("author"))
	viper.BindPFlag("email", rootCmd.Flags().Lookup("email"))
 
	rootCmd.AddCommand(version.VersionCmd)
}

在cobra的命令的run回調(diào)方法中,我們通過(guò)viper的來(lái)獲取輸入的選項(xiàng)值

func run(){
	fmt.Println("go root cmd run")
 
	fmt.Println(viper.GetString("author"))
 
	fmt.Println(viper.GetString("email"))
}

啟動(dòng)飲用,傳入?yún)?shù)測(cè)試一下:

go run main.go --author ckeen --email ck@gmail.com

查看一下打印結(jié)果,可以看到從viper成功獲取到以flag傳入的參數(shù)值:

?  cli git:(master) ? go run main.go --author keen --email ck@gmail.com
go root cmd run
ckeen
ck@gmail.com

2.4 從環(huán)境變量讀取

viper支持環(huán)境變量的函數(shù):

func (v *Viper) AutomaticEnv()			// 開(kāi)啟綁定環(huán)境變量
func (v *Viper) BindEnv(input ...string) error		// 綁定系統(tǒng)中某個(gè)環(huán)境變量
func (v *Viper) SetEnvKeyReplacer(r *strings.Replacer)
func (v *Viper) SetEnvPrefix(in string)

使用方法:

使用AutomaticEnv()開(kāi)啟綁定環(huán)境變量,沒(méi)開(kāi)啟的時(shí)候不會(huì)從環(huán)境變量獲取,開(kāi)啟后可以獲取環(huán)境變量的值。如果不想開(kāi)啟所有環(huán)境變量值,可以使用BindEnv(input …string)方法綁定單個(gè)環(huán)境變量的綁定, 那么只有該綁定的環(huán)境變量的key才能獲取到值

綁定環(huán)境變量后,可以使用正常的Get方法來(lái)獲取變量值,示例代碼如下:

func testEnv(){
	v := New1()
 
	os.Setenv("CK_HOME","123")
	os.Setenv("CK_NAME","ckeen")
 
	v.AutomaticEnv()
	//v.BindEnv("SHELL")
	v.AllowEmptyEnv(true)
 
	log.Printf("os env:%+v\n", os.Environ())
 
	log.Printf("env: %+v\n", v.Get("HOME"))
	log.Printf("env: %+v\n", v.Get("SHELL"))
 
	v.SetEnvPrefix("CK")
 
	log.Printf("ck-home: %+v\n", v.Get("HOME"))
	log.Printf("ck-email: %+v\n", v.Get("NAME"))
 
}

還可以通過(guò)SetEnvPrefix()方法設(shè)置環(huán)境變量前綴, 前綴和Key之間用下劃線分割

2.5 從配置文件讀取

下面我們看一下操作實(shí)例, 先看我們的配置文件app.yml文件:

app:
  name: viper-test
  mode: dev
 
db:
  mysql:
    url: "root:root@tcp(127.0.0.1:3306)/stock?charset=utf8mb4&parseTime=True&loc=Local"
  redis:
    host: 127.0.0.1
    port:  6067
    db: 0
    passwd: 123456

初始化配置

func InitConfig() (*viper.Viper, error) {
  v := viper.New()
  v.AddConfigPath(".")					// 添加配置文件搜索路徑,點(diǎn)號(hào)為當(dāng)前目錄
  v.AddConfigPath("./configs")		// 添加多個(gè)搜索目錄
  v.SetConfigType("yaml")				// 如果配置文件沒(méi)有后綴,可以不用配置
  v.SetConfigName("app.yml")			// 文件名,沒(méi)有后綴
	
	// v.SetConfigFile("configs/app.yml")
  
  // 讀取配置文件
  if err := v.ReadInConfig(); err == nil {
		log.Printf("use config file -> %s\n", v.ConfigFileUsed())
	} else {
		return nil,err
	}
  return v, nil
}

首先這里我們添加一個(gè)配置文件搜索路徑,點(diǎn)號(hào)表示當(dāng)前路徑,搜索路徑可以添加多個(gè)然后設(shè)置了配置文件類型,這里我們?cè)O(shè)置文件類型為yaml,

接著我們?cè)O(shè)置了配置文件名稱,這個(gè)文件可以從配置的搜索路徑從查找。

最后我們通過(guò)提供的ReadInConfig()函數(shù)讀取配置文件

讀取配置文件

// 通過(guò).號(hào)來(lái)區(qū)分不同層級(jí),來(lái)獲取配置值
log.Printf("app.mode=%s\n", v.Get("app.mode"))
log.Printf("db.mysql.url=%s\n", v.Get("db.mysql.url"))
log.Printf("db.redis.host=%s\n", v.GetString("db.redis.host"))
log.Printf("db.redis.port=%d\n", v.GetInt("db.redis.port"))
 
// 使用Sub獲取子配置,然后獲取配置值
v2 := v.Sub("db")
log.Printf("db.mysql.url:%s\n", v2.Sub("mysql").GetString("url"))
log.Printf("db.redis.host:%s\n", v2.Sub("redis").GetString("host"))
log.Printf("db.redis.port:%s\n", v2.Sub("redis").GetInt("port"))

viper還提供了如下獲取類型獲取配置項(xiàng)值:

 注: 其中重要的一個(gè)函數(shù)IsSet可以用來(lái)判斷某個(gè)key是否被設(shè)置

2.6 從遠(yuǎn)程key/value存儲(chǔ)讀取

在Viper中啟用遠(yuǎn)程支持,需要在代碼中匿名導(dǎo)入viper/remote這個(gè)包。

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

Viper將讀取從Key/Value存儲(chǔ)中的路徑檢索到的配置字符串(如JSONTOML、YAML格式)。viper目前支持Consul/Etcd/firestore三種Key/Value的存儲(chǔ)系統(tǒng)。下面我來(lái)演示從etcd讀取配置:

首先我們安裝crypt的工具

go get github.com/bketelsen/crypt/bin/crypt

使用crypt的命令,將app.yml的文件添加到detcd

crypt set --endpoint=http://127.0.0.1:2379 -plaintext /config/app.yml /Users/ckeen/Documents/code/gosource/go-awesome/go-samples/viper/configs/app.yml

添加viper的操作遠(yuǎn)程資源的配置

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

實(shí)現(xiàn)從遠(yuǎn)程讀取配置

func InitConfigFromRemote() (*viper.Viper,error) {
	v := viper.New()
	// 遠(yuǎn)程配置
	v.AddRemoteProvider("etcd","http://127.0.0.1:2379","config/app.yml")
	//v.SetConfigType("json")
	v.SetConfigFile("app.yml")
	v.SetConfigType("yml")
 
	if err := v.ReadRemoteConfig(); err == nil {
		log.Printf("use config file -> %s\n", v.ConfigFileUsed())
	} else {
		return nil, err
	}
	return v, nil
}
 
func main(){
  
  v, err := InitConfigFromRemote()
	if err != nil {
		log.Printf("read remote error:%+v\n")
	}
 
	log.Printf("remote read app.mode=%+v\n", v.GetString("app.mode"))
	log.Printf("remote read db.mysql.url=%+v\n", v.GetString("db.mysql.url"))
 
}

測(cè)試打印結(jié)果

2.7 監(jiān)聽(tīng)配置變化

viper提供如下兩種監(jiān)聽(tīng)配置的函數(shù),一個(gè)是本地的監(jiān)聽(tīng)和一個(gè)遠(yuǎn)程監(jiān)聽(tīng)的:

func (v *Viper) WatchConfig()
func (v *Viper) WatchRemoteConfig() error
func (v *Viper) WatchRemoteConfigOnChannel() error

我們主要看一下監(jiān)聽(tīng)本地文件變更的示例

v, err := InitConfig()
if err != nil {
log.Fatalf("viper讀取失敗, error:%+v\n",err)
}
 
// 監(jiān)聽(tīng)到文件變化后的回調(diào)
v.OnConfigChange(func(e fsnotify.Event) {
  fmt.Println("Config file changed:", e.Name)
  fmt.Println(v.Get("db.redis.passwd"))
})
 
v.WatchConfig()
 
// 阻塞進(jìn)程退出
time.Sleep(time.Duration(1000000) * time.Second)

我們使用前面的InitConfig()方法來(lái)初始化本地文件讀取配置,然后設(shè)定了監(jiān)聽(tīng)函數(shù),最后使用WatchConfig()開(kāi)啟本地文件監(jiān)聽(tīng)。

當(dāng)我們修改本地配置configs/app.yml的db.redis.passwd的值,然后保存后,我們可以看到控制臺(tái)有打印最新修改后的值,不要我們重新去獲取。

2.8 寫入配置到文件

viper提供了如下四個(gè)寫入配置文件發(fā)方法

func (v *Viper) SafeWriteConfig() error
func (v *Viper) SafeWriteConfigAs(filename string) error
func (v *Viper) WriteConfig() error
func (v *Viper) WriteConfigAs(filename string) error

使用SafeWriteConfig()和WriteConfig()時(shí),可以先設(shè)定SetConfigFile()設(shè)定配置文件的路徑。配置寫入示例:

v := New1()
v.SetConfigFile("./hello.yml")
 
log.Printf("config path:%+v\n", v.ConfigFileUsed())
 
v.SetDefault("author","CKeen")
v.SetDefault("email", "ck@gmail.com")
 
v.Set("hello", "foo")
 
v.Set("slice", []string {"slice1","slice2","slice3"})
 
v.SetDefault("test.web", "https://ckeen.cn")
 
v.WriteConfig()
 
//v.WriteConfigAs("./hello.yml")

如果使用SafeWriteConfigAs()或者WriteConfigAs()方法,則直接傳入配置文件路徑即可。

3. 源碼分析--配置讀取的順序

通過(guò)上面的示例我們知道,viper讀取配置主要通過(guò)一系列Get方法來(lái)實(shí)現(xiàn),我們從Get方法跳轉(zhuǎn)到源碼可以發(fā)現(xiàn), 主要獲取的配置值的為find方法, 方法實(shí)現(xiàn)如下:

func (v *Viper) find(lcaseKey string, flagDefault bool) interface{} {
	var (
		val    interface{}
		exists bool
		path   = strings.Split(lcaseKey, v.keyDelim)
		nested = len(path) > 1
	)
 
	// compute the path through the nested maps to the nested value
	if nested && v.isPathShadowedInDeepMap(path, castMapStringToMapInterface(v.aliases)) != "" {
		return nil
	}
 
	// if the requested key is an alias, then return the proper key
	lcaseKey = v.realKey(lcaseKey)
	path = strings.Split(lcaseKey, v.keyDelim)
	nested = len(path) > 1
 
	// Set() override first
	val = v.searchMap(v.override, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.override) != "" {
		return nil
	}
 
	// PFlag override next
	flag, exists := v.pflags[lcaseKey]
	if exists && flag.HasChanged() {
		switch flag.ValueType() {
		case "int", "int8", "int16", "int32", "int64":
			return cast.ToInt(flag.ValueString())
		case "bool":
			return cast.ToBool(flag.ValueString())
		case "stringSlice", "stringArray":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return res
		case "intSlice":
			s := strings.TrimPrefix(flag.ValueString(), "[")
			s = strings.TrimSuffix(s, "]")
			res, _ := readAsCSV(s)
			return cast.ToIntSlice(res)
		case "stringToString":
			return stringToStringConv(flag.ValueString())
		default:
			return flag.ValueString()
		}
	}
	if nested && v.isPathShadowedInFlatMap(path, v.pflags) != "" {
		return nil
	}
 
	// Env override next
	if v.automaticEnvApplied {
		// even if it hasn't been registered, if automaticEnv is used,
		// check any Get request
		if val, ok := v.getEnv(v.mergeWithEnvPrefix(lcaseKey)); ok {
			return val
		}
		if nested && v.isPathShadowedInAutoEnv(path) != "" {
			return nil
		}
	}
	envkeys, exists := v.env[lcaseKey]
	if exists {
		for _, envkey := range envkeys {
			if val, ok := v.getEnv(envkey); ok {
				return val
			}
		}
	}
	if nested && v.isPathShadowedInFlatMap(path, v.env) != "" {
		return nil
	}
 
	// Config file next
	val = v.searchIndexableWithPathPrefixes(v.config, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.config) != "" {
		return nil
	}
 
	// K/V store next
	val = v.searchMap(v.kvstore, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.kvstore) != "" {
		return nil
	}
 
	// Default next
	val = v.searchMap(v.defaults, path)
	if val != nil {
		return val
	}
	if nested && v.isPathShadowedInDeepMap(path, v.defaults) != "" {
		return nil
	}
 
	if flagDefault {
		// last chance: if no value is found and a flag does exist for the key,
		// get the flag's default value even if the flag's value has not been set.
		if flag, exists := v.pflags[lcaseKey]; exists {
			switch flag.ValueType() {
			case "int", "int8", "int16", "int32", "int64":
				return cast.ToInt(flag.ValueString())
			case "bool":
				return cast.ToBool(flag.ValueString())
			case "stringSlice", "stringArray":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return res
			case "intSlice":
				s := strings.TrimPrefix(flag.ValueString(), "[")
				s = strings.TrimSuffix(s, "]")
				res, _ := readAsCSV(s)
				return cast.ToIntSlice(res)
			case "stringToString":
				return stringToStringConv(flag.ValueString())
			default:
				return flag.ValueString()
			}
		}
		// last item, no need to check shadowing
	}
 
	return nil
}

通過(guò)源碼,我們可以知道viper讀取配置的優(yōu)先級(jí)順序:alias別名 > 調(diào)用Set設(shè)置 > flag > env > config > key/value store > default

還有一個(gè)注意點(diǎn):viper配置鍵不區(qū)分大小寫,因?yàn)関iper內(nèi)部對(duì)key統(tǒng)一轉(zhuǎn)為了小寫。

4. 參考資料

viper的包地址:viper package - github.com/spf13/viper - Go Packages

viper的github地址: GitHub - spf13/viper: Go configuration with fangs

總結(jié)

到此這篇關(guān)于Go項(xiàng)目配置管理神器之viper的介紹與使用的文章就介紹到這了,更多相關(guān)Go配置管理神器viper使用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • golang動(dòng)態(tài)創(chuàng)建類的示例代碼

    golang動(dòng)態(tài)創(chuàng)建類的示例代碼

    這篇文章主要介紹了golang動(dòng)態(tài)創(chuàng)建類的實(shí)例代碼,本文通過(guò)實(shí)例代碼給大家講解的非常詳細(xì),需要的朋友可以參考下
    2023-06-06
  • Go環(huán)境變量配置,及GOROOT、GOPATH的區(qū)別小結(jié)

    Go環(huán)境變量配置,及GOROOT、GOPATH的區(qū)別小結(jié)

    本文主要介紹了Go環(huán)境變量配置,及GOROOT、GOPATH的區(qū)別小結(jié),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2024-09-09
  • 一文帶你熟悉Go語(yǔ)言中的for循環(huán)

    一文帶你熟悉Go語(yǔ)言中的for循環(huán)

    這篇文章主要和大家分享一下Go語(yǔ)言中for循環(huán)的定義與使用,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Go語(yǔ)言有一定的幫助,需要的可以參考一下
    2022-11-11
  • golang interface判斷為空nil的實(shí)現(xiàn)代碼

    golang interface判斷為空nil的實(shí)現(xiàn)代碼

    這篇文章主要介紹了golang interface判斷為空nil的實(shí)現(xiàn)代碼,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2021-04-04
  • Go語(yǔ)言變量的聲明實(shí)現(xiàn)示例

    Go語(yǔ)言變量的聲明實(shí)現(xiàn)示例

    本文主要介紹了Go語(yǔ)言變量的聲明實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2024-03-03
  • golang goquery selector選擇器使用示例大全

    golang goquery selector選擇器使用示例大全

    這篇文章主要為大家介紹了golang goquery selector選擇器使用示例大全,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-09-09
  • GoFrame框架garray對(duì)比PHP的array優(yōu)勢(shì)

    GoFrame框架garray對(duì)比PHP的array優(yōu)勢(shì)

    這篇文章主要為大家介紹了GoFrame框架garray對(duì)比PHP的array優(yōu)勢(shì)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-06-06
  • Go項(xiàng)目實(shí)現(xiàn)優(yōu)雅關(guān)機(jī)與平滑重啟功能

    Go項(xiàng)目實(shí)現(xiàn)優(yōu)雅關(guān)機(jī)與平滑重啟功能

    無(wú)論是優(yōu)雅關(guān)機(jī)還是優(yōu)雅重啟歸根結(jié)底都是通過(guò)監(jiān)聽(tīng)特定系統(tǒng)信號(hào),然后執(zhí)行一定的邏輯處理保障當(dāng)前系統(tǒng)正在處理的請(qǐng)求被正常處理后再關(guān)閉當(dāng)前進(jìn)程,這篇文章主要介紹了Go實(shí)現(xiàn)優(yōu)雅關(guān)機(jī)與平滑重啟 ,需要的朋友可以參考下
    2022-10-10
  • Go語(yǔ)言輕松實(shí)現(xiàn)郵件發(fā)送通知功能的完全指南

    Go語(yǔ)言輕松實(shí)現(xiàn)郵件發(fā)送通知功能的完全指南

    在現(xiàn)代 Web 應(yīng)用中,郵件通知是一個(gè)不可或缺的功能,本文將深入解析一個(gè)基于 Go 語(yǔ)言 smtp 協(xié)議和 email 庫(kù)的郵件發(fā)送工具,需要的可以了解下
    2025-04-04
  • 構(gòu)建go鏡像實(shí)現(xiàn)過(guò)程全面講解

    構(gòu)建go鏡像實(shí)現(xiàn)過(guò)程全面講解

    這篇文章主要為大家介紹了構(gòu)建go鏡像實(shí)現(xiàn)過(guò)程全面講解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-12-12

最新評(píng)論

萝北县| 丰宁| 阿克陶县| 崇左市| 土默特左旗| 穆棱市| 德阳市| 双峰县| 盐边县| 梁河县| 太白县| 左贡县| 南郑县| 京山县| 青冈县| 武清区| 聂拉木县| 凌源市| 安西县| 乳源| 洪雅县| 易门县| 理塘县| 宝丰县| 罗田县| 鄯善县| 汝城县| 阿合奇县| 仁怀市| 乌拉特后旗| 康乐县| 高邮市| 渝中区| 阳泉市| 宿松县| 青川县| 米易县| 同德县| 长汀县| 郑州市| 获嘉县|