農林漁牧網

您現在的位置是:首頁 > 漁業

一個GO程式的結構是怎樣的?

2022-03-08由 TTcome 發表于 漁業

齒形鏈輪二維圖怎麼畫

大家好,我是TT。

程式設計師這個歷史並不算悠久的行當,卻有著一個歷史悠久的傳統,那就是每種程式語言都將一個名為“hello, world”的示例作為這門語言學習的第一個例子,這個傳統始於 20 世紀 70 年代那本大名鼎鼎的由布萊恩·科尼根(Brian W。 Kernighan)與 C 語言之父丹尼斯·裡奇(Dennis M。 Ritchie)合著的《C 程式設計語言》。

一個GO程式的結構是怎樣的?

在這一講中,我們也將遵從傳統,從編寫一個可以打印出“hello, world”的 Go 示例程式開始我們正式的 Go 編碼之旅。我希望透過這個示例程式你能夠對 Go 程式結構有一個直觀且清晰的認識。

在正式開始之前,我要說明一下,我們這節課對你開發 Go 程式時所使用的編輯器工具沒有任何具體的要求。

如果你喜歡使用某個整合開發環境(Integrated Development Environment,IDE),那麼就用你喜歡的 IDE 好了。如果你希望我給你推薦一些好用的 IDE,我建議你試試GoLand或Visual Studio Code(簡稱 VS Code)。GoLand 是知名 IDE 出品公司 JetBrains 針對 Go 語言推出的 IDE 產品,也是目前市面上最好用的 Go IDE;VS Code 則是微軟開源的跨語言原始碼編輯器,透過整合語言外掛(Go 開發者可以使用 Go 官方維護的vscode-go 外掛),可以讓它變成類 IDE 的工具。

如果你有駭客情懷,喜歡像駭客一樣優雅高效地使用命令列,那麼像 Vim、Emacs 這樣的基於終端的編輯器同樣可以用於編寫 Go 原始碼。以 Vim 為例,結合vim-go、coc。nvim(程式碼補全)以及 Go 官方維護的gopls語言伺服器,你在編寫 Go 程式碼時同樣可以體會到“飛一般”的感覺。但在我們這門課中,我們將盡量使用與編輯器或 IDE 無關的說明。

建立“hello,world”示例程式

在 Go 語言中編寫一個可以打印出“hello,world”的示例程式,我們只需要簡單兩步,一是建立資料夾,二是開始編寫和執行。首先,我們來建立一個資料夾儲存編寫的 Go 程式碼。

建立“hello,world”資料夾

通常來說,Go 不會限制我們儲存程式碼的位置(Go 1。11 之前的版本另當別論)。但是針對我們這門課裡的各種練習和專案,我還是建議你建立一個可以集合所有專案的根資料夾(比如:~/goprojects),然後將我們這門課中所有的專案都放在裡面。

現在,你可以開啟終端並輸入相應命令,來建立我們用於儲存“hello,world”示例的資料夾 helloworld 了。對於 Linux 系統、macOS 系統,以及 Windows 系統的 PowerShell 終端來說,用下面這個命令就可以建立 helloworld 檔案夾了:

一個GO程式的結構是怎樣的?

建好資料夾後,我們就要開始編寫我們第一個 Go 程式了。

編寫並執行第一個 Go 程式

首先,我們需要建立一個名為 main。go 的原始檔。

這裡,我需要跟你囉嗦一下 Go 的命名規則。Go 原始檔總是用全小寫字母形式的短小單詞命名,並且以。go 副檔名結尾。

如果要在原始檔的名字中使用多個單詞,我們通常直接是將多個單詞連線起來作為原始檔名,而不是使用其他分隔符,比如下劃線。也就是說,我們通常使用 helloworld。go 作為檔名而不是 hello_world。go。

這是因為下劃線這種分隔符,在 Go 原始檔命名中有特殊作用,這個我們會在以後的講解中詳細說明。總的來說,我們儘量不要用兩個以上的單詞組合作為檔名,否則就很難分辨了。

現在,你可以開啟剛剛建立的 main。go 檔案,鍵入下面這些程式碼:

一個GO程式的結構是怎樣的?

寫完後,我們儲存檔案並回到終端視窗,然後在 Linux 或 macOS 系統中,你就可以透過輸入下面這個命令來編譯和執行這個檔案了:

一個GO程式的結構是怎樣的?

如果是在 Windows 系統中呢,你需要把上面命令中的。/main 替換為。\main。exe。

一個GO程式的結構是怎樣的?

不過,無論你使用哪種作業系統,到這裡你都應該能看到終端輸出的“hello, world”字串了。如果你沒有看到這個輸出結果,要麼是 Go 安裝過程的問題,要麼是原始檔編輯出現了問題,需要你再次認真地確認。如果一切順利,那麼恭喜你!你已經完成了第一個 Go 程式,並正式成為了 Go 開發者!歡迎來到 Go 語言的世界!

