笨辦法學 Golang(2):Go包基礎

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

現如今即便是個人開發的一般程式,可能其包含的函數都超過了一萬個,這些函數代碼一般都由他人編寫並打包為“包”或者“模組”的形式,並通過相關社區分發,最後由軟體作者通過調用這些“包”或“模組”的函數來更高效地完成開發。因此在大部分時候,作者只會用到其中很小的一部分函數,但是通過“包”或“模組”的形式重用代碼使得編程開發變得更加輕鬆,這也是現如今大部分程式設計語言都有自己的包管理工具和包分發渠道的原因。

在學習Go語言過程中,我們幾乎每個例子都使用到了Go語言套件,例如像fmt、os等這樣具有常用功能的內建包在Go語言中有一百多個,我們習慣稱之為標準包(標準庫),這些標準包大部分都內建到Go本身。(完整列表可以在Go Walker查看,或者使用go list std命令查看標準包列表)。

在本文的學習中,將包含以下知識點:

  • 包結構認識
  • 包的使用基礎
  • 常用標準包詳解
  • 使用自訂包

包的基礎

在大部分程式設計語言中都存在“包”概念,而任何一種包設計的目的都是為了簡化大型軟體設計和維護的工作,實際上包是函數和資料的集合,通過把一組有相關特性的函數和資料放進一個單元中,方便使用和更新相應的模組。這種包系統的設計使得每一個模組(包)都與程式、其他單元(其他包)保持一定的獨立性,這使得每一個模組(包)可以被應用到不同的程式部分中,當然也包括其他程式中,甚至可以通過社區分發渠道流通到世界各地的項目中被不斷重複利用,不僅降低了項目模組之間的耦合度,也提高了整體的開發效率。

在編寫代碼過程中,不同模組(包)之間為了實現某一個類似的功能可能會採用相同的名字去命名一個函數,如果一個軟體開發過程中需要同時使用兩個模組(包),就會在調用函數時產生歧義。為瞭解決這個問題Go語言引入了命名空間的概念。讓每個包都定義一個命名空間,用於內部標識符的訪問。因為每個命名空間關聯一個特定的包,這使得我們在調用類型、函數時有了獨一無二的簡短明了的名字,避免在使用它們的時候產生命名衝突。

為了提高包的獨立性以及安全性,Go語言的包可以通過控制包內名字的可見度來實現包的封裝,通過限制包成員的可見度、隱藏具體的實現過程可以極大提高軟體的安全性,同時開發人員調用時也不必關心其實現過程,直接使用包的API,另一方面也允許包的維護者在不影響包的使用者使用的前提下調整包的內部實現。通過限制包內變數的可見度還可以強制使用者通過某些特定函數來訪問和更新內部變數,這樣可以保證內部變數的一致性和並發時的互斥約束。

與大部分編譯語言類似,在Go語言中,當我們改動了一個源檔案,就必須重新編譯該源檔案,以及它對應的包和所有依賴該包的其他包。但即使是從頭構建,Go語言編譯器的編譯速度也明顯快於絕大部分編譯語言。如此優異的編譯速度主要得益於其包設計的三個特性。

  1. 顯式聲明:所有匯入的包必須在每個檔案的開頭顯式聲明,這樣的話編譯器就沒有必要讀取和分析整個源檔案來判斷包的依賴關係。
  2. 無環依賴:禁止包的環狀依賴,因為沒有循環相依性,包的依賴關係形成一個有向非循環圖,每個包可以被獨立編譯,而且很可能是被並發編譯。
  3. 無需遍曆:編譯後包的目標檔案不僅僅記錄包本身的匯出資訊,目標檔案同時還記錄了包的依賴關係。因此,在編譯一個包的時候,編譯器只需要讀取每個直接匯入包的目標檔案,而不需要遍曆所有依賴的的檔案,畢竟很多都是重複的間接依賴。

1.1 包的結構

在前面的學習中我們就已經知道Go語言編譯工具對源碼目錄有很嚴格的要求,每個工作空間(workspace)必須由bin、pkg、src三個目錄組成。bin目錄主要存放可執行檔;pkg目錄存放編譯好的庫檔案,主要是*.a檔案;src目錄下主要存放go的源檔案。

