[TIL][vgo] (part2) 開始實戰 vgo

目錄:

摘要:

vgo 是 Golang 將在 1.11 提出的新功能.提供著套件的管理與版本的控制.上面解釋過相關功能後,這篇文章我們將透過 vgo 實際建立一個簡單的專案.並且解釋相關操作.

資料存放地點

govender/dep/godep

原本套件管理系統,不論是 govendor, dep 或是 godep 都是透過 go 1.5 vendor experimental 裡面的管理方式, 將你的套件存放在 vendor 目錄底下. 存放的方式就是跟 GOPATH 一樣的擺放方式,只是 Golang system 會先搜尋 vendor 目錄底下的原始碼,再去搜尋 GOPATH. 也就是說全部人都是使用同一個套件下面的原始碼.

聽起來或許很美好.但是那是第一層的狀況下,也就是說如果你第二層的套件要去找相關的 source code 仍然會去你的 GOPATH 來尋找,當然你也可以全部包在最上面套件管理系統內.

vgo

vgo 存放的方式跟一般的套件管理系統不同,由於他不是使用 舊有的 golang vendor 的管理方式,而是將套件分別存放在每個人的系統裡,目錄結構為

$GOPATH/src/v/cache (壓縮過的版本)

別且透過不同的版本與版號來將原始碼 壓縮過後保存起來

如同上面保存的類似,這邊

$GOPATH/src/v (Sourecode 版本)

此外,也會在另外一個地方放上 source code 的版本(未壓縮過的版本),這個地方是存放透過 vgo 所下載的部分.

值得注意的是這邊存放的位置也都會根據版好的資訊來分開資料夾,如果沒有特定版號 tag 的話,就會使用 {package_name}@v0.0.0-{date}-{commit_id} 的方式來存放. (e. g. [email protected])

版號的控管方式

vgo 使用的版號控制是透過 git tag ,而關於他的格式就是依照目前 github 對於 release 的版號規格:

  • 必須為三碼 v0.0.0,第四碼出現會 vgo 抓不到
  • 第一個 v 必須為小寫,大寫一樣會抓不到
  • 後面不能加上其他的敘述,必須為單純的三碼 v0.0.0v1.0.0_20180702 就抓不到

可以透過 vgo list -t 套件名稱 來查詢套件的版號

開始透過抓取你的專案

你可以透過兩種方式開始使用 vgo 不論是套用在舊的專案上,或是開啟一個全新的專案.

開啟全新專案

vgo 讓你可以不需要在 gopath下面建立你的專案,但是你需要打上以下資訊.

pakcage main // import "github.com/yourname/yourproj"

...

對..你必須要打上一個 build tag // import 某個 remote path 才能讓 vgo 正確抓到資料,正確開始跑.

然後再 touch go.mod 再來開始 import 你需要的版號,就可以.

套用在舊的專案上

將 vgo 套用在已經 Production 的專案上其實也不會有危險,因為他跟 govendor, glide 跟 dep 都是可以共存的.

要套用到舊專案的方式也很簡單,方式如下:

  • 如果是 web service 或是 console app 到舊專案執行目錄

  • 執行 vgo build

  • 這樣會幫你自動建立 go.mod

不過極有可能建立出類似一下的 go.mod :

github.com/docker/spdystream v0.0.0-20170912183627-bc6354cbbc29

你會發現你許多套件的版本號碼都是有問題的,請先注意,以下事情:

  • 版號為 v0.0.0 並不是代表你拿到的 code 是最舊的,而是代表你沒透過 vgo 去拿套件,所以沒有正確版好(此問題在 go 1.11 將會修復)
  • 如果你要去使用版號來抓,請注意當著的 commit ID 對應的版本號碼.避免不小心拿到不能正常運行的版本.

要更新版號方式有以下兩種:

  • vgo get -u 來拿到最新版號
  • 修改 go.mod 指定相關套件版號,來抓取正確的版本

待續

本篇文章先介紹到這裡,由於 go 1.11 已經正式將 vgo (正式名稱為 go module support) 加入 . 我將會研究一下新版本的差異,在下一篇文章跟大家分享.

Reference

[Golang] Golang buffered/unbuffer channel and pipeline

前提

