Golang Modules and Packages

source : https://pjchender.dev/golang/modules-and-packages/

不論是 module 或 package,都可以在本地匯入,不一定要發到遠端 repository。

# 在 hello 資料夾中
# 宣告 module_path,通常會和該 repository 的 url 位置一致
$ go mod init example.com/user/hello

# 編譯該 module 並將執行檔放到 GOBIN,因此在 GOBIN 資料夾中會出現 hello 的執行檔
$ go install . 

# 移除沒用到的套件
$ go mod tidy

# 移除所有下載第三方套件的內容
$ go clean -modcache

安裝到 GOBIN 資料夾的檔案名稱,會是在 go.mod 檔案中第一行定義 module path 中路徑的最後一個。 因此若 module_path 是 example.com/user/hello 則在 GOBIN 中的檔名會是 hello; 若 module_pathexample/user 則在 GOBIN 資料夾中的檔名會是 user

  • 若我們在 go module 中有使用其他的遠端(第三方)套件,當執行 go install、go build 或 go run 時,go 會自動下載該 remote module,並記錄在 go.mod 檔案中。
  • 這些遠端套件會自動下載到 $GOPATH/pkg/mod 的資料夾中
  • 當有不同的 module 之間需要使用相同版本的第三方套件時,會共用這些下載的內容,因此這些內容會是「唯讀」。
  • 若想要刪除這些第三方套件的內容,可以輸入 go clean -modcache

設定 GOPAHT

vi ~/.zshrc

# go path
export GOBIN=$HOME/go/bin
export PATH=$PATH:/usr/local/go/bin:$GOBIN
export GOPATH=$HOME/go

Packages

  • Go 的程式碼都是以 package 的方式在組織,一個 package 就是在同一資料夾中的許多 Go 檔案。
  • 同一個 package 的所有 Go 檔案會放在同一個資料夾內;而這個資料夾內的所有檔案也都會屬於同一個 package,有相同的 package 名稱。
  • 如果套件是在 module 中,go import package 的路徑會是 module path 加上 subdirectory
  • 通常 package 的名稱會跟著 folder 的名稱,舉例來說,若檔案放在 math/rand 資料夾中,則該套件會稱作 rand
  • package scope:
    • 在 Go 語言中,並沒有區分 publicprivateprotected,而是根據變數名稱的第一個字母大小寫來判斷能否被外部引用。
    • 在同一個 package 中變數、函式、常數和 type,都隸屬於同一個 package scope,因此雖然可能在不同支檔案內,但只要隸屬於同一個 package,都可以使用(visible)。
    • 如果需要讓 package 內的變數或函式等能夠在 package 外部被使用,則該變數的第一個字母要大寫才能讓外部引用(Exported names),否則的話會無法使用
  • 有兩種不同類型的 package:
    1. executable package:是用來產生我們可以執行的檔案,一定會包含 package main 並且帶有名為 main 的函式,只有這個檔案可以被執行(run)和編譯(build),並且不能被其他檔案給匯入
    2. reusable package(library package):類似 "helpers" 或常稱作 library / dependency,目的是可以放入可重複使用的程式邏輯,它是不能被直接執行的,可以使用任何的名稱

在 Go 裡面要區別這兩種 Package 的主要方式就是利用「package 名稱」,當使用 main 當做 package 名稱時,就會被當作 executable package,因此接著執行 go build <fileName> 時,會產生一支執行檔; 但是當使用 main 以外的名稱是,都會被當作是 reusable package,因此當使用 go build 指令時,不會產生任何檔案。

要匯入 reusable package 只需要:

// 匯入單一個 package
import "fmt"

// 匯入多個 packages(不用逗號)
import (
  "fmt"
  "strings"
)
// go-hello-world/main.go
package main

import (
    "fmt"

    // 把 foo 這個 package 的方法都放到這隻檔案中,如此不用使用 foo.HelloWorld(不建議)
    . "go-hello-world/foo"

    // 將模組轉換為別名,可以使用 bar.HelloWorld
    bar "go-hello-world/foo"

    // 沒有用到這個 package,但要 init 它
    _ "go-hello-world/foo"
)

func main() {
    fmt.Println(bar.HelloWorld())
    fmt.Println("Hello main")
}
// go-hello-world/foo/helloworld.go

package foo

import "fmt"

func init() {
    fmt.Println("This is init of helloworld")
}

// HelloWorld ...
func HelloWorld() string {
    return "Hello World"
}

package name: coding style and convention

在 Go 中,package 的名稱應該是短而清楚,以小寫(lower case)命名,同時不包含底線(under_scores)或小寫駝峰(mixedCaps),並且通常會是名詞(noun),例如 time、list、或 http。

