Quantcast
Channel: 小惡魔 – 電腦技術 – 工作筆記 – AppleBOY
Viewing all 325 articles
Browse latest View live

用 Go 語言讀取專案內 .env 環境變數

$
0
0

golang logo

在現代開發專案時,一定會用到環境變數,像是讀取 AWS Secret Key 等等,在部署上面也會透過設定變數讓專案依據不同環境讀取不同環境變數,而 Go 語言內如何實現讀取環境變數,又可以讓開發者透過 .env 檔案動態改變環境變數,本影片用簡單的套件來實現。這個在其他語言的 Framework 都有實現,像是 Laravel 的 .env 設定。透過本篇例子也教大家如何 import Go 語言的 Package。

教學影片

更多實戰影片可以參考我的 Udemy 教學系列

import package

直接用 Google 搜尋,一定可以找到第一筆資料,透過『joho/godotenv』套件就可以快速完成此功能,底下來看看例子:

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/joho/godotenv"
)

func main() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }

    s3Bucket := os.Getenv("S3_BUCKET")
    secretKey := os.Getenv("SECRET_KEY")

    fmt.Println(s3Bucket)
    fmt.Println(secretKey)
}

從上面例子可以看到我們使用了『joho/godotenv』套件,並且在主程式開頭就判斷專案底下是否有 .env 檔案,如果沒有的話就直接抱錯『Error loading .env file』,這樣寫倒是沒有什麼問題,但是當工程師在 git clone 專案時預設沒有 .env 檔案,如果執行主程式,肯定就會出錯,這時候我們該如何修正略過此錯誤訊息呢?可以改成如下

func main() {
    godotenv.Load()

    s3Bucket := os.Getenv("S3_BUCKET")
    secretKey := os.Getenv("SECRET_KEY")

    fmt.Println(s3Bucket)
    fmt.Println(secretKey)
}

改完後你會發現 godotenv.Load() 其實很惱人,不知道能不能直接省略,答案是可以的,這邊我們透過 _ 來 import 第三方套件

package main

import (
    "fmt"
    "os"

    _ "github.com/joho/godotenv/autoload"
)

func main() {
    s3Bucket := os.Getenv("S3_BUCKET")
    secretKey := os.Getenv("SECRET_KEY")

    fmt.Println(s3Bucket)
    fmt.Println(secretKey)
}

大家都知道,如果專案內 import 了,但是沒用到,Go 在編譯時就會報錯,這時候可以透過 _ 方式來略過此錯誤訊息,但是這邊的 _ 又有不同的含義,就是可以讀取該套件底下的 func init(),我們看一下這 autolod 裡面做了什麼事情?

package autoload

/*
    You can just read the .env file on import just by doing
        import _ "github.com/joho/godotenv/autoload"
    And bob's your mother's brother
*/

import "github.com/joho/godotenv"

func init() {
    godotenv.Load()
}

裡面只有定義了 func init,而透過 _ import 就能讀取到 init 函式,此 init 會再進入 main 主程式前直接先讀取了。所以專案根本就是 import 了一行代碼,就可以達到讀取環境變數的功能,接著在專案底下建立 .env 檔案

S3_BUCKET=test
SECRET_KEY=1234

有了這功能就可以讓工程師自行調整環境變數,而不用再額外透過 command 方式設定,我覺得相當方便,有在寫 Go 的朋友們,務必 import 此套件


在 appveyor 內指定 Go 語言編譯版本

$
0
0

golang logo

相信比較少人知道 appveyor 這服務,我會接觸到此服務最大的原因是,要提供 Windows 的 Docker Image,並且上傳到 DockrHub,此服務提供了 Windows 環境,讓開發者可以透過此服務編譯 Windows 的 Binary 檔案,並且在 Windows 上執行測試,這對於我在寫 Go 語言開源專案非常有幫助,畢竟平常開發真的沒有 Windows 相關的環境可以使用。但是 appveyor 在更新第三方套件非常的慢,這時候我們想要用 GO 的 1.12 版本就需要自行來安裝,安裝方式其實也不難,請參考底下設定。

appveyor windows

安裝指定 Go 語言版本

安裝特定版本的 Go 語言,只要自行下載 Windows msi 執行檔,接著安裝就可以了:

environment:
  GOPATH: c:\gopath
  GO111MODULE: on
  GOVERSION: 1.12.4

install:
  # Install the specific Go version.
  - rmdir c:\go /s /q
  - appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-amd64.msi
  - msiexec /i go%GOVERSION%.windows-amd64.msi /q
  - go version
  - go env
  - ps: |
      docker version
      go version
  - ps: |
      $env:Path = "c:\gopath\bin;$env:Path"

會碰到這個問題最主要是 Go module 在 1.11.1 ~ 1.11.3 有個 bug 就是,只要在 go.mod 內寫了 go 1.12 這樣此套件就會判斷目前的 Go 版本,如果小於 go1.12 就無法編譯套件,這問題在 go1.11.4 已經被解決,但是 appveyor 還停留在 go 1.11.2 版本,所以造成需要自行升級 Go 版本。有需要在 Windows 測試 GO 語言的,現在透過此方式可以編譯不同版本的環境。

[Go 語言教學影片] 在 struct 內的 pointers 跟 values 差異

$
0
0

golang logo

Struct MethodGo 語言開發上是一個很重大的功能,而新手在接觸這塊時,通常會搞混為什麼會在 function 內的 struct name 前面多一個 * pointer 符號,而有時候又沒有看到呢?以及如何用 struct method 實現 Chain 的實作,本影片會實際用寄信當作範例講解什麼時候該用 pointer 什麼時候該用用 Value。也可以參考我之前的一篇文章『Go 語言內 struct methods 該使用 pointer 或 value 傳值?

教學影片

範例

要區別 pointer 跟 value 可以透過下面的例子快速了解:

package main

import "fmt"

type car struct {
    name  string
    color string
}

func (c *car) SetName01(s string) {
    fmt.Printf("SetName01: car address: %p\n", c)
    c.name = s
}

func (c car) SetName02(s string) {
    fmt.Printf("SetName02: car address: %p\n", &c)
    c.name = s
}

func main() {
    toyota := &car{
        name:  "toyota",
        color: "white",
    }

    fmt.Printf("car address: %p\n", toyota)

    fmt.Println(toyota.name)
    toyota.SetName01("foo")
    fmt.Println(toyota.name)
    toyota.SetName02("bar")
    fmt.Println(toyota.name)
    toyota.SetName02("test")
    fmt.Println(toyota.name)
}

上面範例可以看到如果是透過 SetName02 來設定最後是拿不到設定值,這就代表使用 SetName02 時候,是會將整個 struct 複製一份。假設 struct 內有很多成員,這樣付出的代價就相對提高。

15 分鐘學習 Go 語言如何處理多個 Channel 通道

$
0
0

golang logo