如何說明一個人 Golang 寫得夠不夠熟練,我大部分都是問 “ 可以請你解釋一下 buffered 跟 unbuffered channel 的差異?”

往往這樣的題目都會問倒一堆人.不是大家對這個語言不熟悉,而是一般人在學習 golang 的 goroutine 的時候,原本就已經很少使用 channel 來管理,更別說使用 buffered channel .

這篇文章,我會稍微提一下 buffered/unbuffered channel 的差異.並且透過最近遇到 pipeline 的問題來討論一下.

Buffered/Unbuffered Channel

Unbuffered Channel

先講 unbuffered channel ,也就是最基本大家使用的 channel

ch := make(chan bool)

go func() {
    ch <- true
}()

// keep waiting after goroutine run
<-ch

這邊是一個最簡單的 unbuffered channel 的案例,這邊需要注意的相關事情.

  • 由於 unbuffered channel 只有一個位置,所以當你已經存入之後. (e.g. ch <- true) 第二個要存入也會卡住(直到第一個 pop 出來)
  • <-ch 會造成 STW(Stop The World) ,才會驅動 goroutine 驅動.也才能導致 ch <- true 才能跑得到.不然會卡死.這也是如果你想在同一個 goroutine 跑 chan push 跟 pop 會沒有作用的原因.

Buffered Channel

Buffered Channel 顧名思義就是具有多個的 channel,參考一下:

ch := make(chan int, 3) //建立大小為 3 的 buffered channel
go func() {
    ch <- 1
    ch <- 2
    ch <- 3
}()

fmt.Println(<-ch) //1
fmt.Println(<-ch) //2
fmt.Println(<-ch) //3

這是一個很簡單的例子,既然 channel 為一個 slide .當然也可以 iterate

ch := make(chan int, 3) //建立大小為 3 的 buffered channel
go func() {
    ch <- 1
    ch <- 2
    ch <- 3
}()

for n := range ch {
    fmt.Println(n)
}

當你覺得好像很正確的時候,跑下去就會發生有問題. 參考 playground

問題發生在哪?

可以參考一下 Go By Example的這段說明. https://gobyexample.com/range-over-channels

This range iterates over each element as it’s received from queue. Because we closed the channel above, the iteration terminates after receiving the 2 elements.

必須改成以下的方式:

ch := make(chan int, 3) //建立大小為 3 的 buffered channel
go func() {
    ch <- 1
    ch <- 2
    ch <- 3
    close(ch)
}()

for n := range ch {
    fmt.Println(n)
}

參考 playground

Pipeline pattern

pipeline 可以很方便地處理多步驟地處理流程,比如說 ETL 或是 影像處理. 官方的 pipeline blog 有很詳盡的範例介紹.

在這裡先簡單的串起來一下,

func gen(nums ...int) <-chan int {
	out := make(chan int)
	go func() {
		for _, n := range nums {
			fmt.Printf("%d into first queue \n", n)
			out <- n
			fmt.Printf("%d completed into first queue \n", n)
		}
		close(out)
	}()
	return out
}

func sq(in <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		for n := range in {
			fmt.Printf("%d into second queue \n", n)
			out <- n * n
			fmt.Printf("%d completed into second queue \n", n)
			time.Sleep(1 * time.Second)
		}
		close(out)
	}()
	return out
}