“hello,world”示例程式的結構

現在,讓我們回過頭來仔細看看“hello,world”示例程式中到底發生了什麼。第一個值得注意的部分是這個:

一個GO程式的結構是怎樣的?

這一行程式碼定義了 Go 中的一個包 package。包是 Go 語言的基本組成單元,通常使用單個的小寫單詞命名,一個 Go 程式本質上就是一組包的集合。所有 Go 程式碼都有自己隸屬的包,在這裡我們的“hello,world”示例的所有程式碼都在一個名為 main 的包中。main 包在 Go 中是一個特殊的包,整個 Go 程式中僅允許存在一個名為 main 的包。

main 包中的主要程式碼是一個名為 main 的函式:

一個GO程式的結構是怎樣的?

這裡的 main 函式會比較特殊:當你執行一個可執行的 Go 程式的時候,所有的程式碼都會從這個入口函式開始執行。這段程式碼的第一行聲明瞭一個名為 main 的、沒有任何引數和返回值的函式。如果某天你需要給函式宣告引數的話,那麼就必須把它們放置在圓括號 () 中。

另外,那對花括號{}被用來標記函式體,Go 要求所有的函式體都要被花括號包裹起來。按照慣例,我們推薦把左花括號與函式宣告置於同一行並以空格分隔。Go 語言內建了一套 Go 社群約定俗稱的程式碼風格,並隨安裝包提供了一個名為 Gofmt 的工具,這個工具可以幫助你將程式碼自動格式化為約定的風格。

Gofmt 是 Go 語言在解決規模化(scale)問題上的一個最佳實踐,併成為了 Go 語言吸引其他語言開發者的一大賣點。很多其他主流語言也在效仿 Go 語言推出自己的 format 工具,比如:Java formatter、Clang formatter、Dartfmt 等。因此,作為 Go 開發人員,請在提交你的程式碼前使用 Gofmt 格式化你的 Go 原始碼。

好,回到正題,我們再來看一看 main 函式體中的程式碼:

一個GO程式的結構是怎樣的?

這一行程式碼已經完成了整個示例程式的所有工作了:將字串輸出到終端的標準輸出(stdout)上。不過這裡還有幾個需要你注意的細節。

注意點 1:標準 Go 程式碼風格使用 Tab 而不是空格來實現縮排的,當然這個程式碼風格的格式化工作也可以交由 gofmt 完成。

注意點 2:我們呼叫了一個名為 Println 的函式,這個函式位於 Go 標準庫的 fmt 包中。為了在我們的示例程式中使用 fmt 包定義的 Println 函式,我們其實做了兩步操作。

第一步是在原始檔的開始處透過 import 宣告匯入 fmt 包的包路徑:

一個GO程式的結構是怎樣的?

第二步則是在 main 函式體中,透過 fmt 這個限定識別符號(Qualified Identifier)呼叫 Println 函式。雖然兩處都使用了“fmt”這個字面值,但在這兩處“fmt”字面值所代表的含義卻是不一樣的:

import “fmt” 一行中“fmt”代表的是包的匯入路徑(Import),它表示的是標準庫下的 fmt 目錄,整個 import 宣告語句的含義是匯入標準庫 fmt 目錄下的包;

fmt。Println 函式呼叫一行中的“fmt”代表的則是包名。

通常匯入路徑的最後一個分段名與包名是相同的,這也很容易讓人誤解 import 宣告語句中的“fmt”指的是包名,其實並不是這樣的。

main 函式體中之所以可以呼叫 fmt 包的 Println 函式,還有最後一個原因,那就是 Println 函式名的首字母是大寫的。在 Go 語言中,只有首字母為大寫的識別符號才是匯出的(Exported),才能對包外的程式碼可見;如果首字母是小寫的,那麼就說明這個識別符號僅限於在宣告它的包內可見。

另外,在 Go 語言中,main 包是不可以像標準庫 fmt 包那樣被匯入(Import)的,如果匯入 main 包,在程式碼編譯階段你會收到一個 Go 編譯器錯誤:import “xx/main” is a program, not an importable package。

注意點 3:我們還是回到 main 函式體實現上,把關注點放在傳入到 Println 函式的字串“hello, world”上面。你會發現,我們傳入的字串也就是我們執行程式後在終端的標準輸出上看到的字串。

這種“所見即所得”得益於 Go 原始碼檔案本身採用的是 Unicode 字符集,而且用的是 UTF-8 標準的字元編碼方式,這與編譯後的程式所執行的環境所使用的字符集和字元編碼方式是一致的。

這裡,即便我們將程式碼中的“hello, world”換成中文字串“你好,世界”,像下面這樣:

一個GO程式的結構是怎樣的?

我們依舊可以在終端的標準輸出上看到正確的輸出。