大家在初學 Go 語言時,肯定很少用到 Go Channel,也不太確定使用的時機點,其實在官方 Blog 有提供一篇不錯的文章『Go Concurrency Patterns: Pipelines and cancellation』,相信大家剛跨入學習新語言時,不會馬上看 Go Channel,底下我來用一個簡單的例子來說明如何使用 Go Channel,使用情境非常簡單,就是假設今天要同時處理 20 個背景工作,一定想到要使用 Goroutines,但是又想要收到這 20 個 JOB 處理的結果,並顯示在畫面上,如果其中一個 Job 失敗,就跳出 main 函式,當然又會希望這 20 個 JOB 預期在一分鐘內執行結束,如果超過一分鐘,也是一樣跳出 main 函式。針對這個問題,我們可以整理需要三個 Channel + 一個 Timeout 機制。

  • 使用 outChan 顯示各個 JOB 完成狀況
  • 使用 errChan 顯示 JOB 發生錯誤並且跳出 main 主程式
  • 使用 finishChan 通知全部 JOB 已經完成
  • 設定 Timeout 機制 (1 秒之內要完成所有 job)

在看此文章之前,也許可以先理解什麼是『buffer vs unbuffer channel』。

教學影片

更多實戰影片可以參考我的 Udemy 教學系列

實戰範例

針對上述的問題,先透過 Sync 套件的 WaitGroup 來確保 20 個 JOB 處理完成後才結束 main 函式。

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func main() {
    wg := sync.WaitGroup{}
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func(val int, wg *sync.WaitGroup) {
            time.Sleep(time.Duration(rand.Int31n(1000)) * time.Millisecond)
            fmt.Println("finished job id:", val)
            wg.Done()
        }(i, &wg)
    }

    wg.Wait()
}

大家可以先拿上面的範例來練習看看如何達到需求,而不是在 go func 內直接印出結果。

處理多個 Channel 通道

首先在 main 宣告三個 Channel 通道

    outChan := make(chan int)
    errChan := make(chan error)
    finishChan := make(chan struct{})

接著要在最後直接讀取這三個 Channel 值,可以透過 Select,由於 outChan 會傳入 20 個值,所以需要搭配 for 迴圈方式來讀取多個值

Loop:
    for {
        select {
        case val := <-outChan:
            fmt.Println("finished:", val)
        case err := <-errChan:
            fmt.Println("error:", err)
            break Loop
        case <-finishChan:
            break Loop
        }
    }

這邊我們看到需要加上 Loop 自定義 Tag,來達到 break for 迴圈,而不是 break select 函式。但是有沒有發現程式碼會一直卡在 wg.Wait(),不會進入到 for 迴圈內,這時候就必須將 wg.Wait() 丟到背景。

    go func() {
        wg.Wait()
        fmt.Println("finish all job")
        close(finishChan)
    }()

也就是當 20 個 job 都完成後,會觸發 close(finishChan),就可以在 for 迴圈內結束整個 main 函式。最後需要設定 timout 機制,請把 select 多補上一個 time.After()

Loop:
    for {
        select {
        case val := <-outChan:
            fmt.Println("finished:", val)
        case err := <-errChan:
            fmt.Println("error:", err)
            break Loop
        case <-finishChan:
            break Loop
        case <-time.After(100000 * time.Millisecond):
            break Loop
        }
    }

來看看 go func 內怎麼將值丟到 Channel

    for i := 0; i < 20; i++ {
        go func(outChan chan<- int, errChan chan<- error, val int, wg *sync.WaitGroup) {
            defer wg.Done()
            time.Sleep(time.Duration(rand.Int31n(1000)) * time.Millisecond)
            fmt.Println("finished job id:", val)
            outChan <- val
            if val == 11 {
                errChan <- errors.New("error in 60")
            }

        }(outChan, errChan, i, &wg)
    }

宣告 chan<- int 代表在 go func 只能將訊息丟到通道內,而不能讀取通道。

心得

希望透過上述簡單的例子,讓大家初學 Go 的時候有個基礎的理解。用法其實不難,但是請參考專案內容特性來決定如何使用 Channel,最後附上完整的程式碼:

package main

import (
    "errors"
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func main() {
    outChan := make(chan int)
    errChan := make(chan error)
    finishChan := make(chan struct{})
    wg := sync.WaitGroup{}
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func(outChan chan<- int, errChan chan<- error, val int, wg *sync.WaitGroup) {
            defer wg.Done()
            time.Sleep(time.Duration(rand.Int31n(1000)) * time.Millisecond)
            fmt.Println("finished job id:", val)
            outChan <- val
            if val == 60 {
                errChan <- errors.New("error in 60")
            }

        }(outChan, errChan, i, &wg)
    }

    go func() {
        wg.Wait()
        fmt.Println("finish all job")
        close(finishChan)
    }()

Loop:
    for {
        select {
        case val := <-outChan:
            fmt.Println("finished:", val)
        case err := <-errChan:
            fmt.Println("error:", err)
            break Loop
        case <-finishChan:
            break Loop
        case <-time.After(100000 * time.Millisecond):
            break Loop
        }
    }
}

也可以在 Go Playground 試試看

GitHub 推出 CI/CD 服務 Actions 之踩雷經驗

$
0
0

GitHub Actions 簡介

今年很高興又去 Cloud Summit 研討會給一場議程『初探 GitHub 自動化流程工具 Actions』,這場議程沒有講很多如何使用 GitHub Actions,反倒是講了很多設計上的缺陷,以及為什麼我現在不推薦使用。GitHub Actions 在去年推出來,在這麼多 CI/CD 的免費服務,GitHub 自家出來做很正常,我還在想到底什麼時候才會推出,beta 版出來馬上就申請來試用,但是使用下來體驗非常的不好,有蠻多不方便的地方,底下我們就來聊聊 GitHub Acitons 有哪些缺陷以及該改進的地方。

簡報檔案

無法及時看到 Log 輸出

如果一個 Job 需要跑 30 分鐘,甚至更久,開發者一定會希望可以即時看到目前輸出了哪些 Log,看到 Log 的檔案,就可以知道下一步該怎麼修改,而不是等到 Job 結束後才可以看到 Log,然而 GitHub Actions 就是這樣神奇,需要等到 Job 跑完,才可以看到完整的 Log,你無法在執行過程看到任何一行 Log。光是這一點,直接打槍了 Firmware 開發者。

無法直接重新啟動 Job

通常有時候我們會需要重新跑已經失敗或者是取消的 Job,但是不好意思,GitHub Actions 沒有任何按鈕可以讓你重新跑單一 Commit 的 Job,你必須要重新 push 後,才可以重新啟動 Job,我覺得非常不合理,在 UI 上面有支持 Stop Job,但是不支援 Restart Job,我只能 …. 了。透過底下可以重新啟動 Job,但是太笨了

$ git reset —soft HEAD^
$ git commit -a -m "foo"
$ git push origin master -f

不支援 Global Secrets

