跳至主要内容

[Golang] Modules and Packages

How to Write Go Code @ golang

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

# 在 hello 資料夾中
$ go mod init example.com/user/hello # 宣告 module_path,通常會和該 repository 的 url 位置一致
$ go install . # 編譯該 module 並將執行檔放到 GOBIN,因此在 GOBIN 資料夾中會出現 hello 的執行檔

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

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

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

設定 GOPATH

參考:Go 起步安裝設定 > 設定 GOPATH

# ~/.zshrc
export GOPATH=$HOME/go

# 可以直接在 Terminal 執行 GOPATH 中的 bin
export PATH=$PATH:$GOPATH/bin

Packages

Package Scope:同一個 Package 來的變數和方法是共享的

  • 在 Go 語言中,並沒有區分 publicprivateprotected,而是根據變數名稱的第一個字母大小寫來判斷能否被外部引用。
  • 在同一個 package 中變數、函式、常數和 type,都隸屬於同一個 package scope,因此雖然可能在不同支檔案內,但只要隸屬於同一個 package,都可以使用(visible)。
  • 如果需要讓 package 內的變數或函式等能夠在 package 外部被使用,則該變數的第一個字母要大寫才能讓外部引用(Exported names),否則的話會無法使用

舉例來說,有兩隻檔案,他們在同一個資料夾內,且都屬於 main 這個 package:

// ---
// greetings.go
// ---
package main

import "fmt"

var points = []int{20, 90, 100, 45, 70}

func sayHello(name string) {
fmt.Println("Hello", name)
}

// ---
// main.go
// ---
package main

import "fmt"

func main() {
sayHello("Aaron")

for _, v := range points {
fmt.Println(v)
}
}

雖然 pointssayHello 是在 greetings.go 這支檔案,但在 main.go 這支檔案,是可以取用到(visible)這些變數的。

要留意執行程式的時候要寫:

$ go run main.go greetings.go

留意變數是在 Package Scope 或 Function Scope

要留意 package scope 和 function scope 的差別。假設有兩隻檔案 main.goscore.go,當中有一變數叫 score

// ---
// score.go
// ---

package main

import "fmt"

func showScore() {
// 在 score.go 中,並沒有 score 這個變數
fmt.Println(score)
}

// ---
// main.go
// ---
package main

// score 這個變數是在 package scope
var score int = 87

func main() {
// showScore 仍然可以取得 score 這個變數
showScore()
}
信息

即時 score 這個變數不是在 main.go 中,只要它是在 main 這個 package scope 下,就可以被取用到,不管 showScore 是在哪裡被執行。

然而,如果是把 score 這個變數放在 Function Scope 下,例如:

// main.go
package main

func main() {
// 如果變數放在 function scope 內,雖然 showScore 是在同一個 function 喜愛被執行
// 但還是會無法取用到 score 這個變數
var score int = 87
showScore()
}

Package Name and Folder Structure

  • Go 的程式碼都是以 package 的方式在組織,一個 package 就是在「同一資料夾」中的許多 GO 檔案。

  • 同一個 package 的所有 Go 檔案會放在同一個資料夾內;而這個資料夾內的所有檔案也都會屬於同一個 package,有相同的 package 名稱

  • 通常 package 的名稱會跟著 folder 的名稱,舉例來說,若檔案放在 math/rand 資料夾中,則該套件會稱作 rand

  • 有兩種不同類型的 package:

    • executable package:是用來產生我們可以執行的檔案,一定會包含 package main 並且帶有名為 main 的函式,只有這個檔案可以被執行(run)和編譯(build),並且不能被其他檔案給匯入。
    • 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),例如 timelist、或 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

# 初始化 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 中,go import package 的路徑會是 module path 加上 subdirectory
  • 在一個專案中通常只會有一個 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
// go.mod
module github.com/my/thing

require (
github.com/some/dependency v1.2.3
github.com/another/dependency/v4 v4.0.0
)

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 內找,如果都找不到的話,就無法使用。

參考