最後,不知道你有沒有發現,我們整個示例程式原始碼中,都沒有使用過分號來標識語句的結束,這與 C、C++、Java 那些傳統編譯型語言好像不太一樣呀?

不過,其實 Go 語言的正式語法規範是使用分號“;”來做結尾識別符號的。那為什麼我們很少在 Go 程式碼中使用和看到分號呢?這是因為,大多數分號都是可選的,常常被省略,不過在原始碼編譯時,Go 編譯器會自動插入這些被省略的分號。

我們給上面的“hello,world”示例程式加上分號也是完全合法的,是可以直接透過 Go 編譯器編譯並正常執行的。不過,gofmt 在按約定格式化程式碼時,會自動刪除這些被我們手工加入的分號的。

在分析完這段程式碼結構後,我們來講一下 Go 語言的編譯。雖然剛剛你應該已經執行過“hello, world”這個示例程式了,在這過程中,有一個重要的步驟——編譯,現在我就帶你來看看 Go 語言中程式是怎麼進行編譯的。

Go 語言中程式是怎麼編譯的?

你應該也注意到了,剛剛我在執行“hello, world”程式之前,輸入了 go build 命令,還有它附帶的原始檔名引數來編譯它:

一個GO程式的結構是怎樣的?

假如你曾經有過 C/C++ 語言的開發背景,那麼你就會發現這個步驟與 gcc 或 clang 編譯十分相似。一旦編譯成功,我們就會獲得一個二進位制的可執行檔案。在 Linux 系統、macOS 系統,以及 Windows 系統的 PowerShell 中,我們可以透過輸入下面這個 ls 命令看到剛剛生成的可執行檔案:

一個GO程式的結構是怎樣的?

上面顯示的檔案裡面有我們剛剛建立的、以。go 為字尾的原始碼檔案,還有剛生成的可執行檔案(Windows 系統下為 main。exe,其餘系統下為 main)。

如果你之前更熟悉某種類似於 Ruby、Python 或 JavaScript 之類的動態語言,你可能還不太習慣在執行之前需要先進行編譯的情況。Go 是一種編譯型語言,這意味著只有你編譯完 Go 程式之後,才可以將生成的可執行檔案交付於其他人,並執行在沒有安裝 Go 的環境中。

而如果你交付給其他人的是一份。rb、。py 或。js 的動態語言的原始檔,那麼他們的目標環境中就必須要擁有對應的 Ruby、Python 或 JavaScript 實現才能解釋執行這些原始檔。

當然,Go 也借鑑了動態語言的一些對開發者體驗較好的特性,比如基於原始碼檔案的直接執行,Go 提供了 run 命令可以直接執行 Go 原始碼檔案,比如我們也可以使用下面命令直接基於 main。go 執行:

一個GO程式的結構是怎樣的?

當然像 go run 這類命令更多用於開發除錯階段,真正的交付成果還是需要使用 go build 命令構建的。

但是在我們的生產環境裡,Go 程式的編譯往往不會像我們前面,基於單個 Go 原始檔構建類似“hello,world”這樣的示例程式那麼簡單。越貼近真實的生產環境,也就意味著專案規模越大、協同人員越多,專案的依賴和依賴的版本都會變得複雜。

那在我們更復雜的生產環境中,go build 命令也能圓滿完成我們的編譯任務嗎?我們現在就來探討一下。

複雜專案下 Go 程式的編譯是怎樣的

我們還是直接上專案吧,給 go build 一個機會,看看它的複雜依賴管理到底怎麼樣。

現在我們建立一個新專案“hellomodule”,在新專案中我們將使用兩個第三方庫,zap 和 fasthttp,給 go build 的構建過程增加一些難度。和“hello,world”示例一樣,我們透過下面命令建立“hellomodule”專案:

一個GO程式的結構是怎樣的?

接著,我們在“hellomodule“下建立並編輯我們的示例原始碼檔案:

一個GO程式的結構是怎樣的?

這個示例建立了一個在 8081 埠監聽的 http 服務,當我們向它發起請求後,這個服務會在終端標準輸出上輸出一段訪問日誌。

你會看到,和“hello,world“相比,這個示例顯然要複雜許多。但不用擔心,你現在大可不必知道每行程式碼的功用,你只需要我們在這個稍微有點複雜的示例中引入了兩個第三方依賴庫,zap 和 fasthttp 就可以了。

我們嘗試一下使用編譯“hello,world”的方法來編譯“hellomodule”中的 main。go 原始檔,go 編譯器的輸出結果是這樣的:

一個GO程式的結構是怎樣的?

看這結果,這回我們運氣似乎不佳,main。go 的編譯失敗了!

從編譯器的輸出來看,go build 似乎在找一個名為 go。mod 的檔案,來解決程式對第三方包的依賴決策問題。