假設在一個 organization 底下有 100 個 repository,每個 repository 都需要部署程式到遠端機器,這時候就必須要在每個 repo 設定 SSH Key 在 Secrets 選單內,但是必須要設定 100 次,假設支持在 organization 設定該有多好。

不支援第三方 Secrets 管理

除了用 Git 來管理 Secrets 之外,現在很流行透過第三方,像是 Vault, AWS Secretskubernetes secret,很抱歉,目前還沒支援第三方服務,現在還只能使用 GitHub 後台給的 Secrets 設定。

強制寫 CLI Flag

舉個實際例子比較快,現在有個 repo 必須同時部署到兩台密碼不同的 Linux Server,假設 Image 只支援 Password 這 global variable

  secrets = [
    "PASSWORD",
  ]
  args = [
    "--user", "actions",
    "--script", "whoami",
  ] 

我該怎麼在後台設定兩個不同變數讓 Docker 去讀取,這邊就不能這樣解決,所以開發者必須在程式裡面支援 指定 CLI 變數

  secrets = [
    "PASSWORD01",
  ]
  args = [
    "-p", "$PASSWORD01",
    "--script", "whoami",
  ] 

如果 Actions 沒有支援 -p 來設定參數,我相信這個套件肯定不能用。這就是 GitHub Actions 的缺點。我們看看 Drone 怎麼實作:

kind: pipeline
name: default

steps:
- name: build
  image: appleboy/drone-ssh
  environment:
    USERNAME:
      from_secret: username
    PASSWORD:
      from_secret: password 

有沒有注意到 from_secret 用來接受多個不同的 variable 變數。

環境變數太少

action "Hello World" {
  uses = "./my-action"
  env = {
    FIRST_NAME  = "Mona"
    MIDDLE_NAME = "Lisa"
    LAST_NAME   = "Octocat"
  }
} 

在 Actions 內開發者可以拿到底下變數內容

  • GITHUB_WORKFLOW
  • GITHUB_ACTION
  • GITHUB_EVNETNAME
  • GITHUB_SHA
  • GITHUB_REF

大家有沒有發現,那這個 commit 的作者或者是 commit message 該去哪邊拿?沒有這些資訊我怎麼寫 chat bot 發通知呢?該不會要開發者自己在打 RESTful API 吧?

心得

上面這些缺陷,真的讓大家用不下去,如果你再評估 GitHub Actions 的時候,可以參考看看這邊是否已經改善了?可以參考我開發的 GitHub Actions

Go Module 如何發佈 v2 以上版本

$
0
0

golang logo

Go ModuleGolang 推出的一套件管理系統,在 Go 1.11 推出後,許多 Package 也都陸續支援 Go Module 取代舊有的套件管理系統,像是 govendor 或 dep 等,而再過不久之後,保留 vendor 的方式也會被移除,畢竟現在開發已經不需要在 GOPATH 目錄底下了。對於 Go Module 不熟的話,建議先看官方今年寫的一篇教學部落格,底下是教學會涵蓋的範圍

  • Creating a new module.
  • Adding a dependency.
  • Upgrading dependencies.
  • Adding a dependency on a new major version.
  • Upgrading a dependency to a new major version.
  • Removing unused dependencies.

而本篇最主要會跟大家探討如何發佈 v2 以上的套件版本。

教學影片

更多實戰影片可以參考我的 Udemy 教學系列

go.mod 版本管理

使用 go mod init 之後會在專案目錄產生 go.mod 檔案,裡面可以看到像是底下的訊息

module github.com/go-ggz/ggz

go 1.12

require (
    firebase.google.com/go v3.8.0+incompatible
    github.com/appleboy/com v0.0.1
    github.com/appleboy/gofight/v2 v2.0.0
    gopkg.in/nicksrandall/dataloader.v5 v5.0.0
    gopkg.in/testfixtures.v2 v2.5.3
    gopkg.in/urfave/cli.v2 v2.0.0-20180128182452-d3ae77c26ac8
)

Go module 版本發佈請遵守 semver.org 規範,當然不只有 Go 語言,發佈其他語言的套件也請務必遵守。而在 Go module 內有說明『Semantic Import Versioning』裡面大意大致上是說,如果你的套件版本是 v1 以下,像是 v1.2.3,可以直接透過 go get xxxx 方式將套件版本寫入 go.mod,但是看上面的例子,可以發現有些奇怪的字眼,像是 +incompatible 等。如果是看到像底下的套件

    firebase.google.com/go v3.8.0+incompatible

表示使用此套件版本大於 v1 這時候是不可以直接下 go get firebase.google.com/go,而要在最後補上 v3

$ go get firebase.google.com/go/v3

就如同上面的

    github.com/appleboy/gofight/v2 v2.0.0
    gopkg.in/nicksrandall/dataloader.v5 v5.0.0
    gopkg.in/testfixtures.v2 v2.5.3

而可以發現 gopkg.in 服務已經符合 semver 的需求在做後面補上了 .v2.v5 所以並不會出現 +incompatible 字眼,另外在看

v2.0.0-20180128182452-d3ae77c26ac8

只要後面的格式出現 YYYYmmdd 這種格式表示該套件不支援 Go Module。

發佈 v2 以上版本方式

官方提供了兩種做法讓開發者可以發佈 v2 以上版本,一種是透過主 branch,另一種是建立版本目錄,底下來一一說明,第一種是直接在主 branch,像是 master 內的 go.mod 內補上版本,底下是範例:

module github.com/appleboy/gofight/v2

這時候你就可以陸續發佈 v2 版本的 Tag。大家可以發現這個方式,是不需要建立任何 sub folder,而另外一種方式是不需要改動 go.mod,直接在專案目錄內放置 v2 目錄,然後把所有程式碼複製一份到該目錄底下,接著繼續開發,這方式我個人不太建議,原因檔案容量會增加很快,也不好維護。

用 Ansible 安裝 Drone CI/CD 開源專案

$
0
0

drone and ansible

相信大家對於 Drone 開源專案並不陌生,如果對於 Drone 不了解的朋友們,可以直接看之前寫的系列文章,本篇要教大家如何使用 Ansible 來安裝 Drone CI/CD 開源專案。目前 Drone 可以支援兩種安裝方式: 1. 使用 Docker 2. 使用 binary,如果您是進階開發者,可以使用 binary 方式來安裝,像是在 Debug 就可以透過 build binary 方式來測試。一般來說都是使用 Docker 方式來安裝,在使用 ansible 之前,請先準備一台 Ubuntu 或 Debian 作業系統的 VM 來測試。

影片教學

更多實戰影片可以參考我的 Udemy 教學系列

事前準備

首先在您的電腦上安裝 ansible 環境,在 MacOS 很簡單,只需要透過 pip 就可以安裝完成

$ pip install ansible

更多安裝方式,可以直接看官方文件 Installation Guide

Ansible 環境

來看看 Ansible 專案目錄結構

