在現代 Go 應用程式開發中,將組態外部化,尤其是利用環境變數,已成為不可或缺的實務。如此一來,不僅能提升應用程式的可移植性與安全性,也更符合 12 Factor Application 的設計理念。本文將探討如何有效地運用環境變數管理 Go 應用程式的組態,同時介紹 Viper 函式庫的應用,以及在 Docker 環境中的實踐技巧。此外,文章也涵蓋了功能選項模式在建立資料函式庫連線時的應用,以及 godotenv 函式庫的使用。這些技巧能協助開發者更有效率地管理應用程式組態,並提升程式碼的可維護性。
組態管理 - 將組態儲存在環境中
遵循12因子應用程式(12 Factor Application)原則中的一項重要指導方針,是關於如何宣告和傳遞應用程式的組態。對於使用資料函式庫例項的應用程式,資料函式庫的位置和憑證應該來自應用程式執行的環境。這意味著組態應透過環境變數、命令列引數或甚至是作為命令列引數傳遞的組態檔案來提供。
為何需要這樣做
將組態儲存在環境中有助於避免一些不良實踐,例如在程式碼中定義常數,這使得在不重建整個應用的情況下無法更改環境組態。同時,也避免了將敏感資訊(如資料函式庫憑證或AWS憑證)提交到原始碼控制系統中。
風險
如果不當處理組態,可能會導致敏感資訊洩露。例如,曾經有開發者不小心將AWS憑證洩露到GitHub上,結果收到了數千美元的Amazon賬單。
在Go中處理組態
Go語言的標準函式庫中提供了flag套件,用於解析命令列引數。還有第三方擴充套件,如namsral/flag,它不僅可以解析命令列引數,還可以解析檔案和環境變數。
使用namsral/flag的例子
package main
import (
"fmt"
"github.com/namsral/flag"
)
func main() {
var age int
flag.IntVar(&age, "age", 0, "age of gopher")
flag.Parse()
fmt.Print("age:", age)
}
透過Docker傳遞環境變數
#!/bin/bash
docker run --rm -e "AGE=35" -v $(pwd):/go/src/app -w /go/src/app golang go run flags.go
將組態傳遞給套件
建議在main套件中定義標誌(flags),然後根據需要將它們注入到其他套件中。這樣可以減少不同套件之間因標誌名稱衝突而產生的問題。
最佳實踐
- 在
func main()區塊中定義標誌,這樣可以控制組態如何傳遞給套件和其他函式。 - 使用功能選項(Functional Options)模式為套件提供組態介面。
使用功能選項的Redis例子
package services
import (
"time"
"github.com/garyburd/redigo/redis"
)
// Redis 連線結構
type Redis struct {
conn redis.Conn
address string
connectTimeout, readTimeout, writeTimeout time.Duration
}
type RedisOption func(*Redis)
// RedisAddress 指定Redis伺服器的主機名(包括埠)
func RedisAddress(address string) RedisOption {
return func(r *Redis) {
r.address = address
}
}
// RedisConnectTimeout 設定Redis客戶端的連線超時時間
func RedisConnectTimeout(timeout time.Duration) RedisOption {
return func(r *Redis) {
r.connectTimeout = timeout
}
}
內容解密:
namsral/flag套件的使用:該套件允許從命令列引數、環境變數或檔案中解析組態,使得應用程式的組態更加靈活。- Docker中的環境變數傳遞:透過Docker的
-e選項,可以在執行容器時傳遞環境變數給應用程式。 - 功能選項模式:這種模式透過定義函式型別的選項(如
RedisOption),使得組態的設定更加清晰和可控。 - 組態管理的最佳實踐:將組態儲存在環境中,避免在程式碼中硬編碼敏感資訊,使用功能選項模式為套件提供組態介面,這些都是遵循12因子應用程式原則的重要實踐。
組態管理:將組態儲存在環境變數中
在開發應用程式時,組態管理是一個重要的環節。良好的組態管理能夠使應用程式更具彈性和可移植性。本文將介紹如何在 Go 語言中將組態儲存在環境變數中,並使用相關函式庫來簡化組態管理。
使用 Redis 函式庫的選項模式
在介紹組態管理之前,我們先來看看如何使用選項模式來建立一個 Redis 連線。選項模式是一種建立物件的模式,它允許我們在建立物件時傳入多個選項,以自定義物件的行為。
// RedisOption - a function that configures a Redis connection
type RedisOption func(do *Redis)
// RedisReadTimeout - set a read timeout for a Redis operation
func RedisReadTimeout(timeout time.Duration) RedisOption {
return func(do *Redis) {
do.readTimeout = timeout
}
}
// NewRedis creates a new redis connection
func NewRedis(options ...RedisOption) *Redis {
redis := &Redis{
address: "redis:6379",
connectTimeout: time.Second,
readTimeout: time.Second,
writeTimeout: time.Second,
}
for _, option := range options {
option(redis)
}
return redis
}
內容解密:
RedisOption是一個函式型別,用於組態 Redis 連線。RedisReadTimeout函式傳回一個RedisOption,用於設定 Redis 的讀取超時時間。NewRedis函式使用傳入的RedisOption來建立一個新的 Redis 連線。- 在
NewRedis函式中,我們使用迴圈來套用所有傳入的RedisOption,以自定義 Redis 連線的行為。
使用 godotenv 函式庫載入環境變數
godotenv 是一個 Go 語言的函式庫,用於從 .env 檔案中載入環境變數。我們可以在專案的根目錄中建立一個 .env 檔案,並在其中定義環境變數。
import (
"github.com/joho/godotenv"
)
func main() {
godotenv.Load()
// ...
}
內容解密:
- 我們首先匯入
github.com/joho/godotenv函式庫。 - 在
main函式中,我們呼叫godotenv.Load()來載入.env檔案中的環境變數。 - 這樣,我們就可以在程式中使用這些環境變數。
使用 Viper 函式庫進行組態管理
Viper 是一個功能強大的組態管理函式庫,支援多種組態格式和來源。我們可以使用 Viper 來載入組態檔案、環境變數等。
import (
"github.com/spf13/viper"
)
func readConfig(filename string, defaults map[string]interface{}) (*viper.Viper, error) {
v := viper.New()
for key, value := range defaults {
v.SetDefault(key, value)
}
v.SetConfigName(filename)
v.AddConfigPath(".")
v.AutomaticEnv()
err := v.ReadInConfig()
return v, err
}
內容解密:
- 我們首先匯入
github.com/spf13/viper函式庫。 readConfig函式用於載入組態檔案和設定預設值。- 我們使用
viper.New()建立一個新的 Viper 例項。 - 我們使用迴圈來設定預設值。
- 我們呼叫
SetConfigName和AddConfigPath來指定組態檔案的名稱和路徑。 - 我們呼叫
AutomaticEnv來啟用環境變數的自動載入。 - 最後,我們呼叫
ReadInConfig來載入組態檔案。
組態管理 - 將組態儲存在環境中
在開發應用程式時,組態管理是一項重要的任務。Viper 是一個流行的 Go 語言函式庫,用於處理組態檔案和環境變數。在本文中,我們將探討如何使用 Viper 將組態儲存在環境中。
使用 Viper 讀取組態
以下是一個簡單的範例,展示如何使用 Viper 讀取組態:
func main() {
v1, err := readConfig(".env", map[string]interface{}{
"port": 9090,
"hostname": "localhost",
"auth": map[string]string{
"username": "titpetric",
"password": "12fa",
},
})
if err != nil {
panic(fmt.Errorf("Error when reading config: %v\n", err))
}
port := v1.GetInt("port")
hostname := v1.GetString("hostname")
auth := v1.GetStringMapString("auth")
fmt.Printf("Reading config for port = %d\n", port)
fmt.Printf("Reading config for hostname = %s\n", hostname)
fmt.Printf("Reading config for auth = %#v\n", auth)
}
程式碼解析:
readConfig函式用於讀取組態檔案和設定預設值。v1.GetInt、v1.GetString和v1.GetStringMapString用於從組態中讀取特定型別的值。- 如果組態檔案不存在或格式錯誤,將會傳回錯誤。
巢狀組態結構
Viper 支援巢狀組態結構,可以使用 .(點)分隔巢狀鍵。例如:
v1.GetString("auth.username")
這將讀取 auth 物件下的 username 值。
環境變數
Viper 也支援從環境變數中讀取組態。可以使用 SetEnvKeyReplacer 方法來設定環境變數的鍵替換規則:
v1.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
這樣,可以使用 AUTH_USERNAME 環境變數來覆寫 auth.username 的值。
後端服務 - 將後端服務視為附加資源
在開發應用程式時,後端服務(如資料函式庫、NoSQL 資料儲存和代理)是非常重要的組成部分。將後端服務視為附加資源,可以提高應用程式的靈活性和可擴充套件性。
資料卷容器
資料卷容器是一種特殊的容器,用於持久化儲存資料。可以使用 -v 引數來掛載資料卷:
docker run --rm -v bash-storage:/app bash bash -c 'echo "hello world" > /app/hello'
資料卷管理
- 使用
docker volume ls命令列出所有資料卷。 - 使用
docker volume create命令建立新的資料卷。 - 使用
docker volume inspect命令檢查資料卷的詳細資訊。
資料卷掛載
資料卷掛載是一種將主機目錄掛載到容器中的方法。這樣,可以在容器停止時保留資料:
docker run --rm -v /host/path:/app bash bash -c 'echo "hello world" > /app/hello'
資料卷掛載的優點
- 資料可以持久化儲存。
- 可以在容器執行時更新資料。
滾動更新
在使用 Docker Swarm 時,可以使用滾動更新來更新應用程式。這樣,可以在不中斷服務的情況下更新應用程式。
支援服務 - 將支援服務視為附加資源
在應用程式開發中,支援服務(如資料函式庫、快取系統等)扮演著至關重要的角色。這些服務對於應用程式的正常運作是不可或缺的。在本章節中,我們將探討如何將支援服務視為附加資源,並介紹一些常見的支援服務及其使用方法。
常見的支援服務範例
許多應用程式都需要使用一些常見的支援服務,例如 MySQL、Redis 等。這些服務都有各自的特點和使用場景。在生產環境中執行這些服務需要注意一些事項,例如系統引數的調整、網路設定的最佳化等。
MySQL
MySQL 是一種流行的關聯式資料函式倉管理系統。在本章節中,我們將介紹如何使用 Percona 版本的 MySQL。Percona 版本的 MySQL 提供了更好的效能和更多的功能,例如 XtraBackup 等。
啟動 MySQL 容器
#!/bin/bash
# services/mysql/bin/run -> MYPATH= services/mysql, NAME= mysql
MYPATH=$(dirname $(dirname $(readlink -f $0)))
NAME=$(basename $MYPATH)
if [ ! -d "$MYPATH/data" ]; then
mkdir -p $MYPATH/data
fi
DOCKER_IMAGE="percona:latest"
# generate root password to file for convenience
PASSWORD="default"
perl -pi -e "s/^password.+$/password=$PASSWORD/g" ../conf/.my.cnf
if [ ! -d "$MYPATH/data" ]; then
mkdir -p $MYPATH/data
fi
# stop and remove existing container and start a new one
docker stop $NAME
docker rm $NAME
docker run --restart=always \
-h $NAME \
--name $NAME \
--net=party \
-v /src/$NAME/backups:/backups \
-v /src/$NAME/conf/conf.d:/etc/mysql/conf.d \
-v /src/$NAME/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=$PASSWORD \
-d $DOCKER_IMAGE
docker cp /src/$NAME/conf/.my.cnf $NAME:/root/
連線和使用 MySQL
連線和使用 MySQL 例項非常簡單。根據服務的大小和碎片化的程度,您可能需要同時使用不同的連線。我們的首選客戶端是 jmoiron/sqlx,您應該將其供應商化和匯入。
package service
import (
"strings"
// import database driver for mysql
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
例如,一個多租戶 SaaS 應用程式可能至少使用兩個資料函式庫:一個為客戶建立的資料函式庫,和一個中央管理資料函式庫,其中可能包含只讀的訂閱詳情,如帳單和發票等。在這種情況下,應用程式應該使用兩個不同的資料函式庫連線。
建立資料函式庫連線工廠
// Database connection factory object
type Database struct {
// hold database config
dsn *string
}
// Get creates and returns a new database connection
func (r *Database) Get() (*sqlx.DB, error) {
dsn := *r.dsn
if !strings.Contains(dsn, "?") {
dsn = dsn + "?"
}
// set some default values for dsn config
defaults := map[string]string{
"collation": "utf8_general_ci",
"parseTime": "true",
"loc": "Local",
}
// append default values
for key, value := range defaults {
if !strings.Contains(dsn, key+"=") {
dsn = dsn + "&" + key + "=" + value
}
}
dsn = strings.Replace(dsn, "?&", "?", 1)
return sqlx.Open("mysql", dsn)
}
建立資料函式庫工廠和組態
flags := flag.NewFlagSet("default", flag.ContinueOnError)
database := &service.Database{}
database.Flags("dsn", "DSN for database connection", flags)
flags.Parse(os.Args[1:])
使用資料函式庫連線
func listDatabases(db *sqlx.DB) ([]databaseName, error) {
result := []databaseName{}
err := db.Select(&result, "show databases")
return result, err
}
程式碼解析
MySQL 連線設定
在上述範例中,我們使用了 jmoiron/sqlx 函式庫來連線 MySQL 資料函式庫。首先,我們定義了一個 Database 結構體來儲存資料函式庫連線的組態資訊。然後,我們實作了 Get 方法來建立和傳回一個新的資料函式庫連線。
在 Get 方法中,我們設定了一些預設的 DSN 組態引數,例如字元集和時間解析等。這些引數可以根據實際需求進行調整。
資料函式庫連線工廠
我們定義了一個 Database 結構體來作為資料函式庫連線工廠。這個工廠可以用來建立多個資料函式庫連線,每個連線都有自己的組態資訊。
使用範例
在上述範例中,我們展示瞭如何使用 listDatabases 函式來列出資料函式庫中的所有資料函式庫名稱。這個函式接受一個 *sqlx.DB 物件作為引數,並傳回一個包含資料函式庫名稱的切片。