背景?
這是我學習golang的第三天,大致已經掌握了golang的語法,但是著手開發的時候,卻遇到了許多問題,例如golang導包機制、golang的專案管理規範、go mod生成project怎麼管理依賴的等等。其實這些概念之前也瞭解過,但是也只是如蜻蜓點水般的瞭解。正好,現在遇到了這些問題,那便認真總結一番。
問題總結
一個優秀的Go專案的佈局是怎樣的??
這個我在網上搜了很多的資料,不管是部落格還是影片,他們大部分教的是在Go ENV路徑下建立你的project,然後cd到你的project,接著在該專案資料夾下建立bin、src 和pkg目錄。目錄佈局大致如下:
.
├── bin
├── pkg
├── src
│ ├── github.com
│ │ ├── user
│ │ │ └── project1
│ │ └── user
│ │ └── project2
│ ├── main.go
│ └── other.go
├── vendor
├── Makefile
└── README.md
bin
目錄放置編譯生成的可執行檔案。pkg
目錄放置編譯生成的包檔案。src
目錄是原始碼目錄,它包含了專案的所有Go原始碼,外部依賴庫的原始碼也可以放在該目錄下。vendor
目錄儲存了第三方依賴庫的程式碼,類似於其他語言中的node_modules
目錄或pip
的virtualenv
機制。Makefile
包含了專案的構建與管理規則,如編譯、測試、部署等。README.md
檔案包含了專案的說明文件和使用說明。
這種目錄佈局其實還挺清晰的,但是自從go引入go modules做依賴管理,專案佈局結構會變得更精簡,更靈活。具體目錄佈局如下所示:
.
├── cmd
│ └── main.go
├── internal
├── pkg
├── vendor
├── go.mod
└── README.md
cmd
目錄是程式的入口程式碼,即main
函式的實現。internal
目錄用於存放應用程式的私有程式碼,不能被其他專案引用。pkg
目錄用於存放應用程式的公共庫程式碼,可以被其他專案引用。vendor
目錄用於存放依賴庫的程式碼,類似於其他語言中的node_modules
目錄或pip
的virtualenv
機制。go.mod
是 Go modules 的配置檔案,用於管理依賴關係和版本控制。README.md
檔案包含了專案的說明文件和使用說明。
當然根據業務要求,我們還可以新增docs目錄存放專案文件,新增test目錄存放單元測試程式碼。
Tonybai大佬的圖畫的相當不錯,我這邊引用一下,大家看完圖就知道一個Go語言的經典佈局該是什麼樣的了。
ps:圖有點糊,將就一下......
專案結構目錄重點突出一個清晰明瞭,我們需要清楚每個目錄代表的含義是什麼,每個目錄下的檔案有哪些,目錄檔案的呼叫關係,目錄檔案的隱蔽性等等。
為什麼一定是go mod??
我提出這個問題並不是吹毛求疵,而是真心想了解在沒go module管理時,大家不也寫的好好的麼,為什麼go module出來後,大家會立馬拋棄以前的做法。這 go module到底帶來了什麼好處,如此吸引人。
我還是從專案佈局上理解,在沒有go module管理時,大家的專案佈局應該長這樣:
.
├── bin
├── pkg
├── src
│ ├── github.com
│ │ ├── user
│ │ │ └── project1
│ │ └── user
│ │ └── project2
│ ├── main.go
│ └── other.go
├── vendor
├── Makefile
└── README.md
首先在 Go 1.11 版本之前,如果要在 $GOPATH
中執行一個專案,該專案必須存放在 $GOPATH/src
目錄下。佈局裡就包含了project1和project2兩個專案檔案目錄。這會導致你的src目錄越來越腫大。
其次是手動管理依賴問題。在Go 1.11版本之前,Go 語言沒有官方的依賴管理工具,因此在專案中引入外部依賴庫的時候,通常需要手動將依賴庫的程式碼複製到 $GOPATH/src
目錄下。這個光不是手動下載的事,你還要考慮到手動更新,依賴之間的衝突問題,部署的問題等等。在倡導DEVOPS的時代,哪一個都是讓人分心頭痛的事?。
然而,go module解決了上述問題(怪不得大家會極力擁抱這門新技術)。這裡舉個例子說明兩者的差別,讓思路更清晰。
假設有兩個專案 A 和專案 B,都依賴於 Go 語言的一個第三方庫 github.com/gin-gonic/gin
。其中,專案 A 使用傳統的依賴管理方式,專案 B 使用 Go modules 來管理依賴。
一、使用傳統的依賴管理方式的專案 A:
專案 A 的目錄結構如下:
.
├── main.go
└── vendor
└── github.com
└── gin-gonic
└── gin
├── LICENSE
├── README.md
├── bindings
├── contributing.md
├── favicon.ico
├── gin.go
├── go.mod
├── go.sum
├── handlers
├── logger.go
├── middleware.go
├── render
├── router.go
└── vendor
main.go
檔案中引入了 github.com/gin-gonic/gin
:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// ...
}
在專案 A 中,依賴庫 github.com/gin-gonic/gin
的程式碼被存放在 vendor
目錄中,無法和其他專案共享使用。需要說明地是,使用 vendor
目錄來管理依賴庫是 Go 語言在 Go 1.5 版本時引入的方式。在使用 vendor
目錄管理依賴庫的時候,你需要將依賴庫的程式碼複製到專案目錄下的 vendor
目錄下,然後在程式碼中引用這些依賴庫。如果這個依賴庫的版本發生升級,需要手動更新並重新複製程式碼到 vendor
目錄,容易出現版本衝突或遺漏問題。
二、使用 Go modules 的專案 B:
專案 B 的目錄結構如下:
.
├── go.mod
├── main.go
└── vendor
main.go
檔案中同樣引入了 github.com/gin-gonic/gin
:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// ...
}
在專案 B 中,使用 go mod
命令來管理依賴庫和版本,無需手動複製依賴庫程式碼。執行以下命令會自動下載依賴庫程式碼至 $GOPATH/pkg/mod
目錄下:
go mod init example.com/B
go mod tidy
安裝完依賴庫後,可以把 $GOPATH/pkg/mod
目錄下的 github.com/gin-gonic/gin
目錄複製到其他專案中使用,從而實現依賴共享。如果需要升級或切換依賴庫的版本,只需要修改 go.mod
檔案中的版本號即可,不會影響到其他專案。
這裡可能有一個歧義的點,就是“複製到其它專案中使用”這個說法。依我看,go mod會自動安裝你所需要庫的依賴,它會在本地留下快取資訊。那麼如果另一個專案中也需要使用 github.com/gin-gonic/gin
,可以直接在程式碼中引用該依賴庫,如下所示:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// ...
}
只要之前已經在任意一個專案中使用了 go mod
命令下載過並快取了 github.com/gin-gonic/gin
,Go 會自動從快取中載入依賴庫程式碼,而不會重新下載依賴庫程式碼到 $GOPATH/pkg/mod
目錄下。
這種方式可以實現依賴庫的共享,避免了多個專案同時複製依賴庫程式碼,節省了磁碟空間。同時,如果需要使用不同的版本號,可以透過修改 go.mod
檔案來實現對特定版本的依賴管理。
當你修改了 go.mod
檔案中的依賴版本號或者新增了新的依賴項之後,可以使用 go mod tidy
命令來更新依賴關係,例如:
# 修改 go.mod 檔案中的依賴版本號
go mod edit -require github.com/gin-gonic/gin@v1.7.2
# 更新依賴關係
go mod tidy
go mod tidy
會根據 go.mod
檔案中的依賴關係自動下載並更新依賴庫程式碼,以保持依賴關係的一致性,並且會刪除未被引用的依賴項。
另外,如果你還希望移除某一個不再使用的依賴庫,可以使用 go mod tidy -v
命令,它會輸出垃圾收集的詳細資訊,包括哪些依賴項是被移除的。
Go的導包機制是什麼?✌️
我的人生信條就是優雅,如何優雅地導包也是我所追求的。
在瞭解怎麼導包之前,我們先需要了解包在Go裡面的具象化的體現是什麼?包和模組的區別是什麼?
拋開語言,從軟體角度考慮,我們認為子程式的集合稱為包,包的集合稱為模組,模組的集合稱為子系統,子系統的集合稱為系統。將這個說法往Go語言上代入,能發現我們編寫的go檔案就是子程式,go檔案所在資料夾就是包,根目錄的檔名就是模組名。這些具象化的體現還能從哪裡看出來呢?其實還能從我們建立的檔案中看出。
以這樣的目錄結構為例:
project/
|- go.mod
|- main.go
|- controllers/
|- user.go
|- admin.go
其中:
go.mod
是模組檔案,位於專案根目錄。main.go
是入口檔案,位於專案根目錄。controllers/
目錄下有兩個檔案user.go
和admin.go
一般來說,當你檢視go.mod檔案時,能看到第一行寫著
module project
這是告訴你模組名是project。如果以後你的程式碼開源了,別人想引用你的程式碼,首先就是要根據你的模組名再找對應的包名。這和其它語言引包的方式很相似。
module後面的模組名是允許修改的,你可以換成自定義的寫法,通常的寫法是域名+專案名
。
再開啟user.go,你能發現第一行寫著
package controllers
這是指user.go是controllers包下面的一個子程式。
理清楚這些概念,我們還需要記住一個總綱:“導包其實就是定址!導包其實就是定址!導包其實就是定址!”。重要的話說三遍!
還是以上面的目錄結構為例,如果需要在 main.go
中匯入 controllers/user.go
檔案中的程式碼,可以使用相對路徑 ./controllers
來匯入 user.go
檔案。
// main.go
package main
import "./controllers"
func main() {
// ...
}
對於模組,可以使用模組路徑來引用相對路徑下的檔案。例如,假設你的模組路徑為 example.com/mymodule
,則可以在 main.go
中使用 example.com/mymodule/controllers
來引用相對路徑下的 user.go
檔案。
// main.go
package main
import "example.com/mymodule/controllers"
func main() {
// ...
}
需要注意的是,使用相對路徑匯入包時,包的實際路徑是相對於當前檔案所在的目錄。如果在其他檔案中也要使用相對路徑匯入 controllers/user.go
,則需要將路徑設定為相對於這個檔案的路徑。同時,相對路徑只適用於模組內的程式碼,如果要在不同的模組之間匯入程式碼,必須使用完整的包路徑。
Go的測試程式碼怎麼生成??
在 Go 中,我們可以透過建立測試檔案來編寫測試程式碼。測試檔案應該遵循 Go 的命名規則,即在檔名後面加上 _test
。例如,如果要編寫一個名為 sum
的函式的測試程式碼,那麼測試檔案的檔名應該是 sum_test.go
,而被測試的函式前面要加個Test字首。
在測試檔案中,我們可以使用 testing
包提供的一系列函式來編寫測試程式碼,例如 testing.T
的 Error
、Fail
和 Fatal
函式,以及 testing.B
的 ReportAllocs
、ResetTimer
和 StopTimer
等函式。
下面是一個簡單的測試示例,測試 sum
函式:
// sum_test.go
package main
import "testing"
func TestSum(t *testing.T) {
tables := []struct {
a, b, expected int
}{
{1, 1, 2},
{2, 2, 4},
{3, 3, 6},
}
for _, table := range tables {
result := sum(table.a, table.b)
if result != table.expected {
t.Errorf("Sum of %d + %d was incorrect, expected %d but received %d", table.a, table.b, table.expected, result)
}
}
}
在這個測試檔案中,我們首先匯入 testing
包,並定義了一個名為 TestSum
的測試函式。tables
定義了一個結構體切片,其中包含了一組要測試的引數和期望結果。我們遍歷 tables
切片,逐一測試每組資料,判斷計算結果是否和期望的結果一致。如果不一致,我們使用 t.Errorf
函式輸出錯誤資訊。這樣就完成了一個簡單的測試檔案。
要執行測試檔案,可以在專案根目錄下使用 go test
命令執行。Go 會自動查詢並執行所有的測試檔案,並輸出測試結果。測試結果中會顯示測試用例的數量、測試是否透過以及每個測試用例的具體資訊。例如:
$ go test
PASS
ok project 0.008s
在本示例中,我們只有一個測試用例,測試結果判定為 “ PASS ”,表示測試透過了。如果測試失敗,則測試結果會判定為 “ FAIL ”,並輸出具體的錯誤資訊。
參考資料
https://tonybai.com/2022/04/28/the-standard-layout-of-go-project/
寫的這麼詳細,諸位點個贊不過分吧!!!!