├── Makefile
├── host.ini
├── playbook.yml
├── roles
│   ├── base
│   └── docker
└── vars
    ├── drone-agent.yml
    └── drone-server.yml

其中 roles 目錄是放置原本專案的角色,本篇內容不會提到,接著我們一一講解每個檔案,首先是 Makefile,裡面其實很簡單,只是兩個 ansible 指令,透過 ansible-lint 可以驗證 playbook 語法是否有錯誤,可以選用。

all: ansible

lint:
    ansible-lint playbook.yml

ansible: lint
    ansible-playbook -i host.ini playbook.yml

接著定義要在哪一台 VM 上面安裝 drone-server 或 drone-agent,請打開 host.ini

[drone_server]
dog ansible_user=multipass ansible_host=192.168.64.11 ansible_port=22

[drone_agent]
cat ansible_user=multipass ansible_host=192.168.64.11 ansible_port=22

這邊先暫時把 server 跟 agent 裝在同一台,如果要多台 drone-agent,請自行修改。接下來寫 playbook

- name: "deploy drone server."
  hosts: drone_server
  become: true
  become_user: root
  roles:
    - { role: appleboy.drone }
  vars_files:
    - vars/drone-server.yml

- name: "deploy drone agent."
  hosts: drone_agent
  become: true
  become_user: root
  roles:
    - { role: appleboy.drone }
  vars_files:
    - vars/drone-agent.yml

可以看到其中 var 目錄底下是放 server 跟 agent 的設定檔案,server 預設是跑 sqlite 資料庫。其中 drone_server_enable 要設定為 true,代表要安裝 drone-server

drone_server_enable: "true"
drone_version: "latest"
drone_github_client_id: "e2bdde88b88f7ccf873a"
drone_github_client_secret: "b0412c975bbf2b6fcd9b3cf5f19c8165b1c14d0c"
drone_server_host: "368a7a66.ngrok.io"
drone_server_proto: "https"
drone_rpc_secret: "30075d074bfd9e74cfd0b84a5886b986"

接著看 drone-agent.yml,也會看到要安裝 agent 就必須設定 drone_agent_enabletrue

drone_agent_enable: "true"
drone_version: "latest"
drone_rpc_server: "http://192.168.64.2:8081"
drone_rpc_secret: "30075d074bfd9e74cfd0b84a5886b986"

更多變數內容請參考這邊

Ansible 套件

我寫了 ansible-drone 角色來讓開發者可以快速安裝 drone 服務,安裝方式如下

$ ansible-galaxy install appleboy.drone

上面步驟是安裝 master 版本,如果要指定穩定版本請改成如下 (後面接上 ,0.0.2 版號)

$ ansible-galaxy install appleboy.drone,0.0.2

安裝角色後,就可以直接執行了,過程中會將機器先安裝好 Docker 環境,才會接著安裝 server 及 agent。

$ ansible-playbook -i host.ini playbook.yml

以上 Ansible 程式碼可以直接從這邊下載

心得

如果有多台機器需要建置,用 Ansible 非常方便。如果是多個 VM 需要快速開啟跟關閉,請透過 packer 來建置 Image 來達到快速 auto scale。更多詳細的設定可以參考 drone role of ansible

架設 Go Proxy 服務加速 go module 下載速度

$
0
0

golang logo

Go 語言在 1.11 推出 go module 來統一市面上不同管理 Go 套件的工具,像是 dep 或 govendor 等,還不知道如何使用 go module,可以參考之前寫的一篇文章『Go Module 導入到專案內且搭配 Travis CI 或 Drone 工具』,在團隊內如果每個人在開發專案時,都透過網路去下載專案使用到的套件,這樣 10 個人就會浪費 10 個人的下載時間,並且佔用公司網路頻寬,所以我建議在公司內部架設一台 Go Proxy 服務,減少團隊在初始化專案所需要的時間,也可以減少在跑 CI/CD 流程時,所需要花費的時間,測試過公司 CI/CD 流程,有架設 Go Proxy,一般來說可以省下 1 ~ 2 分鐘時間,根據專案使用到的相依性套件用量來決定花費時間。本篇來介紹如何架設 ATHENS 這套開源 Go Proxy 專案。

教學影片

架設 ATHENS 服務

你也可以使用外面公開的 GO Proxy 服務,非 China 地區請使用 goproxy.io,如果在中國內地,請使用 gorpoxy.cn,會降低不少 CI/CD 時間。架設 ATHENS 並不難,只需要透過 Docker 一個指令就可以完成,更詳細的步驟可以參考官方文件

export ATHENS_STORAGE=~/athens-storage
mkdir -p $ATHENS_STORAGE
docker run -d -v $ATHENS_STORAGE:/var/lib/athens \
   -e ATHENS_DISK_STORAGE_ROOT=/var/lib/athens \
   -e ATHENS_STORAGE_TYPE=disk \
   --name athens-proxy \
   --restart always \
   -p 3000:3000 \
   gomods/athens:latest

其中 ATHENS_STORAGE 請定義一個實體空間路徑,存放從網路抓下來的第三方套件,當然 ATHENS 還有支援不同的 storage type,像是 Memory, AWS S3 或公司內部有架設 Minio,都是可以設定的。

如何使用 Go Proxy

使用方式非常簡單,只要在您的開發環境加上一些環境變數

$ export GO111MODULE=on
$ export GOPROXY=http://127.0.0.1:3000

接著專案使用的任何 Go 指令,只要需要 Donwload 第三方套件,都會先詢問公司內部的 Proxy 服務,如果沒有就會透過 Proxy 抓一份下來 Cache,下次有團隊同仁需要用到,就不需要上 Internet 抓取了。

至於 CI/CD 流程該如何設定呢?非常簡單,底下是 drone 的設定方式:

- name: embedmd
  pull: always
  image: golang:1.12
  commands:
  - make embedmd
  environment:
    GO111MODULE: on
    GOPROXY: http://127.0.0.1:3000
  volumes:
  - name: gopath
    path: /go

心得

團隊如果尚未導入 GO Proxy 的朋友們,請務必導入,不然就要自己 cache mod 目錄,但是我覺得不是很方便就是了,架設一台 Proxy,不用一分鐘,但是可以省下團隊開發及部署很多時間,這項投資很值得的。


透過 Drone CLI 手動觸發 CI/CD 流程

$
0
0

drone promotion

相信大家對於 Drone 並不陌生,這次來介紹 Drone 1.0 的新功能 (更多的 1.0 功能會陸續介紹,也可以參考之前的文章),叫做 promotion,為什麼作者會推出這功能呢?大家在團隊工作時,有些步驟真的無法導入 CI/CD 自動化流程,而是需要人工介入後,再做後續處理?相信一定會遇到此狀況,PM 或老闆看過沒問題後,才需要手動觸發流程,在此功能以前,都會麻煩工程師幫忙做後續自動化流程,但是有了 promotion,現在連 PM 都可以透過 Drone CLI 來自己做部署啦,這邊就是介紹給大家,如何透過 Drone CLI 指令來觸發已存在的工作項目。