func main() {
	// Set up the pipeline.
	c := gen(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
	out := sq(c)

	// Consume the output.
	for v := range out {
		fmt.Println("Result:", v)
	}
}

//1 into first queue 
//1 completed into first queue 
//2 into first queue 
//1 into second queue 
//1 completed into second queue 

這個範例你可以很清楚看到,由於使用 unbuffered channel 所以 1 要先離開第一個 channel 之後,2 才能進去.

Fan-out with Pipeline

Fan-out 是一個作法就是一次讓多個 goroutine 來跑.也是可以搭配著 pipeline 來跑 fan-out . 讓我們來看以下的範例.

func sq(in <-chan int) <-chan int {
	out := make(chan int, 1000)
	for i := 0; i <= 3; i++ {
		go func() {
			for n := range in {
				out <- n * n
				fmt.Printf("%d is out queue \n", n)
				time.Sleep(1 * time.Second)
			}
		}()
	}
	return out
}

參考: Playground

你會發現這個無法跑完,因為他會卡住而跑出

fatal error: all goroutines are asleep - deadlock!

原因就出在之前提過的忘記 close channel . 記得,如果前面一個 pipeline 沒有 close channel ,後面的 for range 就會卡住.

那要如何能夠精準地知道何時才能 close channel?

最後解法

最後改法,加上 waitgroup 的方式來等到 fan-out 的結果:

Refer: Playground

[TIL][Kubernetes] 開發一個 Kubernetes Secret 相關應用筆記

前提:

通常在 Kubernetes 裡面要將設定檔寫入的方式有兩種:

當資料不敏感(具有保密資料) 的時候,我們會使用 ConfigMap ,當你要儲存比較敏感的資料 (service account, password, 私密資料) 就要使用 Secret

簡單介紹 Kubernetes Secret:

Kubernetes secret introduction from Evan Lin

不囉唆,看之前整理好的投影片.

不分類小筆記:

Secret 設定相關:

  • Kubenetes Secret 無法在 Entrypoint 就讀取到,必須等到 POD 跑起來後執行.
  • 如果要透過 Secret 來讀取 Kubernetes service account 的相關設定,建議跑在 Command, Args 裡面設定.

Kubernetes 執行 Commands 與 Args:

  • Kubernetes yaml 的 ARGs 主要拿來跑參數用,如果想要作為 command 跑 sh 的內容,要使用 && 就不能分散在 Args 之中.

如何透過 Secret 處理敏感資料 (密碼, Key, Kubenetes 設定文件)

  • 如果要將敏感的 json data (configuration, service accoutn, password config) 寫入 secret volume 可以透過 base64 來 encode
  • 透過 base64 encode 的 Secret 資料,不需要再跑 decode .可以直接在 Pod 裡面讀取.

[TIL][makefile] make file 進階用法 (Secondary Expansion)

問題敘述

我有下列的服務要透過 make file 來編譯 Dockerfile ,由於那些服務有版本的區別,而且不同的版本都需要同時存在. 也就是說 app:v1 跟 app:v2 必需要同時能存在,並且要能夠讓 make file 能夠支援新的版本 app:v3 的產生. 舉例來說,我之後只要輸入 make build-images 就要能夠自動跑出 app1-v1, app1-v2, app2-v1 並且能夠根據這些 build target 自動 build 相關的 docker image

  • build
    • app1
      • v1
        • Dockerfile
      • v2
        • Dockerfile
    • app2
      • v1
        • Dockerfile

如何得到 build target

首先我們要能搜集所有的 build targets ,透過一系列的 build target 再來跑相關的 make process.

BUILD_DOCKERFILES := $(sort $(wildcard build/*/*/Dockerfile))

這一段是找到所有具有 Dockerfile 的檔案.. 在這裡會找出…

  • build/app1/v1/Dockerfile
  • build/app1/v2/Dockerfile
  • build/app2/v1/Dockerfile

這時候我們要做一點小處理,要去除 Dockerfile 這個不需要的檔案名稱.這個要透過上一篇提過的 % 來處理. 快速講解一下,下面的例子是說,不論何種字串只要是 xxxx/Dockerfile 都會被取代成 xxxx

BUILD_DIRS := $(patsubst %/Dockerfile,%,$(BUILD_DOCKERFILES))

這時候 BUILD_DIRS 就會轉換為:

  • build/app1/v1
  • build/app1/v2
  • build/app2/v1

再來,我們要整理成 build-app-v1 的格式,這個可以透過 subst 來替代

BUILD_APP_VER := $(subst /,-,$(BUILD_DIRS))

這樣就會得到:

  • build-app1-v1
  • build-app1-v2
  • build-app2-v1

最後,我們要其轉換成 app1-v1, app1-v2 … 一樣透過 % 來替換

BUILD_NAMES := $(patsubst build-%,%,$(BUILD_APP_VER))

最後我們要得到 build target 希望是 build-app1-v1, build-app1-v2…

BUILD_TARGETS := $(addprefix notebook-image-,$(BUILD_NAMES))

這樣就可以得到我們要的結果.

最後整理一下…

BUILD_DOCKERFILES := $(sort $(wildcard build/*/*/Dockerfile))
BUILD_DIRS := $(patsubst %/Dockerfile,%,$(BUILD_DOCKERFILES))
BUILD_APP_VER := $(subst /,-,$(BUILD_DIRS))
BUILD_NAMES := $(patsubst build-%,%,$(BUILD_APP_VER))
BUILD_TARGETS := $(addprefix notebook-image-,$(BUILD_TARGETS))

開始撰寫 build target 本體要做的事情

這時候因為我們得到 build-app1-v1, build-app1-v2build-app2-v1. 我們就要來展開要做的事情.我們要透過展開 build-app1-v1 來編譯出 docker.io/evanlin/app:v1 的 docker tag image

一個錯誤的範例

build-%: build/$(subst -,/,%)/Dockerfile $(shell find build -type f)
...

先解釋一下,在 build-%: 後面的就是條件式 prerequisites ,也就是必須要符合條件內才會繼續往下執行.在這裡條件是 透過找所有 build 子目錄所有檔案,來確認是否有符合 build/app1/v1/Dockerfile 的檔案.

起出這樣看起來很正常,我們想要透過替換字串來讓 % 裡面的內容來修改 app1-v1app1/v1 但是這時候會發現無法替換.

Secondary Expansion

經過查詢 GNU Make: Secondary Expansion 可以達成我的需求.所以修改如下:

.SECONDEXPANSION:
build-%: build/$$(subst -,/,%)/Dockerfile $(shell find build -type f)
...

這樣就能夠找到,也能夠繼續之後的處理.

Reference

[TIL][shell] Switch from fish to zsh

摘要

當初玩 Mac 從一開始很痛苦的 bash ,到了換了好像超好用的 fish (friendly interactive shell) 可以參考一下 [TIL] Note: about change your shell from bash to fish

當然,之後也有想要換到 zsh 參考這篇 [TIL] Change fish shell theme with nerd font 但是又遲遲不敢換.

不過今天總算發生了重要的問題. 公司一堆 Operation 相關的 Makefile 竟然完全死在 fish 上面. GG

只好認真來換… zsh

基本的 zsh 配備

當然要先看看這篇我認為超適合當 zsh (還有 oh-my-zsh) 的推坑文超簡單!十分鐘打造漂亮又好用的 zsh command line 環境 基本流程就依照這篇就好,補充一些:

  • iterm2 換字型到 Sauce Code Pro Nerd Font Complete 如果你字體大小是在 12 很容易卡死(hang) . 就得要清除所有 iterm2 的設定 · 建議要先切換字體大小到 14 再換字型.
defaults delete com.googlecode.iterm2
  • iTerm2 本身對於各個 Shell 都有 integration ,可以讓你使用起來更方便.
curl -L https://iterm2.com/shell_integration/zsh \
  -o ~/.iterm2_shell_integration.zsh

source ~/.iterm2_shell_integration.zsh
  • POWERLEVEL9K 雖然很棒,但是不是太多功能需要.這裡列出我使用的功能
export TERM="xterm-256color"


POWERLEVEL9K_MODE='nerdfont-complete'
ZSH_THEME="powerlevel9k/powerlevel9k"
# command line 左邊想顯示的內容
POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(dir vcs) 
# command line 右邊想顯示的內容
POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(kubecontext) 

POWERLEVEL9K_SHORTEN_DIR_LENGTH=1
POWERLEVEL9K_SHORTEN_DELIMITER=""
POWERLEVEL9K_SHORTEN_STRATEGY="truncate_from_right"

進階設定

這裡介紹幾個好用 plugin ,可以幫助大家快速(無痛?)切換到 zsh

plugins=(
#git 可以有一些快捷鍵 ggl ...
  git
# 自動補全   
  zsh-completions
  gitfast
# 解壓縮用
  extract
# z: 可以快速並且支援模糊切換目錄 ex: z docume <tab>
  z
# 讓你喚回 fish 漂亮的顏色
  zsh-syntax-highlighting
# 就像 fish 一樣,打到一半自動建議你
  zsh-autosuggestions
)

相關習慣變更

  • Quick Search History:
    • 先按下 “Ctrl + R” 再打你需要的…
  • Auto-Completed:
    • 先打出你需要的 ex: gcloud 然後再按下 (up) 鍵
  • 相關路徑與設定
    • GOBIN, GOPATH
    • Heroku path
    • Gcloud path (reinstall again)
    • cocoa pods PATH
    • LIBRARY_PATH

其他就還好,大概就是之前被 fish 強制修改的習慣.

  • ; —> &&
  • $(VAR) —> (VAR)
  • env ABC=edc —> ABC=edc

VSCode integrated terminal 也要改

其實剛換過去,還沒有感覺到有太多的不同.不過 VSCode 一打開就發現有個嚴重的問題, ` vscode 還在使用 fish shell`

趕緊打開 user.setting (cmd+,) ,然後加入以下的修改

    "terminal.integrated.shell.osx": "zsh",
    "terminal.integrated.cursorBlinking": true,
    "terminal.integrated.fontSize": 12,

Reference

[TIL][makefile] 一些常用的 Makefile 指令整理

摘要

花了一點時間玩了一下 makefile ,有一些字串對應方式與使用方式在不使用 sed 的前提下其實也很方便. 順手整理一下一些常用的案例.

並且也列出一些使用 makefile 在編譯 Dockerfile 的時候經常會用到的範例.

基礎常見語法:

wildcard

透過 wildcard 可以擴展找出所有匹配的項目.

找出所有具有 Dockerfile 的目錄(並且依序排列)

DOCKERFILE_PATHS := $(sort $(wildcard */Dockerfile))

patsubst 替換字串

常用再換掉路徑,去除路徑.以下紀錄幾個常用的.

取得 Parent Path (ex: a/b/c/d –> a/b/c)

PARENT_PATHS := $(patsubst %/,%,$(dir $(CURRENT_PATHS)))

nodir 去除所有目錄

這個可以幫你去除所有目錄,剩下檔名. ex: a/b/c/d –> d

取得上一層目錄列表

PARENT_PATH_LIST := $(notdir $(dir $(CURRENT_PATHS)))

這裡也要解釋一下, dir 會取得目錄而已.跟 nodir 剛好相反.

另外, basenamedir 也不同.

$(basename a/b/c/d.a) —> d

進階用法 *, %$*$<

這邊講解起來會有點難懂,就用一個案例來講解.

  • $* 會找出所有結果
  • % 會將前方變數帶給之後用
  • $< 列出前面的結果

以下用個例子

範例(1) : 找出 Dockerfile 並且編譯

build-image-%: %/Dockerfile $(shell find $* -type f)
	time docker build  \
		--tag docker.io/$(YOUR_PROJECT)/%:master \
		--file $< \
		$(dir $<)

這個會去編譯某個你輸入底下的 Dockerfile .

舉例而言: 你有個目錄 tensorflow/Dockerfile 你就要輸入 make build-image-tensorflow ,他就會自動找出這個 Dockerfile 並且編譯名稱為 $(YOUR_PROJECT)/tensorflow:master 以下透過這個案例解釋每個用法:

  • % 會將前面輸入的,帶到後面.在這裡也就是 tensorflow 帶到第二個與第三個出現%的地方
  • $< 會列出 shell find $* -type f 結果,也就是列出所有檔案,並且符合 %/Dockerfile 也就是如果你有其他檔案舉例為 tensorflow/README.md 也不會列出.

似乎這樣好像很麻煩? 其實透過前面介紹的基礎常見語法,可以更方便編譯.

範例(2) : 找出所有符合目錄直接帶到以上編譯

#find all docker files
DOCKERFILES := $(sort $(wildcard */Dockerfile))

#extract dir name
DIRS := $(patsubst %/Dockerfile,%,$(basename $(DOCKERFILES)))

#remain only dir name list
NAMES := $(notdir $(DIRS))

#add prefix combine to function strings
TARGETS := $(addprefix build-image-,$(NAMES))

#get all functions and run it.
all: $(TARGETS)

舉例而言,你有以下目錄結構

  • a/README.md
  • b/Dockerfile
  • b/README.md
  • c/Dockerfile
  • d/test.c

你現在只要輸入 make all 就會自動把 b/Dockerfile 與 c/Dockerfile 找出來,並且組成字串 build-image-b build-image-c 然後呼叫到範例(1)來呼叫兩個 docker build

Reference