.|-- bin|   |-- goimports|   `-- gophernotes|-- pkg|   `-- linux_amd64|       |-- github.com|       |   |-- gopherds|       |   |   `-- gophernotes|       |   |       |-- internal|       |   |       |   `-- repl.a|       ... ...`-- src    |-- github.com    |   |-- gopherds    ... ...

1. 工作空間

因為Go語言採用了工作空間這種方式來管理本地代碼,這與大部分程式設計語言不一樣,因此這裡解釋一下GOROOT和GOPATH之間的關係。首先顯而易見的一點就是GOROOT是一個全域並且唯一的變數,用於指定存放Go語言本身的目錄路徑(安裝路徑);而GOPATH是一個工作空間的變數,它可以有很多個(用;號分隔),用於指定工作空間的目錄路徑。例如:

GOPATH=$HOME/workspace/golib:$HOME/projects/go

通常go get會使用第一個工作空間儲存下載的第三方庫(包),在開發時不管是哪一個工作空間下載的包都可以在任意工作空間使用。注意一點就是盡量不要把GOROOT和GOPATH設定為同一個路徑。

包的原始碼書寫與正常的Go語言沒有區別,檔案必須是UTF-8格式,編寫規範與Go語言編程規範一致。

2. 包的源檔案

包的代碼必須全部都放在包中,並且源檔案頭部都必須一致使用package <name>的語句作聲明。Go包可以由多個檔案組成,所以檔案名稱不需要與包名一致,包名建議使用小寫字元。包名類似命名空間(namespace),與包所在目錄、編譯檔案名稱無關,目錄名盡量不要使用保留名稱(main、all、std),對於可執行檔必須包含package main以及入口函數main。

Go語言使用名稱首字母大小寫來判斷一個對象(全域變數、全域常量、類型、結構欄位、函數、方法)的存取權限,對於包而言同樣如此。包中成員名稱首字母大小寫決定了該成員的存取權限。首字母大寫,可被包外訪問,即為public(公開的);首字母小寫,則僅包內成員可以訪問,即為internal(內部的)。

與大部分現代程式設計語言一樣,Go語言同樣支援使用UTF-8字元來命名物件,因此關於“大寫”這個概念不限於US ASCII,它被擴充到了所有大小寫字母表(包括拉丁文、希臘文、斯拉夫文、亞美尼亞文和埃及古文等)。漢字一般沒有大小寫概念(除了漢字數字),因此如果你使用漢字作為一個函數的名稱,該函數預設是私人的,你需要在漢字前面加上一個大寫字母才能使其變為公有函數。

3. 包的聲明

上面提到每一個包內源檔案都需要在開頭聲明所在包,這其實就是包的聲明。包的聲明對於包內而言主要用於源檔案編譯時間能夠為編譯器指明哪些是包的原始碼;對於包外而言,在匯入包的時候可以使用“包名.函數名”的方式使用包內函數。

對於包名相同的情況,例如math/rand包和crypto/rand包的包名都是rand,Go語言也有相應的辦法解決,在下一節匯入包時我們會介紹。

關於包的聲明有一個例外,那就是包編譯後是一個可執行程式時,我們會使用package main的方式聲明main包,這時候main包本身的匯入路徑是無關緊要的,這個名字實際是給go build構建命令一個資訊,這個包編譯完之後必須調用連接器產生一個可執行程式。(本文暫不討論測試包的情況)

1.2 包的匯入

就如前面闡述的一樣,使用包成員之前先要匯入包。匯入包的關鍵字是import,因為Go語言套件不能形成環形依賴,如果遇到匯入包循環相依性的情況,Go語言的構建工具將返回錯誤。一般而言對於直接從分發渠道下載回來的包都不會輕易產生依賴環。

import "相對目錄/包主檔案名"

相對目錄是指從<workspace>/pkg/<os_arch>開始的子目錄,以標準庫為例:

import "fmt" // 對應/usr/local/go/pkg/linux_amd64/fmt.aimport "os/exec" // 對應/usr/local/go/pkg/linux_amd64/os/exec.a

除了一行一個包的方式匯入,還可以使用一條語句匯入多個包的寫法:

import (    "fmt"    "os/exec")

1. 匯入聲明

在上一節中我們有一個問題沒有解決,就是同名包匯入時會有衝突。雖然包的命名空間解決了函數重名的情況,但是沒有避免包重名的情況,因此Go語言在匯入包時可以對包名作重新導向,以解決包名衝突的情況。例如下面的幾種例子:

import   "crypto/rand" // 預設模式: rand.Functionimport R "crypto/rand" // 包重新命名: R.Functionimport . "crypto/rand" // 簡便模式: Functionimport _ "crypto/rand" // 匿名匯入: 僅讓該包執行行初始化函數。

另一種寫法:

import (    "crypto/rand"    mrand "math/rand" // 包重新命名)

注意:

  1. Go語言不允許匯入包卻又不使用,如果匯入的包並未使用,在編譯時間會被視為錯誤(不包括 "import _")。
  2. 包的重新命名不僅可以用於解決包名衝突,還可以解決包名過長、避免與變數或常量名稱衝突等情況。

除了以上比較常見的包匯入方式,還有子包匯入方式以及自訂路徑導包方式。其中對於目前的目錄下的子包,除使用預設完整匯入路徑外,還可使用相對路徑的方式。

.└── src    └── test        ├── main.go        └── test2            └── test.go3 directories, 2 files

如上面的目錄結構,可以在main.go檔案中使用下面的方式匯入test2這個包:

import "test/test2" // 一般我們使用這種方式匯入import "./test2" // 也可以使用相對目錄,但這種方式匯入的包僅對go run main.go有效。

如果在一個檔案中匯入的包比較多,為了管理原始碼中匯入的包,還可以為匯入的包分組。分組是通過空行來分隔的,例如:

import (    "fmt"    "html/template"    "os"    "golang.org/x/net/html"    "golang.org/x/net/ipv4")

我們知道Go語言編譯的時候會格式化代碼,因此匯入包的順序並不需要我們調整,編譯時間會自動按字母排序。我們可以調整的只有包分組,同樣每一個分組內的包在編譯時間會被格式化為按字母排序。

2. 匯入路徑

當前Go語言的規範並沒有強制包的匯入路徑字串的格式,匯入路徑由構建工具來解釋。但如果你打算分享或發布你編寫的包,那麼最好使用全球唯一的匯入路徑。

這主要是為了避免匯入路徑衝突,因此有一個約定俗成的路徑格式是:所有非標準庫包的匯入路徑以所在組織的互連網網域名稱為首碼,這樣一來就有了一個獨一無二的路徑,另一方面也有利於包的檢索。

例如下面匯入的包中就有兩個使用了互連網網域名稱為首碼:

import (    "fmt"    "math/rand"    "encoding/json"    "golang.org/x/net/html"    "github.com/go-sql-driver/mysql")

3. 自訂路徑

在上面一節中我們使用了一種網域名稱為首碼的匯入路徑,對於編譯器來說,只有較為流行的代碼託管網站才可以直接使用這種路徑。對於一些個人網站(例如企業自己搭建的私人GitLab倉庫),為了可以更方便使用這種方式匯入就需要告訴編譯器這是一個包代碼連結。

我們有三種方式實現這個功能,一是直接在包連結中加上VCS格式,目前支援的格式有:

Bazaar      .bzrGit         .gitMercurial   .hgSubversion  .svn

例如:

import "example.org/user/foo.git"

第二種辦法是針對沒有提供版本控制符的連結,go get甚至不知道應該如何下載代碼的情況,例如下面這種連結:

example.org/repo/foo

這個時候就需要在網頁中加入一句標籤:

<meta name="go-import" content="import-prefix vcs repo-root">

然後就可以使用連結匯入:

import "example.org/pkg/foo"

第三種情況是重新導向網頁連結:

例如下面的情況,go get訪問連結時會被重新導向到example.org/r/p/exproj。

<meta name="go-import" content="example.org git https://example.org/r/p/exproj">

如果你沒有伺服器還可以使用Go語言搭建一個簡單的本機伺服器:

package mainimport (    "fmt"    "net/http")func handler(w http.ResponseWriter, r *http.Request) {    fmt.Fprint(w, `<meta name="go-import"    content="example.com/zuolan/test git https://github.com/zuolan/test">`)}func main() {    http.HandleFunc("/zuolan/test", handler)    http.ListenAndServe(":80", nil)}

儲存為server.go,然後編譯執行,就可以實現把example.com/zuolan/test重新導向到github.com/zuolan/test。

改動網頁(這也是官網的方法),在

1.3 包的使用

為了更好理解包匯入的細節,在本節中將建立一個包,這個包很簡單,首先在工作空間建立一個項目test:

mkdir $GOPATH/src/test

在src/test中建立一個檔案如下:

package test// 公開函數func Even(i int) bool {      return i % 2 == 0}// 私人函數func odd (i int) bool {      return i % 2 == 1}

然後儲存為test.go檔案,最後使用go build和go install命令編譯和安裝這個包。現在我們有了一個包,接下來看如何匯入剛才建立的包,應用於新的程式之中。

建立一個檔案,就叫做main.go吧(當然也可以命名為其他名字):

package main// 下面匯入了當地套件test和官方標準包fmtimport (    "test"    "fmt")// 調用test包中的Even函數。訪問一個包中的函數的文法是package.Function()。func main() {      i := 5      fmt.Printf("Is %d even? %v\n", i, test.Even(i))}

在上面的例子中,如果使用了odd這個函數,在編譯的時候就會報錯,因為在test包中定義了odd函數為私人函數,不能被外部存取。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.