影片教學

如何使用

首先你必須要先安裝好 Drone CLI,安裝方式可以直接參考官方教學即可,透過底下例子來了解怎麼使用 promotion

kind: pipeline
name: testing

steps:
- name: stage
  image: golang
  commands:
  - echo "stage"
  when:
    event: [ promote ]
    target: [ staging ]

- name: production
  image: golang
  commands:
  - echo "production"
  when:
    event: [ promote ]
    target: [ production ]

- name: testing
  image: golang
  commands:
  - echo "testing"
  when:
    event: [ promote ]
    target: [ testing ]

上面可以看到,在 when 的條件子句內,可以設定 event 為 promote,接著 target 可以設定為任意名稱,只要是 promote 的 event type,在透過 git commit 預設都不會啟動的,只能透過 drone CLI 方式才可以觸發,那該如何執行命令呢?請看底下

drone build promote <repo> <build> <environment>

其中 build 就是直接在後台列表上找一個已經執行過的 job ID

drone build promote appleboy/golang-example 6 production

心得

Drone 提供手動觸發的方式相當方便,畢竟有些情境真的是需要人工審核確認過後,才可以進行後續的流程,透過此方式,也可以寫一些 routine 的 job 讓其他開發者,甚至 PM 可以透過自己的電腦觸發流程。

[Drone] 將單一 Job 分配到多台機器,降低部署執行時間

$
0
0

drone multiple machine

在傳統 CI/CD 流程,都是會在同一台機器上進行,所以當有一個 Job 吃了很大的資源時,其他工作都必須等待該 Job 執行完畢,釋放出資源後,才可以繼續進行。現在 Drone 推出一個新功能,叫做 Multiple Machine 機制,現在開發者可以將同一個 Job 內,拆成很多步驟,將不同的步驟丟到不同機器上面去執行,降低部署執行時間,假設現在有兩台機器 A 及 B,你可以將前端的測試丟到 A 機器,後端的測試,丟到 B 機器,來達到平行處理,並且享受兩台機器的資源,在沒有這機制之前,只能在單一機器上面跑平行處理,沒有享受到多台機器的好處。

影片介紹

實際範例

底下來看看如何將前端及後端的工作拆成兩台機器去跑:

kind: pipeline
name: frontend

steps:
- name: build
  image: node
  commands:
  - npm install
  - npm test

---
kind: pipeline
name: backend

steps:
- name: build
  image: golang
  commands:
  - go build
  - go test

services:
- name: redis
  image: redis

簡單設定兩個不同的 pipeline,就可以將兩條 pipeline 流程丟到不同機器上面執行。上述平行執行後,可以透過 depends_on 來等到上述兩個流程跑完,再執行。

---
kind: pipeline
name: after

steps:
- name: notify
  image: plugins/slack
  settings:
    room: general
    webhook: https://...

depends_on:
- frontend
- backend

Minio 從 Docker 容器移除 healthcheck 腳本

$
0
0

minio golang

Minio 是一套開源專案的 Object 儲存容器,如果你有使用 AWS S3,相信要找一套代替 S3 的替代品,一定會想到這套用 Go 語言開發的 Minio 專案。讓您在公司內部也可以享有 S3 的儲存容器,不需要變動任何程式碼就可以無痛從 AWS S3 搬到公司內部。剛好最近在整合 Traefik 搭配 Minio,由於 Minio 原先已經內建 healthcheck 腳本,所以當運行 Minio 時,使用 docker ps 正常來說可以看到類似 Up 7 weeks (healthy) 字眼,但是 Minio 運行了三分鐘之後,狀態就會從 healthy 變成 unhealthy,造成 Traefik 會自動移除 frontend 的對應設定,這樣 Web 就無法顯示了。我在 Udemy 上面有介紹如何用 Golang 寫 healthcheck,大家有興趣可以參考看看,coupon code 可以輸入 GOLANG2019

官方移除 healthcheck 腳本

我在官方發了一個 Issue,發現大家 workaround 的方式就是自己移除 healthcheck 檢查,然後再自行發布到 DockerHub,這方法也是可行啦,只是這樣還要自己去更新版本有點麻煩,後來官方直接發個 PR 把整段 Healthcheck 腳本移除,官方說法是說,容器那大家的設定的執行 User 或權限都不同,所以造成無法讀取 netstat 資料,所以直接移除,用大家熟悉的 curl 方式來執行,在 kubernets 內可以使用

healthcheck:
  image: minio/minio:RELEASE.2019-08-14T20-37-41Z
      test: ["CMD", "curl", "-f", "http://minio1:9000/minio/health/live"]
  volumes:
      interval: 1m30s
   - data2:/data
      timeout: 20s
  ports:
      retries: 3
   - "9002:9000"
      start_period: 3m

自行開發 healthcheck

如果你有看之前 minio 程式碼,可以發現寫得相當複雜,通常預設只要 ping 通 web 服務就可以了

resp, err := http.Get("http://localhost" + config.Server.Addr + "/healthz")
if err != nil {
  log.Error().
    Err(err).
    Msg("failed to request health check")
  return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
  log.Error().
    Int("code", resp.StatusCode).
    Msg("health seems to be in bad state")
  return fmt.Errorf("server returned non-200 status code")
}
return nil

接著在 Dockerfile 裡面寫入底下,就大功告成啦。

HEALTHCHECK --start-period=2s --interval=10s --timeout=5s \
  CMD ["/bin/crosspoint-server", "health"]

用 10 分鐘安裝好 Drone 搭配 GitLab

$
0
0

Drone+GitLab

如果你沒在使用 GitLab CI,那可以來嘗試看看 Drone CI/CD,用不到 10 分鐘就可以快速架設好 Drone,並且上傳一個 .drone.yml 並且開啟第一個部署或測試流程,安裝步驟非常簡單,只需要對 Docker 有基本上的了解,通常都可以在短時間完成 Drone CI/CD 架設。

教學影片

更多實戰影片可以參考我的 Udemy 教學系列

安裝 Drone Server

用 docker-compose 可以快速設定 Drone Server

services:
  drone-server:
    image: drone/drone:1
    ports:
      - 8081:80
    volumes:
      - ./:/data
    restart: always
    environment:
      - DRONE_SERVER_HOST=${DRONE_SERVER_HOST}
      - DRONE_SERVER_PROTO=${DRONE_SERVER_PROTO}
      - DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
      - DRONE_AGENTS_ENABLED=true
      # Gitlab Config
      - DRONE_GITLAB_SERVER=https://gitlab.com
      - DRONE_GITLAB_CLIENT_ID=${DRONE_GITLAB_CLIENT_ID}
      - DRONE_GITLAB_CLIENT_SECRET=${DRONE_GITLAB_CLIENT_SECRET}
      - DRONE_LOGS_PRETTY=true
      - DRONE_LOGS_COLOR=true