在幫 package 命名的時候,試想自己就是使用該 pkg 的開發者,用這種角度來替自己的 pkg 命名。

另外,由於使用者在匯入該 package,假設引入的 使用時,一定會需要使用該 package 的 name 作為前綴,因此在 package 中的變數名稱盡可能不要和 package name 重複:

  • http package 中如果要使用 Server,不需要使用 http.HTTPServer,而是可以直接使用 http.Server
  • 當在 uuid 的 package 要產生一組 uuid.UUID 時,不需要使用 uuid.NewUUID() 的方法,而是可以直接使用 uuid.New(),也就是說如果回傳的型別名稱(UUID)和該 pkg 的名稱相同時,可以直接將該方法命名成 New,而不用是 NewOOO
    • time.Now() 會回傳 time.Time
  • 如果 pkg 會回傳的 struct 名稱不同於 package 本身的名稱時,則可以使用 NewOOO
    • time.NewTicker()
    • uuid.NewRandom()

      💡 雖然 pkg 中 variable name 的前綴會盡量不和 package name 重複,但很常見的情況是在該 pkg 中有其同名的 struct,例如 time pkg 中有名為 Time 的 struct,因此型別會是 time.Time,

不好的用法:

  • 盡可能不要使用 util, common, misc 這類的名稱作為 package name,因為這對使用者來說是沒有意義的名字,而是去想使用者這如果要用這些方法的話,最有可能使用到的關鍵字是什麼。
  • 見範例「Break up generic packages

go tool 找 package 的邏輯

go tool 會使用 $GOPATH 來找對應的 package,假設引入的 package 路徑是 "github.com/user/hello",那麼 go tool 就會去找 $GOPATH/src/github.com/user/hello

Modules

GO111MODULE 描述
GO111MODULE=on 支持 module,需要使用 go mod,有 go.mod 才能正常運作
GO111MODULE=off 不支持 module,會從 GOPATH 或 vendor 來尋找。如果不是在 GOPATH 執行 go mod init 則會報錯。
GO111MODULE=auto 會檢查當前目錄是否啟用 go.mod,以及陸續往上層尋找,最後才會從 GOPATH 檢查。
# 初始化 Go Module
$ export GO111MODULE=on  # 在 GOPATH 外要使用 module 需要啟動

# go mod init [module_path]
$ go mod init example.com/user/hello        # 宣告 module path
$ go mod tidy        # 移除沒用到的 library
$ go mod download    # 下載套件(go build 和 go test 也會自動下載)

$ go get [library]   # 新增或更新 package 到 Module 內
$ go get -u ./...    # 等同於,go get -u=patch ./...
$ go get foo@master. # 下載特定版本的 go package

$ go list -m all      # 印出 module 正在使用的所有套件
$ go list -m -versions [package]   # 列出所有此套件可下載的版本
$ go list -u -m all   # 檢視有無任何 minor 或 patch 的更新
  • 在一個專案中通常只會有一個 module(但也可以有多個),並且放在專案的根目錄,module 裡面會集合所有相關聯的 Go packages。在 go.mod 中會宣告 module path,這是用來匯入所有在此 module 中的路徑的前綴(path prefix),同時它也讓 go 的工具知道要去哪裡下載它。
  • 透過 Modules 可以準確紀錄相依的套件,讓程式能再次被編譯。
  • 總結來說:
    • 一個 repository 會包含一個或以上的 Go modules
    • 每個 module 會包含一個或以上的 Go packages
    • 每個 package 會包含一個或以上的檔案在單一資料夾中
  • 在執行 go buildgo test 時,會根據 imports 的內容自動添加套件,並更新 go.mod
  • 當需要的時候,可以直接在 go.mod 指定特定的版本,或使用 go get,例如 go get foo@v1.2.3, go get foo@master, go get foo@e3702bed2

go.mod

在 root directory 中會透過 go.mod 來定義 Module,而 Module 的原始碼可以放在 GOPATH 外,有四種指令 module, require, replace, exclude 可以使用:

// go.mod
module github.com/my/thing

require (
    github.com/some/dependency v1.2.3
    github.com/another/dependency/v4 v4.0.0
)
參數 描述
module 指定 package name or path
require 依賴的 package
replace 取代 package 依賴位置
exclude 忽略 package

module 用來宣告 Module 的身份,並帶入 module 的路徑。在這個 module 中所有匯入的路徑都會以這個 module path 當作前綴(prefix)。透過 module 的路徑,以及 go.mod 到 package's 資料夾的相對路徑,會共同決定 import package's 時要使用的路徑。

replace and execute 這兩個命令都只能用在當前模組(即, main),否則將會在編譯時被忽略。

其他

階層關係上:Module > Package > Directory

