用 Go 語言撰寫簡單的 Command Line 工具

golang logo

之前介紹了一個開源工具『用 Docker 每天自動化備份 MySQL, Postgres 或 MongoDB 並上傳到 AWS S3』,讓開發者可以快速透過 Docker 方式來備份資料庫,而本篇要介紹我如何用 Go 語言來撰寫 CLI 並且整合 Docker 來實現備份。此工具都是透過各大資料庫官方提供的 CLI 指令 (pg_dump, mysqldump … 等),故大家不用猜想是什麼神奇的技巧。底下來依序介紹整個目錄結構,及我如何實現。

影片介紹

  • 00:00 backup 資料庫工具簡介
  • 01:29 專案目錄分層介紹
  • 03:16 介紹 pkg/storage interface
  • 04:35 介紹 pkg/dbdump interface
  • 06:15 為什麼要用 urfave/cli v2 版本
  • 07:25 用 Drone 做自動化上傳多種不同 Docker Image
  • 08:40 go build + demo 使用 CLI
  • 09:33 介紹 cmd 目錄底下產生多個 CLI 目錄

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

如果需要搭配購買請直接透過 FB 聯絡我,直接匯款(價格再減 100

目錄結構

底下教學就拿 docker-backup-database 為範例,我從目錄結構開始講,剛入門的朋友肯定對於 Go 語言在目錄這塊定義的不是很清楚,但是其實這跟個人或團隊喜好也有點關係,底下看看目錄結構

 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
├── LICENSE
├── Makefile
├── README.md
├── cmd
│   └── backup
│       ├── config.go
│       └── main.go
├── docker
│   ├── Dockerfile.mongo.3.6
│   ├── Dockerfile.mongo.4
│   ├── Dockerfile.mongo.4.2
│   ├── Dockerfile.mongo.4.4
│   ├── Dockerfile.mysql.5.6
│   ├── Dockerfile.mysql.5.7
│   ├── Dockerfile.mysql.8
│   ├── Dockerfile.postgres.10
│   ├── Dockerfile.postgres.11
│   ├── Dockerfile.postgres.12
│   ├── Dockerfile.postgres.13
│   └── Dockerfile.postgres.9
├── docker-compose.yml
├── go.mod
├── go.sum
└── pkg
    ├── config
    │   └── config.go
    ├── dbdump
    │   ├── dbdmp.go
    │   ├── mongo
    │   │   └── mongo.go
    │   ├── mysql
    │   │   └── mysql.go
    │   └── postgres
    │       └── postgres.go
    ├── helper
    │   └── cmd.go
    └── storage
        ├── core
        │   └── core.go
        ├── disk
        │   ├── disk.go
        │   └── disk_test.go
        ├── minio
        │   └── minio.go
        └── storage.go

其實目錄結構相當清楚,根目錄底下只會放跟部署或教學相關的資訊,像是 .drone.yml 用來做 CI/CD 幫忙自動化建立 Docker Image 並且上傳到 Docker Hub。而 docker-compose.yml 則是一份簡單的教學範例,讓想使用此工具的開發者可以快速建置出 Minio 或 Postgres 環境。而最後一個 Makefile 存放很多相關的指令,我本身不太喜歡打很長的指令,直接把用到的指令全都寫在 Makefile 內,這樣在寫 CI/CD 或同事及開發者想快速使用時,幾個指令就可以搞定了。盡量不要把指令在 CI/CD 流程中複雜化,這樣不好維護。而 docker 目錄會是此專案用到的所有 Dockerfile,就放在一起了,透過 Drone 直接平行化編譯 Image 並上傳。

cmd 目錄

1
2
3
4
├── cmd
│   └── backup
│       ├── config.go
│       └── main.go

開發者都可以發現在 GitHub 上面的 Go 開源專案,幾乎都會有一個 cmd 目錄,因為不想把 main.go 放在跟目錄下,然後又要命名一個還不錯的名稱,就需要建立在 cmd 目錄底下,這樣大家透過 go get 才可以正確下載到您要的命令,通常一個專案也許會有多個 CLI 工具,那就會在 cmd 底下建立多個目錄,每個目錄都會有 main.go 檔案,以現在這個範例為例,只會有一個 CLI,大家可以透過底下指令來下載 CLI。

1
go get github.com/appleboy/docker-backup-database/cmd/backup

在 CLI 套件選擇,我則是選擇了 urfave/cli,原因很簡單,此工具未來會應用在 CI/CD 流程上,所以會希望可以直接支援 GitHub Actions, Drone CI 或 GitLab CI,而 urfave/cli 讓開發者可以自行定義 ENV,原因是 GitHub Actions 只支援 INPUT_ 而 Drone CI 只支援 PLUGIN_,故 urfave/cli 讓我自由定義,只有這個原因才選這套件。

pkg 目錄

我會把專案用到的其他功能都一併建立在這邊,由這個目錄底下在做分類

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
└── pkg
    ├── config
    │   └── config.go
    ├── dbdump
    │   ├── dbdmp.go
    │   ├── mongo
    │   │   └── mongo.go
    │   ├── mysql
    │   │   └── mysql.go
    │   └── postgres
    │       └── postgres.go
    ├── helper
    │   └── cmd.go
    └── storage
        ├── core
        │   └── core.go
        ├── disk
        │   ├── disk.go
        │   └── disk_test.go
        ├── minio
        │   └── minio.go
        └── storage.go

可以看到我分了幾個目錄,config 用來存放 CLI 用到的環境變數,而 storage 用來定義上傳雲服務 AWS S3 或 Minio 的 Interface:

 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
// Storage for s3 and disk
type Storage interface {
    // CreateBucket for create new folder
    CreateBucket(string, string) error
    // UploadFile for upload single file
    UploadFile(string, string, []byte, io.Reader) error
    // DeleteFile for delete single file
    DeleteFile(string, string) error
    // FilePath for store path + file name
    FilePath(string, string) string
    // GetFile for storage host + bucket + filename
    GetFileURL(string, string) string
    // DownloadFile downloads and saves the object as a file in the local filesystem.
    DownloadFile(string, string, string) error
    // BucketExist check object exist. bucket + filename
    BucketExists(string) (bool, error)
    // FileExist check object exist. bucket + filename
    FileExist(string, string) bool
    // GetContent for storage bucket + filename
    GetContent(string, string) ([]byte, error)
    // Copy Create or replace an object through server-side copying of an existing object.
    CopyFile(string, string, string, string) error
    // Client get storage client
    Client() interface{}
    // SignedURL get signed URL
    SignedURL(string, string, *core.SignedURLOptions) (string, error)
}

定義完成後,未來有其他的 Storage 需要支援,就可以直接在 storage 目錄底下建立新的目錄,接著就可以直接開發了。另外 dbdump 也是同樣原理,現在支援三種 Database 而已,未來可以接受擴充到任何資料庫型態。

1
2
3
4
5
// Backup database interface
type Backup interface {
    // Exec backup database
    Exec() error
}

可以看到 NewEngine

 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
// NewEngine return storage interface
func NewEngine(cfg config.Config) (backup Backup, err error) {
    switch cfg.Database.Driver {
    case "postgres":
        return postgres.NewEngine(
            cfg.Database.Host,
            cfg.Database.Username,
            cfg.Database.Password,
            cfg.Database.Name,
            cfg.Storage.DumpName,
            cfg.Database.Opts,
        )
    case "mysql":
        return mysql.NewEngine(
            cfg.Database.Host,
            cfg.Database.Username,
            cfg.Database.Password,
            cfg.Database.Name,
            cfg.Storage.DumpName,
            cfg.Database.Opts,
        )
    case "mongo":
        return mongo.NewEngine(
            cfg.Database.Host,
            cfg.Database.Username,
            cfg.Database.Password,
            cfg.Database.Name,
            cfg.Storage.DumpName,
            cfg.Database.Opts,
        )
    }

    return nil, errors.New("We don't support Databaser Dirver: " + cfg.Database.Driver)
}

心得

雖然是一個不起眼的功能,但是還是花了一些時間把文件及結構寫清楚,對於之後在團隊導入或者是有新的 CLI 工具,都可以按照這格式進行,當然技術會一直改變,只會更好不會更差。希望這教學可以分享給想踏入 Go 語言,或者是想寫寫簡單的 CLI 工具的朋友們參考看看。


See also