只要在 docker-compose.yml 底下新增 .env 檔案,將上面的變數值填寫進去即可

安裝 Drone Agent

雖然 drone 在 1.0 提供單機版,也就是 server 跟 agent 可以裝在同一台,但是本篇教學還是以分開安裝為主,對未來擴充性會更好。

  drone-agent:
    image: drone/agent:1
    restart: always
    depends_on:
      - drone-server
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - DRONE_RPC_SERVER=http://drone-server
      - DRONE_RPC_SECRET=${DRONE_RPC_SECRET}
      - DRONE_RUNNER_CAPACITY=3

完整的設定檔可以參考這邊

Go 語言目錄結構與實踐

$
0
0

golang logo

很高興今年錄取 Modernweb 講師,又有機會去宣傳 Go 語言,這次的議程最主要跟大家介紹 Go 專案的目錄該如何設計,一個基本的專案該需要有哪些功能,以及如何實現。大家剛入門 Go 時,肯定會開始找是否有一套 Web Framework 可以參考實踐,可惜的是,在 Go 語言沒有定義任何的目錄結構,所有的結構都可以根據團隊的狀況而有所改變,而這邊我想強調的是如果能讓團隊看到結構後,一目瞭然知道什麼功能該放哪個目錄,或什麼目錄內大概有什麼功能,那其實就夠了。看了許多開源專案,每個設計方式都是不同,但是當你要找什麼功能時,其實從根目錄就可以很清楚的知道要進入哪個地方可以找到您想要的功能及程式碼。這次在 Moderweb 上面的議題,就是分享我在開源專案所使用的目錄結構,以及結構內都放哪些必要的功能。

Go 語言基礎實踐

除了講 Go 的目錄架構外,我還會提到很多小技巧及功能,讓大家可以知道更多相關要入門的 Go 基礎知識,底下是大致上的功能清單:

  1. 如何使用 Makefile 管理 GO 專案
  2. 如何用 docker-compose 架設相關服務
  3. Go module proxy 介紹及部署
  4. 專案版本號該如何控制
  5. 如何在 Go 語言嵌入靜態檔案
  6. 如何實現 304 NOT Modified 功能
  7. 簡易的 Healthy check API
  8. Command Line 撰寫
  9. 如何實現讀取 .env 及環境變數 10 . 整合 Prometheus 搭配 Token 驗證
  10. 如何測試 Dokcer 容器是否正確
  11. 實作 custome errors
  12. 用 yaml 來產生真實 DB 資料來測試 (支援 SQLite, MySQL 或 Postgres)
  13. 透過 TestMain 來實現 setup 或 teardown 功能
  14. 用 Go 語言 Build Tags 支援 SQLite
  15. 介紹如何撰寫 Go 語言測試

最後來推廣我的兩門課程,由於 modernweb 不會提供會後錄影,所以我打算把上面的部分在製作影片放到 Udemy 平台給學生學習。

投影片

用 Drone 自動化上傳 Docker Image 到 GitHub Docker Registry

$
0
0

github

很高興收到 GitHub 的 Beta 邀請函來開始試用 GitHub Package Registry 相關功能,從說明文件可以知道目前 Registry 支援了好幾種 Package 像是 npm, gem, docker, mvnnuget,這篇主要跟大家介紹如何用 Drone 快速串接 CI/CD 流程的『自動上傳 Docker Image 到 GitHub Registry』,底下來看看如何使用 GitHub 提供的 Docker Registry。

教學影片

更多實戰影片可以參考我的 Udemy 教學系列

GitHub 認證

$ docker login docker.pkg.github.com -u USERNAME -p PASSWORD/TOKEN

要登入 GitHub 的 Docker Registry,最快的方式就是用個人的帳號及密碼就可以直接登入,而 Registry 設定則是 docker.pkg.github.com,這邊請注意,雖然官方有寫可以用個人的 Password 登入,如果你有使用 OTP 方式登入,這個方式就不適用,也不安全,我個人強烈建議去後台建立一把專屬的 Token。

Personal Token

其中 read:packages and write:packages 兩個 scope 請務必勾選,如果是 private 的 repo,再把 repo 選項打勾,這樣就可以拿到一把 token 當作是密碼,你可以透過 docker login 來登入試試看

串接 Drone CI/CD

從 commit 到自動化上傳 Docker Image 可以透過 Drone 快速完成,底下我們先建立 Dockerfile

FROM plugins/base:multiarch

LABEL maintainer="Bo-Yi Wu <appleboy.tw@gmail.com>" \
  org.label-schema.name="Drone Workshop" \
  org.label-schema.vendor="Bo-Yi Wu" \
  org.label-schema.schema-version="1.0"

ADD release/linux/amd64/helloworld /bin/

HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD [ "/bin/helloworld", "-ping" ]

ENTRYPOINT ["/bin/helloworld"]

接著透過 Drone 官方 docker 套件來完成自動化上傳

kind: pipeline
name: default

steps:
- name: build
  image: golang:1.13
  commands:
  - make build_linux_amd64

- name: docker
  image: plugins/docker
  settings:
    registry: docker.pkg.github.com
    repo: docker.pkg.github.com/appleboy/test/demo
    auto_tag: true
    auto_tag_suffix: linux-amd64
    username: appleboy
    password:
      from_secret: docker_password

比較需要注意的是 GitHub 跟 DockerHub 不同的是,GitHub 格式是 OWNER/REPOSITORY/IMAGE_NAME,注意中間有多一個 REPOSITORY 而 DockerHub 是 OWNER/IMAGE_NAME。接著到後台將 docker_password 設定完成,就可以正確部署了。

Ansible 設定 Google Container Registry 搭配 Drone 自動上傳

$
0
0

blog logo

最近剛好有需求要串接 GCR (Google Container Registry),專案如果是搭配 GCP 服務,個人建議就直接用 GCR 了。本篇要教大家如何透過 Ansible 管理遠端機器直接登入 GCR,透過特定的帳號可以直接拉 Image,接著用 docker-compose 來重新起動服務,這算是最基本的部署流程,那該如何用 Ansible 登入呢?請看底下教學。

使用 ansible docker_login 模組

Google 提供了好幾種方式來登入 Docker Registry 服務,本篇使用『JSON 金鑰檔案』方式來長期登入專案,開發者會拿到一個 JSON 檔案,在本機電腦可以透過底下指令登入:

cat keyfile.json | docker login \
  -u _json_key \
  --password-stdin \
  https://[HOSTNAME]

如果沒有支援 password-stdin 則可以使用底下:

docker login -u _json_key \
  -p "$(cat keyfile.json)" \
  https://[HOSTNAME]

請注意這邊的使用者帳號統一都是使用 _json_key,而在 Ansible 則是使用 docker_login 模組