好了,我們也不打啞謎了,是時候讓 Go module 登場了!

Go module 構建模式是在 Go 1。11 版本正式引入的,為的是徹底解決 Go 專案複雜版本依賴的問題,在 Go 1。16 版本中,Go module 已經成為了 Go 預設的包依賴管理機制和 Go 原始碼構建機制。

Go Module 的核心是一個名為 go。mod 的檔案,在這個檔案中儲存了這個 module 對第三方依賴的全部資訊。接下來,我們就透過下面命令為“hello,module”這個示例程式新增 go。mod 檔案:

一個GO程式的結構是怎樣的?

你會看到,go mod init 命令的執行結果是在當前目錄下生成了一個 go。mod 檔案:

一個GO程式的結構是怎樣的?

其實,一個 module 就是一個包的集合,這些包和 module 一起打版本、釋出和分發。go。mod 所在的目錄被我們稱為它宣告的 module 的根目錄。

不過呢,這個時候的 go。mod 檔案內容還比較簡單,第一行內容是用於宣告 module 路徑(module path)的。而且,module 隱含了一個名稱空間的概念,module 下每個包的匯入路徑都是由 module path 和包所在子目錄的名字結合在一起構成。

比如,如果 hellomodule 下有子目錄 pkg/pkg1,那麼 pkg1 下面的包的匯入路徑就是由 module path(github。com/bigwhite/hellomodule)和包所在子目錄的名字(pkg/pkg1)結合而成,也就是 github。com/bigwhite/hellomodule/pkg/pkg1。

另外,go。mod 的最後一行是一個 Go 版本指示符,用於表示這個 module 是在某個特定的 Go 版本的 module 語義的基礎上編寫的。

有了 go.mod 後,是不是我們就可以構建 hellomodule 示例了呢?

來試試看!我們執行一下構建,Go 編譯器輸出結果是這樣的:

一個GO程式的結構是怎樣的?

你會看到,Go 編譯器提示原始碼依賴 fasthttp 和 zap 兩個第三方包,但是 go。mod 中沒有這兩個包的版本資訊,我們需要按提示手工新增資訊到 go。mod 中。

這個時候,除了按提示手動新增外,我們也可以使用 go mod tidy 命令,讓 Go 工具自動新增:

一個GO程式的結構是怎樣的?

從輸出結果中,我們看到 Go 工具不僅下載並添加了 hellomodule 直接依賴的 zap 和 fasthttp 包的資訊,還下載了這兩個包的相關依賴包。go mod tidy 執行後,我們 go。mod 的最新內容變成了這個樣子:

一個GO程式的結構是怎樣的?

這個時候,go。mod 已經記錄了 hellomodule 直接依賴的包的資訊。不僅如此,hellomodule 目錄下還多了一個名為 go。sum 的檔案,這個檔案記錄了 hellomodule 的直接依賴和間接依賴包的相關版本的 hash 值,用來校驗本地包的真實性。在構建的時候,如果本地依賴包的 hash 值與 go。sum 檔案中記錄的不一致,就會被拒絕構建。

有了 go。mod 以及 hellomodule 依賴的包版本資訊後,我們再來執行構建:

一個GO程式的結構是怎樣的?

這次我們成功構建出了可執行檔案 main,執行這個檔案,新開一個終端視窗,在新視窗中使用 curl 命令訪問該 http 服務:curl localhost:8081/foo/bar,我們就會看到服務端輸出如下日誌:

一個GO程式的結構是怎樣的?

這下,我們的“ hellomodule”程式可算建立成功了。我們也看到使用 Go Module 的構建模式,go build 完全可以承擔其構建規模較大、依賴複雜的 Go 專案的重任。還有更多關於 Go Module 的內容,我會在第 7 節課再詳細跟你講解。

到這裡,我們終於親手編寫完成了 Go 語言的第一個程式“hello, world”,我們終於知道一個 Go 程式長成啥樣子了,這讓我們在自己的 Go 旅程上邁出了堅實的一步!

在這一節課裡,我們透過 helloworld 示例程式,瞭解了一個 Go 程式的原始碼結構與程式碼風格自動格式化的約定。

我希望你記住這幾個要點:

Go 包是 Go 語言的基本組成單元。一個 Go 程式就是一組包的集合,所有 Go 程式碼都位於包中;

Go 原始碼可以匯入其他 Go 包,並使用其中的匯出語法元素,包括型別、變數、函式、方法等,而且,main 函式是整個 Go 應用的入口函式;

Go 原始碼需要先編譯,再分發和執行。如果是單 Go 原始檔的情況,我們可以直接使用 go build 命令 +Go 原始檔名的方式編譯。不過,對於複雜的 Go 專案,我們需要在 Go Module 的幫助下完成專案的構建。

歡迎你把這節課分享給更多對 Go 語言學習感興趣的朋友。