在現代 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
	}
}

內容解密:

  1. namsral/flag套件的使用:該套件允許從命令列引數、環境變數或檔案中解析組態,使得應用程式的組態更加靈活。
  2. Docker中的環境變數傳遞:透過Docker的-e選項,可以在執行容器時傳遞環境變數給應用程式。
  3. 功能選項模式:這種模式透過定義函式型別的選項(如RedisOption),使得組態的設定更加清晰和可控。
  4. 組態管理的最佳實踐:將組態儲存在環境中,避免在程式碼中硬編碼敏感資訊,使用功能選項模式為套件提供組態介面,這些都是遵循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
}

內容解密:

  1. RedisOption 是一個函式型別,用於組態 Redis 連線。
  2. RedisReadTimeout 函式傳回一個 RedisOption,用於設定 Redis 的讀取超時時間。
  3. NewRedis 函式使用傳入的 RedisOption 來建立一個新的 Redis 連線。
  4. NewRedis 函式中,我們使用迴圈來套用所有傳入的 RedisOption,以自定義 Redis 連線的行為。

使用 godotenv 函式庫載入環境變數

godotenv 是一個 Go 語言的函式庫,用於從 .env 檔案中載入環境變數。我們可以在專案的根目錄中建立一個 .env 檔案,並在其中定義環境變數。

import (
    "github.com/joho/godotenv"
)

func main() {
    godotenv.Load()
    // ...
}

內容解密:

  1. 我們首先匯入 github.com/joho/godotenv 函式庫。
  2. main 函式中,我們呼叫 godotenv.Load() 來載入 .env 檔案中的環境變數。
  3. 這樣,我們就可以在程式中使用這些環境變數。

使用 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
}

內容解密:

  1. 我們首先匯入 github.com/spf13/viper 函式庫。
  2. readConfig 函式用於載入組態檔案和設定預設值。
  3. 我們使用 viper.New() 建立一個新的 Viper 例項。
  4. 我們使用迴圈來設定預設值。
  5. 我們呼叫 SetConfigNameAddConfigPath 來指定組態檔案的名稱和路徑。
  6. 我們呼叫 AutomaticEnv 來啟用環境變數的自動載入。
  7. 最後,我們呼叫 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)
}

程式碼解析:

  1. readConfig 函式用於讀取組態檔案和設定預設值。
  2. v1.GetIntv1.GetStringv1.GetStringMapString 用於從組態中讀取特定型別的值。
  3. 如果組態檔案不存在或格式錯誤,將會傳回錯誤。

巢狀組態結構

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 物件作為引數,並傳回一個包含資料函式庫名稱的切片。