- name: Log into GCR private registry and force re-authorization
  docker_login:
    registry: "https://asia.gcr.io"
    username: "_json_key"
    password: "{{ lookup('template', 'gcr.json', convert_data=False) | string }}"
    config_path: "{{ deploy_home_dir }}/.docker/config.json"
    reauthorize: yes

注意 password 欄位,請將 gcr.json 放置在 role/templates 目錄,透過 lookup 方式並轉成 string 才可以正常登入,網路上有解法說需要在 password 前面加上一個空白才可以登入成功,詳細情況可以參考這篇

使用 Drone 自動化上傳 Image

講 Drone 之前,我們先來看看 GitLab 怎麼上傳,其實也不難:

cloudbuild:
  stage: deploy
  image: google/cloud-sdk
  services:
    - docker:dind
  dependencies:
    - build
  script:
    - echo "$GCP_SERVICE_KEY" > gcloud-service-key.json
    - gcloud auth activate-service-account --key-file gcloud-service-key.json
    - gcloud config set project $GCP_PROJECT_ID
    - gcloud builds submit . --config=cloudbuild.yaml --substitutions _IMAGE_NAME=$PROJECT_NAME,_VERSION=$VERSION
  only:
    - release

透過 gcloud 就可以快速自動上傳。而使用 Drone 設定也是很簡單:

- name: publish
  pull: always
  image: plugins/docker
  settings:
    auto_tag: true
    auto_tag_suffix: linux-amd64
    registry: asia.gcr.io
    cache_from: asia.gcr.io/project_id/image_name
    daemon_off: false
    dockerfile: docker/ponyo/Dockerfile.linux.amd64
    repo: asia.gcr.io/project_id/image_name
    username:
      from_secret: docker_username
    password:
      from_secret: docker_password
  when:
    event:
      exclude:
      - pull_request

其中 password 可以透過後台將 json 資料寫入。這邊有幾個重要功能列給大家參考

  • 使用 cache_from 加速
  • 使用 auto_tag 快速部署標籤

這兩項分別用在什麼地方,cache_from 可以直接看我之前寫過的一篇『在 docker-in-docker 環境中使用 cache-from 提升編譯速度』裡面蠻詳細介紹,並且有影片。而 auto_tag 最大的好處是在 release 開源專案 Image,只要你的 tag 有按照標準格式,像是如果是打 v1.0.1 這時候 Drone 會分別產生三個 Image: 1, 1.0, 1.0.1,下次又 release v1.0.2,這時候 1.0 就會指向 1.0.2,類似這樣以此類推,方便其他使用者抓取 Image。這是在其他像是 GitLab 無法做到,應該說不是無法做到,而是變成要自己寫 script 才能做到。

教學影片

底下是教您如何使用 docker cache 機制,如果你的 Image 特別大,像是有包含 Linux SDK 之類的,就真的用 cache 會比較快喔。

歡迎訂閱我的 Youtube 頻道: http://bit.ly/youtube-boy

更多實戰影片可以參考我的 Udemy 教學系列


在 Docker 內設定使用者及群組權限的三種方式

$
0
0

docker

如果平常本身有在玩 Docker 的開發者肯定知道透過 docker command 啟動的容器預設是使用 root 來當作預設使用者及群組,這邊會遇到一個問題,當 Host 環境你有 root 權限就沒有此問題,如果你沒有 root 權限,又有需求在 Docker 容器內掛上 Volume,會發現產生出來的檔案皆會是 root 權限,這時候在 Host 完全無法寫入。本篇教大家使用三種方式來設定容器使用者權限。

使用 docker 指令時指定使用者

進入 Ubuntu 容器會透過底下指令:

docker run -ti ubuntu /bin/bash

這時候可以透過 -u 方式將使用者 uid 及群組 gid 傳入容器內。

mkdir tmp
docker run -ti -v $PWD/tmp:/test \
  -u uid:gid ubuntu /bin/bash

如何找到目前使用者 uid 及 gid 呢,可以透過底下方式

id -u
id -g

上述指令可以改成:

docker run -ti -v $PWD/tmp:/test \
  -u $(id -u):$(id -g) ubuntu /bin/bash

使用 Dockerfile 指定使用者

也可以直接在 dockerfile 內直接指定使用者:

# Dockerfile

USER 1000:1000

我個人不是很推薦這方式,除非是在 container 內獨立建立使用者,並且指定權限。

透過 docker-compose 指定權限

透過 docker-compose 可以一次啟動多個服務。用 user 可以指定使用者權限來寫入特定的 volume

services:
  agent:
    image: xxxxxxxx
    restart: always
    networks:
      - proxy
    logging:
      options:
        max-size: "100k"
        max-file: "3"
    volumes:
      - ${STORAGE_PATH}:/data
    user: ${CURRENT_UID}

接著可以透過 .env 來指定變數值

STORAGE_PATH=/home/deploy/xxxx
CURRENT_UID=1001:1001

心得

會指定使用者權限通常都是有掛載 Host volume 進入容器內,但是您又沒有 root 權限,如果沒有這樣做,這樣產生出來的檔案都會是 root 權限,一般使用者無法寫入,只能讀取,這時就需要用到此方法。

用 Go 語言實作 Job Queue 機制

$
0
0

golang logo

很高興可以在 Mopcon 分享『用 Go 語言實現 Job Queue 機制』,透過簡單的 goroutinechannel 就可以實現簡單 Queue 機制,並且限制同時可以執行多少個 Job,才不會讓系統超載。最後透過編譯放進 Docker 容器內,就可以跑在各種環境上,加速客戶安裝及部署。

議程大綱

本次大致上整理底下幾個重點:

  1. What is the different unbuffered and buffered channel?
  2. How to implement a job queue in golang?
  3. How to stop the worker in a container?
  4. Shutdown with Sigterm Handling.
  5. Canceling Workers without Context.
  6. Graceful shutdown with worker.
  7. How to auto-scaling build agent?
  8. How to cancel the current Job?

由於在投影片內也許寫得不夠詳細,所以我打算錄製一份影片放在 Udemy 教學影片上,如果有興趣可以參考底下影片連結:

之前的教學影片也可以直接參考底下連結:

投影片

Drone CI/CD 支援 Auto cancellation 機制

$
0
0

大家一定會問什麼是『Auto cancellation』呢?中文翻作自動取消,這機制會用在 CI/CD 的哪個流程或步驟呢?我們先來探討一個情境,不知道大家有無遇過在同一個 branch 陸續發了 3 個 commit,會發現在 CI/CD 會依序啟動 3 個 Job 來跑這 3 個 commit,假設您有設定同時間只能跑一個 Job,這樣最早的 commit 會先開始啟動,後面兩個 commit 則會處於 Penging 的狀態,等到第一個 Job 完成後,後面兩個才會繼續執行。

