[實作筆記] Golang DI Wire 使用範例與編輯器 GoLand 設定

前情提要

學習 Golang 一陣子了,最近開始使用在正式的產品上,
老實說我還不覺得有用到它的特色。

在學習程式語言上的一個現實是,我在台灣面臨的商業規模大多瓶頸並不在語言本身,
開發者的寫法(甚至稱不上演算法)、軟硬架構基本上可以解決大部份的問題。

我的背景是 C#、JavaScript(TypeScript) 為主要開發項目,
除此之外,也有寫過 C、C++、Java、Php、VB.NET、Ruby 與 Python

Go 的優勢常見如下:

  1. 讓人容易了解的語意:作為漢語母語者我感受不強烈,我的英文不夠好可以感受到這點(同樣我對 Python 的感受也不深)
  2. 容易上手的多緒:這個有感,相比 C# 的確易懂好寫
  3. 靜態語言: 原本寫 Python 的開發人員可能比較有感,對我來說這是基本(C# 開發者角度)
  4. 高效,快:目前的專案複雜性還未可以感到其差異,或許需要壓測用實際數據比較。
    經驗上是,錯誤的架構或寫法往往才是瓶頸之所在。
  5. 編譯快:在 C# 的不好體驗,巨大單體專案,不包測試編譯就要 2~5 分鐘,不曉得 Go 在這樣情況的表現如何? 不過目前主流開發方式為雲原生,微服務,或許有生之年不會再看巨型單體專案了
  6. 原生測試:這點我覺得真是棒,我的學習之路就是由 Learn Go with tests 開始的
  7. IOP:介面導向程式設計,目前還無法體會其哲學,不過因為其語言的特性會促使人思考,這點我還在慢慢嚐試

一個新的語言我會從測試開始學,
這表示你通常會需要這些工具:測試框架、相依注入、Mocking、語意化 Assert,
本文會專注在使用相依注入的套件 WIRE

WIRE

一些 Q&A

為什麼選用 WIRE ?

google 官方推薦 Wire

還有哪些選擇 ?

有什麼不同

官方推薦,本質上更像代碼生成器(Code Generator)

Clear is better than clever ,Reflection is never clear.
— Rob Pike

示範

參考本篇文章

高耦合版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package main

import (
"bytes"
"fmt"
)

type Logger struct{}

func (logger *Logger) Log(message string) {
fmt.Println(message)
}

type HttpClient struct {
logger *Logger
}

func (client *HttpClient) Get(url string) string {
client.logger.Log("Getting " + url)

// make an HTTP request
return "my response from " + url
}

func NewHttpClient() *HttpClient {
logger := &Logger{}
return &HttpClient{logger}
}

type ConcatService struct {
logger *Logger
client *HttpClient
}

func (service *ConcatService) GetAll(urls ...string) string {
service.logger.Log("Running GetAll")

var result bytes.Buffer

for _, url := range urls {
result.WriteString(service.client.Get(url))
}

return result.String()
}

func NewConcatService() *ConcatService {
logger := &Logger{}
client := NewHttpClient()

return &ConcatService{logger, client}
}

func main() {
service := NewConcatService()

result := service.GetAll(
"http://example.com",
"https://drewolson.org",
)

fmt.Println(result)
}

在上面的程式中,可以明顯看到 ConcatService 相依於 HttpClientLogger
HttpClient 本身又與 Logger 耦合。
這是一種高耦合,在這個例子裡 Logger 還會產生兩份實體,但實際上我們只需要一份。

Golang 實際上不像 C# 有建構子(Constructor)的設計,不過常見的實踐會用大寫 New 開頭的方法作為一種類似建構子的應用,
比如說上面例子的 NewConcatServiceNewHttpClient
我們可以透過這個方法來注入我們相依的服務。

相依注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main

import (
"bytes"
"fmt"
)

type Logger struct{}

func (logger *Logger) Log(message string) {
fmt.Println(message)
}

type HttpClient struct {
logger *Logger
}

func (client *HttpClient) Get(url string) string {
client.logger.Log("Getting " + url)

// make an HTTP request
return "my response from " + url
}

func NewHttpClient(logger *Logger) *HttpClient {
return &HttpClient{logger}
}

type ConcatService struct {
logger *Logger
client *HttpClient
}

func (service *ConcatService) GetAll(urls ...string) string {
service.logger.Log("Running GetAll")

var result bytes.Buffer

for _, url := range urls {
result.WriteString(service.client.Get(url))
}

return result.String()
}

func NewConcatService(logger *Logger, client *HttpClient) *ConcatService {
return &ConcatService{logger, client}
}

func main() {
logger := &Logger{}
client := NewHttpClient(logger)
service := NewConcatService(logger, client)

result := service.GetAll(
"http://example.com",
"https://drewolson.org",
)

fmt.Println(result)
}

我們把焦點放在 main 函數中,實作實體與注入會在這裡發生,當你的程式變得複雜時,這裡也變得更複雜。
一個簡單的思路是我們可以把重構這些邏輯,
到另一個檔案 container.goCreateConcatService 方法中。

1
2
3
4
5
6
7
8
// container.go
package main

func CreateConcatService() *ConcatService {
logger := &Logger{}
client := NewHttpClient(logger)
return NewConcatService(logger, client)
}
1
2
3
4
5
6
7
8
9
10
func main() {
service := CreateConcatService()

result := service.GetAll(
"http://example.com",
"https://drewolson.org",
)

fmt.Println(result)
}

接下來我們看看怎麼透過 wire 實作

使用 Wire

安裝 wire

1
go get github.com/google/wire/cmd/wire

接下來改寫 container.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//go:build wireinject

package main

import "github.com/google/wire"

func CreateConcatService() *ConcatService {
panic(wire.Build(
wire.Struct(new(Logger), "*"),
NewHttpClient,
NewConcatService,
))
}

在專案中執行

1
wire

wire 將會產生 wire_gen.go 檔,裡面幫你實作 CreateConcatService 函數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//wire_gen.go
// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from container.go:

func CreateConcatService() *ConcatService {
logger := &Logger{}
httpClient := NewHttpClient(logger)
concatService := NewConcatService(logger, httpClient)
return concatService
}

簡單回顧一下:

  1. 我們需要針對特定的 Service 寫出一個方法的殼

    1
    2
    3
    func CreateConcatService() *ConcatService {
    ////skip
    }
  2. 在方法中透過 wire.Build 加入相依的類別,實際上我們不需要 return 實體,所以用 panic 包起來

    1
    2
    3
    4
    5
    6
    7
    func CreateConcatService() *ConcatService {
    panic(wire.Build(
    wire.Struct(new(Logger), "*"),
    NewHttpClient,
    NewConcatService,
    ))
    }
  3. 執行 wire 建立檔案

  4. 實務上需要這個 Service 時,直接呼叫 Create 方法

    1
    service := CreateConcatService()

GoLand 設定

如果你跟我一樣使用 GoLand 作為主要編輯器,
應該會收到 customer tags 的警告
customer tags

解決方法:
GoLand > Preferences > Build Tags & Vendoring > Editor Constraints > Custom Tags
設定為 wireinject 即可

參考

(fin)

[實作筆記] Dialog Button 實作 React 父子元件交互作用

前情提要

最近我設計了一個 Dialog Button ,過程十分有趣,稍微記錄一下

需求

首先 Dialog Button 的設計上是一個按鈕,被點擊後會開啟一個 Dialog,
它可以是表單、一個頁面、另一個組件或是一群組件的集合。
簡單的說 Dialog 可以是任何的 Component。

用圖說明的話,如下

Dialog Button

藍色表示父層組件,我們會把 Dialog Button 放在這裡
綠色就是 Dialog Button 本身,它會有兩種狀態,Dialog 的隱藏或顯示
紅色則是任意被放入 Dialog Button 的 Component。
也就是說我們會用 attribute 傳遞 Component 給 Dialog Button

注意黃線的部份,我們會幾種互動的行為

  1. Component 自身的行為,比如說計數器 Component 的計數功能
  2. Component 與 Dialog Button 的互動,比如說關閉 Dialog Button
  3. Component 與 Dialog Button 的 Parents Component 互動,比如說重新 Fetch 一次資料

具體的 Use Case 如下,
我在一個表格(Parents Component)中找到一筆資料,
點下編輯(Dialog Button)時會跳出一個編輯器(Component),
編輯器在我錯誤輸入時會提示1 警告訊息,
在修改完畢儲存成功時,會關閉編輯器2並且同時刷新表格資料3
參考下圖
count、close and fetch

實作

首先先作一個陽春的計數器,這個是未來要放入我們 Dialog 之中的 Component,
這裡對有 React 經驗的人應該不難理解,我們透過 useState 的方法來與計數器互動

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default function Counter() {
const handleClick = () => {
setCount(count + 1);
};
const [count, setCount] = useState(0);
return (
<>
Count :{count}
<Stack spacing={2} direction="row">
<Button onClick={handleClick} variant="contained">
Add
</Button>
</Stack>
</>
);
}

接下來我們來實作 Dialog Button,大部份的實作細節我們先跳過
我們來看看如何使用傳入的 Component 並將其與 Dialog 內部的函數銜接起來

1
2
3
<DialogContent>
{cloneElement(props.content, { closeDialog: closeDialog })}
</DialogContent>

這個神奇方法就是 cloneElement
從官方文件可知,我們可以透過 config 參數提供 prop、key 與 ref 給 clone 出來的元素,

1
React.cloneElement(element, [config], [...children]);

所以我們將 closeDialog 方法作為一個 props 傳遞給 dialog 內開啟的子組件,比如說 : Counter

完整程式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
export function DialogButton(props: DialogButtonProps) {
const [open, setOpen] = useState(false);
const openDialog = () => {
setOpen(true);
};

const closeDialog = () => {
setOpen(false);
};

return (
<>
<Button {...props} onClick={openDialog}>
{props.children}
</Button>
<Dialog
open={open}
onClose={closeDialog}
maxWidth={props.max_width}
fullWidth
>
<DialogTitle>
{props.title}
<IconButton
aria-label="close"
onClick={closeDialog}
sx={{
position: "absolute",
right: 6,
top: 6,
}}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
{cloneElement(props.content, { closeDialog: closeDialog })}
</DialogContent>
</Dialog>
</>
);
}

最後我們父層元素,也可以將自身的方法傳遞給 content Component
以達到 Component 與父層元素互動的功能,不需要再經過 Dialog Button 了

parent interact with content

參考

(fin)

[實作筆記] 最近操作 k8s 的常用指令集錦 (透過 GKE 實作)

Docker 相關

在建立映像檔的時候

--build-arg 可以提供參數給 Dockerfile

e.g

1
2
3
docker build \
--build-arg PackageServerUrl="{Url}" \
-t deposit -f ./Dockerfile .

在 container 裡 一些 Linux 常用指令

有時候使用太輕量的 image 會缺少一些工具,
我們可以直接換成有工具的 image。
如果真的需要即時的在 container 裡面操作,
可以參考以下語法
更新 apt-get

apt-get update

透過 apt-get 安裝 curl (其它套件也適用)

apt install curl

印出環境變數
方法一、 後面可以加上指定的變數名

printenv {environment value}

方法二、 效果跟 printenv 一樣

env | grep {environment value}

GKE(k8s)相關指令

準備好 manifests.yml,

GCP 授權

gcloud container clusters get-credentials {kubernetes cluster name} –region {region} –project {project name}

e.g

  • gcloud container clusters get-credentials gke-cluster-qa –region asia-east1 –project my-first-project

取得專案列表

kubectl config get-contexts

切換專案(prod/qa)

kubectl config use-context {namespace}

e.g

kubectl config use-context {prod_namespace}
kubectl config use-context {qa_namespace}

套用 manifests(記得切換環境)

kubectl apply (-f FILENAME | -k DIRECTORY)

e.g

kubectl apply -f ./manifests.yml
kubectl apply -f ./k8s/manifests.yml –namespace=prod

查詢 configMap

  • kubectl -n=qa get configmap {config_name} -o yaml

查詢 secret

  • kubectl -n prod get secret {secret_name} -o json
  • kubectl -n qa get secret {secret_name} -o json

pods port 轉入開發者環境

kubectl -n={namespace} port-forward {service} 80:80

e.g

kubectl -n=qa port-forward service/deposit 80:80

取得資訊

  • kubectl -n=qa get po
  • kubectl -n=qa get deploy
  • kubectl -n=qa get svc

進入 pods terminal 環境

kubectl -n=qa exec -it {pods_name} – /bin/bash

e.g

kubectl -n=qa exec –stdin –tty my-service-5b777f56b8-q7lf7 – /bin/sh

重啟服務

kubectl rollout restart {service_name} -n qa

e.g

kubectl rollout restart my-service-n qa

其它 CI 中用到的指令

綜合應用

設定環境變數,並且置換 yml 檔中的參數,最後 apply 到線上環境

e.g

export K8S_NAMESPACE=qa && envsubst < k8s/manifests.yml | kubectl -n=$K8S_NAMESPACE apply -f -

export K8S_NAMESPACE=qa && envsubst < k8s/configmap.qa.yml | kubectl -n=$K8S_NAMESPACE apply -f -

manifests.yml 的範例

Deployment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: apps/v1
kind: Deployment
metadata:
name: $SERVICE_NAME
namespace: $K8S_NAMESPACE
spec:
replicas: 1
selector:
matchLabels:
app: $SERVICE_NAME
template:
metadata:
labels:
app: $SERVICE_NAME
spec:
containers:
- name: $SERVICE_NAME
image: "asia-east1-docker.pkg.dev/$PROJECT_NAME/docker/$SERVICE_NAME:latest"
imagePullPolicy: Always
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: my-service-config
- secretRef:
name: my-service-secret

Service

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: $SERVICE_NAME
namespace: $K8S_NAMESPACE
spec:
type: ClusterIP
selector:
app: $SERVICE_NAME
ports:
- port: 8080
targetPort: 8080

ConfigMap

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: ConfigMap
metadata:
name: my-service-config
namespace: $K8S_NAMESPACE
data:
THIRD_PARTY_URL: "https://third_party_url/"
THIRD_PARTY_TOKEN: TBD
THIRD_PARTY_ID: TBD

Secret

!!要記得作 base64 Encode

1
2
3
4
5
6
apiVersion: v1
kind: Secret
metadata:
name: my-service-secret
namespace: $K8S_NAMESPACE
data: secret.Key:<<must encoded base64>>

manifest 實作上的 tip

更多參考官方文件與範例,設定太多了,不懂就去查
可以用 ---- 串連多份文件,但我實務上不會串 Secret
e.g

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: apps/v1
kind: Deployment
metadata:
name: $SERVICE_NAME
namespace: $K8S_NAMESPACE
spec:
replicas: 1
selector:
matchLabels:
app: $SERVICE_NAME
template:
## 中間省略
----
apiVersion: v1
kind: Service
metadata:
name: $SERVICE_NAME
## 中間省略
----
apiVersion: v1
kind: ConfigMap
metadata:
name: my-service-config
## 以下省略

參考

(fin)

[實作筆記] 12-Factor Config 使用 Golang (with Viper) II

前情提要

在前篇提到了如何透過 Viper 來取得 config 檔與環境變數來組成我們的組態檔。
這篇我們會進一步討論,透過 Go 的強型別特性,取得組態物件,並處理巢狀的結構。

假想情境

假設今天我們要作一個金流服務,
後面可能會介接 PayPal、Strip 等等不同金流的服務,
那我的組態設定上可能就會有不同的階層關係

1
2
3
4
5
6
7
8
9
10
11
12
{
"Version": "1.10.1",
"Stripe": {
"Enable": true,
"MerchantId": 123,
"ClientId": 123
},
"Paypal": {
"Enable": false,
"MerchantNo": "A003"
}
}

那麼問題來了

  1. 我應該怎麼取用這些組態 ?
    • 可以直接透過 viper.Get("name") 取得值
    • 但是每次都要用組態名稱字串取得值,其實是一個風險;
      比如打錯字,很難有工具有效防範
    • 所以我希望建立一個 type 去定義組態檔,並透過物件去存取,
      這可以有效將寫死字串限定至一次。
  2. 有了上述的方向後,另一個問題是巢狀解構的解析,
    一般來說,我認為組態檔不應該有超過三層的深度,這次就用兩作為範例說明
  3. Viper 本身支援多種組態格式,本篇僅以 envjsonyaml 作範例說明
  4. 在上一篇提到,當存在環境變數時,環境變數的優先度應高於檔案

期待的結果

我們在啟動程式的時候,一次性將組態載入事先定義好的物件之中,
如果有環境變數,優先取用。
如果沒有環境變數,會讀開發者提供的組態檔(env 檔優先,也要能支援 json 或 yaml )
如果完全沒有組態,應該發出警告

定義 Type

首先建立我們的 Type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Config struct {
Version string
Stripe StripeType
Paypal PaypalType
}

type StripeType struct {
Number int
Enable bool
}

type PaypalType struct {
MerchantNo string
Enable bool
}

接下來我們如果上一篇宣告 Of 方法用來取得組態

1
2
3
4
5
6
7
8
// define instance
var instance *Config
func Of() *Config {
once.Do(func() {
Load()
})
return instance
}

此時我們要將組態載入,先考慮開發者環境的實體檔案

1
2
3
4
5
6
7
8
9
func Load() {
vp := viper.New()
vp.AddConfigPath(".")
// todo here, change config name & type
vp.SetConfigName("config.json")
vp.SetConfigType("json")
vp.ReadInConfig()
vp.Unmarshal(&instance)
}

實作 env 使用的 config.env 檔案如下

1
2
3
4
5
Version="1.10.1.yaml"
Stripe.Enable=true
Stripe.Number=123
Paypal.Enable=false
Paypal.Merchant_No="A003"

修改 viper 相關設定

1
2
3
// just for development environment
vp.SetConfigName("config.env")
vp.SetConfigType("env")

實作 yml 使用的 config.yml 檔案如下

1
2
3
4
5
6
7
Version: "1.10.1.yml"
Stripe:
Enable: true
Number: 765
Paypal:
Enable: true
MerchantNo: "Yml003"

修改 viper 相關設定

1
2
3
// just for development environment
vp.SetConfigName("config.yml")
vp.SetConfigType("yml")

實作 json 使用的 config.json 檔案如下

1
2
3
4
5
6
7
8
9
10
11
{
"version": "1.10.1.json",
"stripe": {
"enable": true,
"number": 123
},
"paypal": {
"enable": true,
"merchantNo": "Json003"
}
}

修改 viper

1
2
3
// just for development environment
vp.SetConfigName("config.json")
vp.SetConfigType("json")

以上提供了幾種不同的 type

環境變數

我使用的環境變數如下

1
STRIPE__ENABLE=true;STRIPE__No=678;VERSION=8.88.8;PAYPAL__ENABLE=false;PAYPAL__MERCHANT_NO=ENV9999

首先,我們知道環境變數並不像 jsonyaml 檔一樣可以提供巢狀的結構,
這就與我們的需求有了衝突,好在 viper 提供了 BindEnv 的方法, 我們可以強制讓它建立出巢狀的結構,
如下:

1
2
3
4
5
vp.BindEnv("VERSION")
vp.BindEnv("Stripe.NUMBER", "STRIPE__No")
vp.BindEnv("Stripe.Enable", "STRIPE__ENABLE")
vp.BindEnv("Paypal.Enable", "PAYPAL__ENABLE")
vp.BindEnv("Paypal.MerchantNo", "PAYPAL__MERCHANT_NO")

寫在後面,在查找資料的過程中,可以發現 viper 提供了兩個功能強大的方法,
SetEnvPrefix 與 SetEnvKeyReplacer ;
SetEnvPrefix 可以自動加上前綴,SetEnvKeyReplacer 可以將分隔符置換。
可惜在我的情境尚且用不到,未來再作研究。

參考

(fin)

[實作筆記] Hexo Generate Error 處理

前情提要

我的 Blog 是透過 Hexo 這套框架建立出來的,
流程是:

  1. 撰寫文章
  2. 執行 hexo g 建立靜態檔
  3. 如果想在本地看 Blog 的效果,可以用 hexo s
  4. 執行 hexo d 部署到 Github

更多細節可以參考我以前寫得相關文章

問題

在上述的第 2 步驟,執行命令後,雖然可以產生靜態檔,
但會伴隨著以下的錯誤訊息,這就非常擾人了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FATAL {
err: [OperationalError: EPERM: operation not permitted, unlink '/Users/marklin/Repo/Marsen/Marsen.Node.Hexo/public/2022/03/18/2022/https_and_Brave_Browser'] {
cause: [Error: EPERM: operation not permitted, unlink '/Users/marklin/Repo/Marsen/Marsen.Node.Hexo/public/2022/03/18/2022/https_and_Brave_Browser'] {
errno: -1,
code: 'EPERM',
syscall: 'unlink',
path: '/Users/marklin/Repo/Marsen/Marsen.Node.Hexo/public/2022/03/18/2022/https_and_Brave_Browser'
},
isOperational: true,
errno: -1,
code: 'EPERM',
syscall: 'unlink',
path: '/Users/marklin/Repo/Marsen/Marsen.Node.Hexo/public/2022/03/18/2022/https_and_Brave_Browser'
}
} Something's wrong. Maybe you can find the solution here: %s https://hexo.io/docs/troubleshooting.html

這個錯誤訊息 OperationalError: EPERM: operation not permitted, unlink… .
是一個非常籠統的錯誤訊息,來自作業系統的底層,中間經過 hexo 與 node 的流程,
如果不深入鑽研(但是我沒有要深入),我們難以知道錯誤的細節。

好在,我發現當刪除了 public 資料夾之後,
再次執行 hexo g 就不會有錯誤訊息。

解決方法

簡單來說,我可以在每次 hexo -g 之前刪除 public 資料夾就可以了。
以下可以用一行指令替代。

1
rm -rf public | hexo -g

再進一步,我修改了 alias 指令如下

1
alias hxg="rm -rf public | hexo g"

如此一來,每次我執行 hxg 的時,就會依上述步驟先刪除再建立 public 靜態資料。
有關 alias 的設定,可以參考這篇文章

參考

(fin)

[實作筆記] 12-Factor Config 使用 Golang (with Viper) I

前情提要

最近在開發需要以微服務的形式部署在雲端之上的 web service,
為此,被要求需符合雲原生的 12 Factory
在本篇只討論第三點 Config - Store config in the environment,
簡單介紹一下這個因子;

組態應與程式分離
簡單的問自已程式是否可以立即開源,而不擔心洩露任何機敏資料??

先說明一些非微服務的組態設定實踐方式:

將組態設定以常數寫在程式當中

最糟糕的實踐,你永遠不該在一個產品中這樣作
每次改變一個環境,就要修改程式,這會使得你的服務變得脆弱

使用不同環境的組態檔,但是不納入版本控制

這是個常見的實踐,但是有著以下的缺點

  1. 開發人員將組態檔簽入版控(如果是 Git 應該可以透過 .gitignore 避免這些狀況)
  2. 配置文件很有可能分散在不同的位置,依照你所使用的語言或框架很有可能會有不同的格式(xml、yaml、json 等等…)
    這導致統一管理與維護的困難,特別是在擴展的時候。

以前在開發單體服務的時候,一個組態會達上千個設定值,
而在擴展時,開發者永遠無法理解這上千個組態應該填入什麼值
這有點超出主題了,但是微服務的架構下,單一服務不應該有上千個組態設定

有時候我們的環境不止有 ProdQAStaging
比如說開發環境、多國/語系/開發人員、壓測,
都可能導致環境組態檔案數量爆增,而難以維護。

問題 12-Factor 與開發環境

12-Factor 推薦的方式是,使用環境變數來避免上述的問題。
但在開發環境上這不是很理想的方式

對於新進的開發者:
需要自行加上環境變數,而不是下載程式後即可使用,會形成一種門檻

可以提供 script 或 bash 讓開發者執行後寫入環境變數
但很可能變成一個灰盒子(一次性執行的 script,只有需要編輯時才去會細看),
而且新增、刪除、修改組態都要修改 script,而產生額外的工作

開發者很可能同時開發多個不同服務,
這樣如果發生變數的命名衝突會難以解決。

生產環境(Prod、QA、Staging)不會有這樣的問題,
因為微服務的一個單體(Pod)應該都是單純的
加上服務名的前綴或許可以解決開發環境的問題,
但是會讓變數名變得相對冗長

解決方式

  1. 提供development.env作為開發者的組態檔
  2. 程式會優先讀取環境變數的值,才會讀取development.env的值

Golang 的實作如下:
我們使用 viper package,viper 本身包含一定的順序規則去讀取組態,如下:

  • explicit call to Set 顯示設定
  • flag 命令提示字元參數
  • env 環境變數
  • config 設定檔
  • key/value store 遠端儲存
  • default 默認值

程式如下,先讀取設定檔或環境變數的順序並沒有差,
因為 Viper 已經提供這樣功能實踐(env 環境變數優先於 config 設定檔)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var instance *Config

func Load() {
// new viper
vp := viper.New()
// just for development environment
vp.SetConfigName("development.env")
vp.SetConfigType("env")
vp.AddConfigPath(".")
vp.ReadInConfig()
// follow 12 factors principles https://www.12factor.net/config
vp.AutomaticEnv()
// bind to struct
vp.Unmarshal(&instance)
}

structdevelopment.env ,純量或是巢狀結構都可以支援,
見以下的程式 Config 內部包含另一個 struct NestConfig

1
2
3
4
5
6
7
8
9
type Config struct {
Port string `env:"PORT"`
Nest NestConfig `env:"NestConfig"`
}

type NestConfig struct {
MarsenNo int `env:"MarsenNo"`
MarsenFlag bool `env:"MarsenFlag"`
}

development.env範例如下

1
2
3
Port=9009
Nest.MarsenNo=1231456
Nest.MarsenFlag=1

Config 的使用:
這裡用了 singleton 的方法實作,有查看了一些文章可能會有問題,未來可能會再調整

1
2
3
4
5
6
7
8
var instance *Config

func Of() *Config {
if instance == nil {
Load()
}
return instance
}

完整代碼可以見此

未解之問題

提供給開發者使用的組態,仍有機敏資料進版的疑慮?
考慮使用 Consul ???
或是改成 docker image 並在其中寫入環境變數 ???

參考

(fin)

[閱讀筆記] Learn Go with tests

前情提要

在學習 Go 的過程中偶然發現的資源,
作者提供了一種有效學習新的語言(Go)的方法—測試。
我很認同這個想法,作者的文章簡單易懂,配合上測試案例,很快就能掌握 Go 語言的幾大特性,
同時你也會了測試,很酷的是,Golang 本身的測試語法就很好用,除了 Mocking 那個部份需引入外部資源外,
其它就內建其中了,也就是說你不會像 C# 需要面對選擇的障礙(MsTest、NUnit、XUnit),
同時網路上已經有簡體中文的資源(就像它們的文字一樣有些許殘缺,但對英文不夠好的人也是個福音了)

測試心法

Mocking 是萬惡之源嗎?

通常,如果你的程式需要大量 mocking 這隱含著錯誤的抽象。
而背後代表的意義是你正在作糟糕的設計。

What people see here is a weakness in TDD but it is actually a strength,
more often than not poor test code is a result of bad design or put more nicely,
well-designed code is easy to test.

測試的壞味道與方針

如果你的測試變得複雜,或是需要 Mocking 需多依賴,這一種壞味道,
可能是你的模組負擔太多的事情,試著去切分它。(注:沒切或切太塊,一個模組要運作要 mocking 數 10 個相依)
或是依賴關係變得太細緻,試著將適當的模組作分類(注:切太細,a 依賴 b、b 依賴 c……一路 mocking 到天涯海角)
或是太注重細節,應專注於行為而不是功能的實現(注:太注重細節會變成整合測試,需要完成的功能實現)

1
2
3
4
5
6
- The thing you are testing is having to do too many things (because it has too many dependencies to mock)
- Break the module apart so it does less
- Its dependencies are too fine-grained
- Think about how you can consolidate some of these dependencies into one meaningful module
- Your test is too concerned with implementation details
- Favour testing expected behaviour rather than the implementation

傳說中 KentBeck 大叔說的過的話

Make It Work Make It Right Make It Fast

在本書中 work 意謂通過測試,right 是指重構代碼使意圖明顯好懂,最後 fast 才是優化效能。
如果沒有 workright 之前是無法變 fast

章節 reflection 的重構步驟

參考章節 reflection,當完成 slice 的 test case 時候,
程式已經變得相當噁心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func walk(x interface{}, fn func(input string)) {
val := getValue(x)

if val.Kind() == reflect.Slice {
for i := 0; i < val.Len(); i++ {
walk(val.Index(i).Interface(), fn)
}
return
}

for i := 0; i < val.NumField(); i++ {
field := val.Field(i)

switch field.Kind() {
case reflect.String:
fn(field.String())
case reflect.Struct:
walk(field.Interface(), fn)
}
}
}

我來探討一下重構的思路,不然書中的重構步驟(對我來說)太快了,無法掌握變化的過程。
首先,我們有一個前提是每個步驟完成都要跑測試,並全數通過才行。
除了 Production Code 我們不會異動測試的任何程式碼。

消除 return

這一步應該不難理解,我們用 switch 語法取代 if return 的寫法
這樣讓我們的程式更有整體性,而不是被 if return 切分成兩個區塊
但是這樣又產生了新的壞味道,巢狀 switch
如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func walk(x interface{}, fn func(input string)) {
val := getValue(x)
switch val.Kind() {
case reflect.Slice:
for i := 0; i < val.Len(); i++ {
walk(val.Index(i).Interface(), fn)
}
default:
for i := 0; i < val.NumField(); i++ {

field := val.Field(i)
switch field.Kind() {
case reflect.String:
fn(field.String())
case reflect.Struct:
walk(field.Interface(), fn)
}
}
}
}

巢狀 switch

先來看一下這個巢狀 switch 的條件判斷為何?
我們可以發現兩個 switch 最終都是在對 .Kind() 作判斷,
這帶來了可能性,
我們可以把內層 switch 的處理往上提昇
下層使用遞迴呼叫 walk(... 如果內層的 case 都被提昇至上層,
那麼內層的 switch 就可以被剝離

巢狀 switch : case reflect.String

先把 case reflect.String: 往上層提昇
內層保留 case ,但改呼叫遞迴,執行測試,全部通過

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func walk(x interface{}, fn func(input string)) {
val := getValue(x)
switch val.Kind() {
case reflect.Slice:
for i := 0; i < val.Len(); i++ {
walk(val.Index(i).Interface(), fn)
}
case reflect.String:
fn(val.String())
default:
for i := 0; i < val.NumField(); i++ {

field := val.Field(i)
switch field.Kind() {
case reflect.String:
walk(field.Interface(), fn)
case reflect.Struct:
walk(field.Interface(), fn)
}
}
}
}

巢狀 switch 消除重複

在本篇我只對內層的 switch 進行重構,
下面我只展示內層的 switch。
兩個問題,

  1. case 重複(這是我們刻意製造出來的)→ 所以要消重複
  2. 消除重複後,參數 field 就變得有點多餘,我們可以用 inline 的手法消除
1
2
3
4
5
6
7
field := val.Field(i)
switch field.Kind() {
case reflect.String:
walk(field.Interface(), fn)
case reflect.Struct:
walk(field.Interface(), fn)
}

測試通過後,我們的內層 switch 就會變成這樣

1
2
3
4
switch val.Field(i).Kind() {
case reflect.String, reflect.Struct:
walk(val.Field(i).Interface(), fn)
}

巢狀 switch default case

這個時候我們看整體的程式碼,會發現一個怪異的現象,
外層的 default 值會直接視作存在多個 Field 進行遞迴拆解 for i := 0; i < val.NumField(); i++ {
內層的 switch 語法只有一個 case 同時處理 reflect.Structreflect.String 兩種條件,
以邏輯來說,外層的 default 只會處理 reflect.Struct 其它的資料型態都不處理,
reflect.String 在同層的 switch 其它條件被處理掉了
所以我們可以把 default 的區段改寫如下,執行測試,通過

1
2
3
4
5
6
7
8
case reflect.Struct:
for i := 0; i < val.NumField(); i++ {
switch val.Field(i).Kind() {
//case reflect.String, reflect.Struct://這個寫法也可以
default://這個寫法比較有交換率的等價概念
walk(val.Field(i).Interface(), fn)
}
}

這時候內層的 switch 就變得相當的多餘,可以整個拿掉。
執行測試,通過。
最後的程式重構就會與書上的一致,十分雅緻

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func walk(x interface{}, fn func(input string)) {
val := getValue(x)
switch val.Kind() {
case reflect.String:
fn(val.String())
case reflect.Slice:
for i := 0; i < val.Len(); i++ {
walk(val.Index(i).Interface(), fn)
}
case reflect.Struct:
for i := 0; i < val.NumField(); i++ {
walk(val.Field(i).Interface(), fn)
}
}
}

參考

(fin)

[實作筆記] 錯誤處理 dotnet core @ GKE logger

問題

為了追蹤 Dotnet Core 專案上的 log ,
參考這篇文章進行了設定,
沒想到在 GKE 上啟動服務時,會產生錯誤,導致服務啟動異常

1
2
3
4
system.io.ioexception:
error loading native library "/app/runtimes/linux-x64/native/libgrpc_csharp_ext.x64.so".
error loading shared library ld-linux-x86-64.so.2:
no such file or directory (needed by /app/runtimes/linux-x64/native/libgrpc_csharp_ext.x64.so)

原因

看起來是專案啟動之時,會需要底層的 libgrpc_csharp_ext.x64.so 檔案。
tl;dr
簡而言之,我使用的 docker image mcr.microsoft.com/dotnet/aspnet:6.0-alpine
裡面沒有包 Grpc.Core ,但是 Grpc.Core 壞壞要被 grpc-dotnet 汰換(tl;dr)
所以要嘛你自已在 alpine image 自已裝上 libgrpc_csharp_ext.x64.so
或是使用一個不公開的 image 版本
ex:6.0.1-cbl-mariner1.0-distroless-amd64
這些不公開的版本可以在這裡查到喔

參考

(fin)

[實作筆記] 錯誤處理 the gcp auth plugin is deprecated

前情提要

環境 Mac OS,
安裝了
Docker daemon 版本 4.8.2(79419)
gcloud
版本資訊如下

1
2
3
4
5
gcloud --version
Google Cloud SDK 389.0.0
beta 2022.06.03
bq 2.0.74
core 2022.06.03

執行以下指令時

❯ kubectl -n qa get secret deposit-secret -o json

會出現以下的警告訊息

1
2
W0608 15:50:50.291340   13396 gcp.go:120] WARNING: the gcp auth plugin is deprecated in v1.22+, unavailable in v1.25+; use gcloud instead.
To learn more, consult https://cloud.google.com/blog/products/containers-kubernetes/kubectl-auth-changes-in-gke

原因

在安裝 Docker 時,會同時安裝上 kubectl
我們可以用以下的指令作查詢,

1
2
❯ where kubectl
/usr/local/bin/kubectl

這個時候我們可以看看這篇,
簡而言之,就是在 kubectl v1.25 版本後,
對授權的處理作了一點變化,改用 plugin 處理。
所以當使用舊版本( < v1.25 版)時會出示警告

解法

兩個步驟
移除原有的 kubectl 並透過 gcloud 安裝新版本的 kubectl

1
sudo rm kubectl

這是在安裝 Docker 時產生的。
如果你想查看 link 的資訊,可以在移除前用以下指令切換到系統目錄

❯ cd /usr/local/bin

再透過 ll 檢視細節,大概的回應如下

1
2
3
4
5
❯ ll
total 412936
...略
lrwxr-xr-x 1 root wheel 55B 6 8 15:45 kubectl -> /Applications/Docker.app/Contents/
...略

gcloud 安裝 kubectl

1
gcloud components install kubectl

出現以下訊息再按 y

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Your current Google Cloud CLI version is: 389.0.0
Installing components from version: 389.0.0

┌──────────────────────────────────────────────────────────────────┐
│ These components will be installed. │
├─────────────────────┬─────────────────────┬──────────────────────┤
│ Name │ Version │ Size │
├─────────────────────┼─────────────────────┼──────────────────────┤
│ kubectl │ 1.22.9 │ 65.2 MiB │
│ kubectl │ 1.22.9 │ < 1 MiB │
└─────────────────────┴─────────────────────┴──────────────────────┘

For the latest full release notes, please visit:
https://cloud.google.com/sdk/release_notes

Do you want to continue (Y/n)? y

再次執行 kubectl 相關的指令就不會噴錯了

參考

(fin)

[實作筆記] 貢獻微軟文件 MSDN 記錄

起因

專案上我使用 .net core 並且有需要使用到 Config 相關的設定,
在看文件時不小心,查錯了文件(查到了這一篇 ContextInformation.GetSection(String) Method)。

這裡寫到 configuration section 是區分大小寫的(case-sensitive)

Remarks

When specifying a section within the configuration,
note that the name of the configuration section is case-sensitive.

而我實測時,才發現並沒有區分大小寫,才注意到我查錯了文件

起念

進一步我去微軟 MSDN 找到正確的文件查詢 ConfigurationManager.GetSection,發現並沒有註明是否區分大小寫。

所以我就想貢獻一下成就一之已力,
幫微軟加上說明,並好奇多久可以將修改的文件上線

歷程

首先只要點選右上角的鉛筆圖示,就可以連線至 Github 頁面,
很快的我完成了修改並且發出了 Pull Request
發出後很快就有自動化的機器人 dotnet-issue-labeler 與 opbld33 進行審查。

接下來有人工審核,Gewarren 很友善的給我一些建議,
大概就在我發出 PR 的幾個小時內,不過我自已的動作沒那麼快,
我過了 5 天才發現,並依照建議完成了修改(我想他們應該有一些文件修改的 Guide Line),
修改完後送出,一樣會有自動化審核,Gewarren approve 後,再有 3 個自動化的檢查,

  • snippets-build
  • OpenPublishing.Build Validation status: passed
  • license/cla All CLA requirements met.

接下來就 merged,這是 2022/05/17 ,不知道多久才會更新文件。

結尾

2022/05/23 我已經在微軟官方文件見到更新,小小成就達成 🎉

(fin)

Please enable JavaScript to view the LikeCoin. :P