跨檔案引用函式

從下面的例子中可以看到,雖然 main.go 裡面有一個函式是定義在 state.go 的檔案中,但因為它們屬於同一個 package,所以當從 Terminal 執行 go run main.go state.go 時,程式可以正確執行。

或者也可以輸入 go run *.go

// main.go
package main

func main() {
  printState()
}
// state.go
package main

import "fmt"

func printState() {
  fmt.Println("California")
}

套件載入的流程

在 golang 中,使用某一個套件時,go 會先去 GOROOT 找看看是不是內建的函式庫,如果找不到的話,會去 GOPATH 內找,如果都找不到的話,就無法使用。

go get vs go install

Deprecation of 'go get' for installing executables 但 Go1.17 開始,如果使用 go get 安裝命令,會警告:

$ go get github.com/github/hub
go get: installing executables with 'go get' in module mode is deprecated.
 To adjust and download dependencies of the current module, use 'go get -d'.
 To install using requirements of the current module, use 'go install'.
 To install ignoring the current module, use 'go install' with a version,
 like 'go install example.com/cmd@latest'.
 For more information, see https://golang.org/doc/go-get-install-deprecation
 or run 'go help get' or 'go help install'.

也就是說,go get 只用來下載普通的包(先前提到的 reusable package) 安裝可執行程序(executable package),應該使用 go install

$ go install github.com/github/hub

go get 有一個 flag -d,指示 go get 下載對應的包,但不做編譯和安裝。 將來的版本,-d 會成爲默認行爲,這樣會更快。此外, 因爲不編譯,即使目標依賴在特定平臺編譯報錯, go get 也能正常執行完。

至於爲什麼用 go install 代替 go get 執行命令安裝,這裏有詳細的說明:https://docs.studygolang.com/doc/go-get-install-deprecation,簡單說就是和命令的語義更符合。

go get

Go get 常用參數 通常在使用 go get 管理 package ,常會看到 -u go get 命令通常也被稱為遠程代碼導入,可以從網路下載及安裝指定的 package 以及相關的依賴代碼,存放在 $GOPATH 的 src 目錄下。 使用 go module 時,go build 使用的 package 則儲存在 $GOPATH/pkg/mod。 支持的 VCS 包括 git, mercurial, subversion, bazaar。

這裡是在下載時,可以使用的參數:

參數 描述
-d 執行下載,但不執行安裝動作。
-f 只有在使用 -u 參數時,才會有效。可以忽略檢查以下載 package 的導入路徑。如果代碼是從其他項目 fork ,就可以採取這樣的做法。
-fix 下載 package 先執行修正在進行編譯安裝。
-insecure 允許使用 http (非安全) 來下載 package,通常是在內部開發環境使用。
-t 下載 package ,在安裝過程同時下載安裝相關依賴代碼包。
-u 強制更新已經下載的 package 以及依賴代碼包。(go get 預設指會不會更新已經下載的package)

廢棄 -insecure

go get 的這個 flag 使用的人可能不多。什麼時候會用到呢? Go1.16 版本關於這個 flag 的說明:

The -insecure flag permits fetching from repositories and resolving custom domains using insecure schemes such as HTTP, and also bypassess module sum validation using the checksum database. Use with caution. This flag is deprecated and will be removed in a future version of go. To permit the use of insecure schemes, use the GOINSECURE environment variable instead. To bypass module sum validation, use GOPRIVATE or GONOSUMDB. See 'go help environment' for details.

這主要用來處理私有倉庫沒有提供 HTTPS 的情況,同時避免進行數據庫校驗和檢查。不過更建議使用 GOINSECURE 環境變量。看看這個環境變量的說明:

GOINSECURE Comma-separated list of glob patterns (in the syntax of Go's path.Match) of module path prefixes that should always be fetched in an insecure manner. Only applies to dependencies that are being fetched directly. Unlike the -insecure flag on 'go get', GOINSECURE does not disable checksum database validation. GOPRIVATE or GONOSUMDB may be used to achieve that.

Go1.17 直接廢棄了 -insecure 這個 flag,必須使用 GOINSECURE 環境變量。 但這個環境變量不會禁用數據庫校驗和檢查。 因此,對於私有倉庫,如果沒有提供 HTTPS,應該配置 GOINSECURE,指明哪些地址啓用 INSECURE 模式,同時配置 GOPRIVATE 環境變量,避免數據庫校驗和檢查。

$ go get -insecure github.com/labstack/echo/v4
go get: -insecure flag is no longer supported; use GOINSECURE instead

Reference

© Kimi Tsai all right reserved.            Updated : 2023-07-12 09:04:53

results matching ""

    No results matching ""

    results matching ""

      No results matching ""