這邊就會有個問題出現,假設後續團隊又 commit 了 10 個 job,這樣 Pending 狀態則會越來越多,不會越來越少,這時候開發者一定會想,有沒有辦法只保留最新的 Job,而舊有的 Pending Job 系統幫忙取消呢?這個功能在 Travis CI 已經有後台可以啟動,新專案也是預設啟動的,也就是假設現在有一個 job 正在執行,有九個 job 正在 pending 時,新的 job 一進來後,CI/CD 服務就會自動幫忙取消舊有的九個 Job,只保留最新的,確保系統不會浪費時間在跑舊的 Job。Drone 在 1.6 也支持了此功能,底下來看看如何設定 Drone 達到此需求。

影片教學

更多實戰影片可以參考我的 Udemy 教學系列

設定 Drone

Drone 在 1.6 版才正式支援『Auto cancellation』,而且每個專案預設是不啟動的,需要透過 Drone CLI 才能正確啟動。底下來看看如何透過 CLI 啟動:

# 啟用 pull request
drone repo update \
  --auto-cancel-pull-requests=true 
  appleboy/go-hello
# 啟用 push event
drone repo update \
  --auto-cancel-pushes=true \
  appleboy/go-hello

目前還沒有辦法透過後台 UI 介面啟用,請大家使用上述指令來開啟 Auto Cancellation 功能。

用 Go 語言 buffered channel 實作 Job Queue

$
0
0

上個月在高雄 mopcon 講了一場『Job Queue in Golang』,裡面提到蠻多技術細節,但是要在一場 40 分鐘的演講把大家教會,或者是第一次聽到 Go 語言的,可能都很難在 40 分鐘內吸收完畢,所以我打算分好幾篇部落格來分享細部的實作,本篇會講解投影片第 19 ~ 25 頁,透過本篇你可以清楚學到什麼是 buffered channel,以及實作的注意事項。

投影片及教學影片

本篇的實作,也有錄製成教學影片,請參考如下:

如果對於課程內容有興趣,可以參考底下課程。

實際案例

怎麼透過 buffered channel 來建立簡單的 Queue 機制。請看底下程式碼:

func worker(jobChan <-chan Job) {
    for job := range jobChan {
        process(job)
    }
}

// make a channel with a capacity of 1024.
jobChan := make(chan Job, 1024)

// start the worker
go worker(jobChan)

// enqueue a job
jobChan <- job

上面很清楚看到把 worker 丟到背景,接著將 Job 丟進 Channel 內,就可以在背景做一些比較複雜的工作。但是大家看到 jobChan <- job 是不是會想到一個問題,這邊會不會 blocking 啊?答案是會的,那你會說可以把 1024 調整大一點啊,這我不否認這是一種解法,但是你還是無法保證不會 blocking 啊。底下用一個簡單的例子來說明問題:

package main

import (
    "fmt"
    "time"
)

func worker(jobChan <-chan int) {
    for job := range jobChan {
        fmt.Println("current job:", job)
        time.Sleep(3 * time.Second)
        fmt.Println("finished job:", job)
    }
}

func main() {
    // make a channel with a capacity of 1.
    jobChan := make(chan int, 1)

    // start the worker
    go worker(jobChan)

    // enqueue a job
    fmt.Println("enqueue the job 1")
    jobChan <- 1
    fmt.Println("enqueue the job 2")
    jobChan <- 2
    fmt.Println("enqueue the job 3")
    jobChan <- 3

    fmt.Println("waiting the jobs")
    time.Sleep(10 * time.Second)
}

可以到 playground 執行上面的例子會輸出什麼?答案如下:

enqueue the job 1
enqueue the job 2
enqueue the job 3
current job: 1 <- 程式被 blocking
finished job: 1
waiting the jobs
current job: 2
finished job: 2
current job: 3
finished job: 3

大家應該都知道這個 main 被 blocking 在 jobChan <- 3,因為我們只有設定一個 channel buffer,所以當我們送第一個數字進去 channel 時 (channel buffer 從 0 -> 1),會馬上被 worker 讀出來 (buffer 從 1 -> 0),接著送第二個數字進去時,由於 worker 還正在處理第一個 job,所以第二個數字就會被暫時放在 buffer 內,接著送進去第三個數字時 jobChan <- 3 這時候會卡在這邊,原因就是 buffer 已經滿了。

這邊有兩種方式來解決主程式被 blocking 的問題,第一個方式很簡單,把丟 Job 的程式碼用 goroutine 丟到背景。也就是改成如下:

    fmt.Println("enqueue the job 3")
    go func() {
        jobChan <- 3
    }()

另一種方式透過 select 來判斷 Channel 是否可以送資料進去

func enqueue(job int, jobChan chan<- int) bool {
    select {
    case jobChan <- job:
        return true
    default:
        return false
    }
}

有了 enqueue 之後,就可以知道目前的 Channel 是否可以送資料,也就是可以直接回應給 User,而不是等待被送入,這邊大家要注意的是使用情境,假設你的 Job 是需要被等待的,那這個方式就不適合,如果 Job 是可以丟棄的,就是用此方式回傳 503 或是其他 error code 告訴使用者目前 Queue 已滿。上面例子改寫如下:

    fmt.Println(enqueue(1, jobChan)) // true
    fmt.Println(enqueue(2, jobChan)) // true
    fmt.Println(enqueue(3, jobChan)) // false

可以很清楚知道當程式拿到 false 時,就可以做相對應處理,而不會卡到主程式,至於該做什麼處理,取決於商業邏輯。所以 buffer channel 要設定多大,以及達到限制時,該做哪些處理,這些都是在使用 buffer channel 時要考慮進去,來避免主程式或者是 goroutine 被 blocking。上述如果不了解的話,可以參考上面 Youtube 影片,會有很詳細的講解。

初探 Go 語言 Project Layout (新人必看)

$
0
0

cover photo

很多人初次進入 Go 語言,肯定都會尋找在 Go 裡面是否有一套標準且最多人使用的 Framework 來學習,但是在 Go 語言就是沒有這樣的標準,所有的開源專案架構目錄都是由各團隊自行設計,沒有誰對誰錯,也沒任何一個是最標準的。那你一定會問,怎樣才是最好的呢?很簡單,如果可以定義出一套結構是讓團隊所有成員可以一目瞭然的目錄結構,知道發生問題要去哪個地方找,要加入新的功能,就有相對應的目錄可以存放,那這個專案就是最好的。當然這沒有標準答案,只是讓團隊有個共識,未來有新人進入專案,可以讓他在最短時間內吸收整個專案架構。

投影片

本次教學會著重在投影片 P5 ~ P20。

教學影片

喜歡我的 Youtube 影片,可以訂閱 + 分享喔

  1. project layout 基本簡介 00:47
  2. 為什麼要用 go module 07:28
  3. 使用 Makefile 09:59
  4. .env 使用情境 11:42
  5. 如何設定專案版本資訊 12:54

未來會將投影片剩下的內容,錄製成影片放在 Udemy 平台上面,有興趣的可以直接參考底下:

Viewing all 325 articles
Browse latest View live