W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
原文鏈接:https://gopl-zh.github.io/ch2/ch2-06.html
Go語言中的包和其他語言的庫或模塊的概念類似,目的都是為了支持模塊化、封裝、單獨(dú)編譯和代碼重用。一個(gè)包的源代碼保存在一個(gè)或多個(gè)以.go為文件后綴名的源文件中,通常一個(gè)包所在目錄路徑的后綴是包的導(dǎo)入路徑;例如包gopl.io/ch1/helloworld對應(yīng)的目錄路徑是$GOPATH/src/gopl.io/ch1/helloworld。
每個(gè)包都對應(yīng)一個(gè)獨(dú)立的名字空間。例如,在image包中的Decode函數(shù)和在unicode/utf16包中的 Decode函數(shù)是不同的。要在外部引用該函數(shù),必須顯式使用image.Decode或utf16.Decode形式訪問。
包還可以讓我們通過控制哪些名字是外部可見的來隱藏內(nèi)部實(shí)現(xiàn)信息。在Go語言中,一個(gè)簡單的規(guī)則是:如果一個(gè)名字是大寫字母開頭的,那么該名字是導(dǎo)出的(譯注:因?yàn)闈h字不區(qū)分大小寫,因此漢字開頭的名字是沒有導(dǎo)出的)。
為了演示包基本的用法,先假設(shè)我們的溫度轉(zhuǎn)換軟件已經(jīng)很流行,我們希望到Go語言社區(qū)也能使用這個(gè)包。我們該如何做呢?
讓我們創(chuàng)建一個(gè)名為gopl.io/ch2/tempconv的包,這是前面例子的一個(gè)改進(jìn)版本。(這里我們沒有按照慣例按順序?qū)舆M(jìn)行編號,因此包路徑看起來更像一個(gè)真實(shí)的包)包代碼存儲在兩個(gè)源文件中,用來演示如何在一個(gè)源文件聲明然后在其他的源文件訪問;雖然在現(xiàn)實(shí)中,這樣小的包一般只需要一個(gè)文件。
我們把變量的聲明、對應(yīng)的常量,還有方法都放到tempconv.go源文件中:
gopl.io/ch2/tempconv
// Package tempconv performs Celsius and Fahrenheit conversions.
package tempconv
import "fmt"
type Celsius float64
type Fahrenheit float64
const (
AbsoluteZeroC Celsius = -273.15
FreezingC Celsius = 0
BoilingC Celsius = 100
)
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }
轉(zhuǎn)換函數(shù)則放在另一個(gè)conv.go源文件中:
package tempconv
// CToF converts a Celsius temperature to Fahrenheit.
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
// FToC converts a Fahrenheit temperature to Celsius.
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
每個(gè)源文件都是以包的聲明語句開始,用來指明包的名字。當(dāng)包被導(dǎo)入的時(shí)候,包內(nèi)的成員將通過類似tempconv.CToF的形式訪問。而包級別的名字,例如在一個(gè)文件聲明的類型和常量,在同一個(gè)包的其他源文件也是可以直接訪問的,就好像所有代碼都在一個(gè)文件一樣。要注意的是tempconv.go源文件導(dǎo)入了fmt包,但是conv.go源文件并沒有,因?yàn)檫@個(gè)源文件中的代碼并沒有用到fmt包。
因?yàn)榘墑e的常量名都是以大寫字母開頭,它們可以像tempconv.AbsoluteZeroC這樣被外部代碼訪問:
fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"
要將攝氏溫度轉(zhuǎn)換為華氏溫度,需要先用import語句導(dǎo)入gopl.io/ch2/tempconv包,然后就可以使用下面的代碼進(jìn)行轉(zhuǎn)換了:
fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"
在每個(gè)源文件的包聲明前緊跟著的注釋是包注釋(§10.7.4)。通常,包注釋的第一句應(yīng)該先是包的功能概要說明。一個(gè)包通常只有一個(gè)源文件有包注釋(譯注:如果有多個(gè)包注釋,目前的文檔工具會根據(jù)源文件名的先后順序?qū)⑺鼈冩溄訛橐粋€(gè)包注釋)。如果包注釋很大,通常會放到一個(gè)獨(dú)立的doc.go文件中。
練習(xí) 2.1: 向tempconv包添加類型、常量和函數(shù)用來處理Kelvin絕對溫度的轉(zhuǎn)換,Kelvin 絕對零度是?273.15°C,Kelvin絕對溫度1K和攝氏度1°C的單位間隔是一樣的。
在Go語言程序中,每個(gè)包都有一個(gè)全局唯一的導(dǎo)入路徑。導(dǎo)入語句中類似"gopl.io/ch2/tempconv"的字符串對應(yīng)包的導(dǎo)入路徑。Go語言的規(guī)范并沒有定義這些字符串的具體含義或包來自哪里,它們是由構(gòu)建工具來解釋的。當(dāng)使用Go語言自帶的go工具箱時(shí)(第十章),一個(gè)導(dǎo)入路徑代表一個(gè)目錄中的一個(gè)或多個(gè)Go源文件。
除了包的導(dǎo)入路徑,每個(gè)包還有一個(gè)包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的聲明處指定。按照慣例,一個(gè)包的名字和包的導(dǎo)入路徑的最后一個(gè)字段相同,例如gopl.io/ch2/tempconv包的名字一般是tempconv。
要使用gopl.io/ch2/tempconv包,需要先導(dǎo)入:
gopl.io/ch2/cf
// Cf converts its numeric argument to Celsius and Fahrenheit.
package main
import (
"fmt"
"os"
"strconv"
"gopl.io/ch2/tempconv"
)
func main() {
for _, arg := range os.Args[1:] {
t, err := strconv.ParseFloat(arg, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "cf: %v\n", err)
os.Exit(1)
}
f := tempconv.Fahrenheit(t)
c := tempconv.Celsius(t)
fmt.Printf("%s = %s, %s = %s\n",
f, tempconv.FToC(f), c, tempconv.CToF(c))
}
}
導(dǎo)入語句將導(dǎo)入的包綁定到一個(gè)短小的名字,然后通過該短小的名字就可以引用包中導(dǎo)出的全部內(nèi)容。上面的導(dǎo)入聲明將允許我們以tempconv.CToF的形式來訪問gopl.io/ch2/tempconv包中的內(nèi)容。在默認(rèn)情況下,導(dǎo)入的包綁定到tempconv名字(譯注:指包聲明語句指定的名字),但是我們也可以綁定到另一個(gè)名稱,以避免名字沖突(§10.4)。
cf程序?qū)⒚钚休斎氲囊粋€(gè)溫度在Celsius和Fahrenheit溫度單位之間轉(zhuǎn)換:
$ go build gopl.io/ch2/cf
$ ./cf 32
32°F = 0°C, 32°C = 89.6°F
$ ./cf 212
212°F = 100°C, 212°C = 413.6°F
$ ./cf -40
-40°F = -40°C, -40°C = -40°F
如果導(dǎo)入了一個(gè)包,但是又沒有使用該包將被當(dāng)作一個(gè)編譯錯(cuò)誤處理。這種強(qiáng)制規(guī)則可以有效減少不必要的依賴,雖然在調(diào)試期間可能會讓人討厭,因?yàn)閯h除一個(gè)類似log.Print("got here!")的打印語句可能導(dǎo)致需要同時(shí)刪除log包導(dǎo)入聲明,否則,編譯器將會發(fā)出一個(gè)錯(cuò)誤。在這種情況下,我們需要將不必要的導(dǎo)入刪除或注釋掉。
不過有更好的解決方案,我們可以使用golang.org/x/tools/cmd/goimports導(dǎo)入工具,它可以根據(jù)需要自動添加或刪除導(dǎo)入的包;許多編輯器都可以集成goimports工具,然后在保存文件的時(shí)候自動運(yùn)行。類似的還有g(shù)ofmt工具,可以用來格式化Go源文件。
練習(xí) 2.2: 寫一個(gè)通用的單位轉(zhuǎn)換程序,用類似cf程序的方式從命令行讀取參數(shù),如果缺省的話則是從標(biāo)準(zhǔn)輸入讀取參數(shù),然后做類似Celsius和Fahrenheit的單位轉(zhuǎn)換,長度單位可以對應(yīng)英尺和米,重量單位可以對應(yīng)磅和公斤等。
包的初始化首先是解決包級變量的依賴順序,然后按照包級變量聲明出現(xiàn)的順序依次初始化:
var a = b + c // a 第三個(gè)初始化, 為 3
var b = f() // b 第二個(gè)初始化, 為 2, 通過調(diào)用 f (依賴c)
var c = 1 // c 第一個(gè)初始化, 為 1
func f() int { return c + 1 }
如果包中含有多個(gè).go源文件,它們將按照發(fā)給編譯器的順序進(jìn)行初始化,Go語言的構(gòu)建工具首先會將.go文件根據(jù)文件名排序,然后依次調(diào)用編譯器編譯。
對于在包級別聲明的變量,如果有初始化表達(dá)式則用表達(dá)式初始化,還有一些沒有初始化表達(dá)式的,例如某些表格數(shù)據(jù)初始化并不是一個(gè)簡單的賦值過程。在這種情況下,我們可以用一個(gè)特殊的init初始化函數(shù)來簡化初始化工作。每個(gè)文件都可以包含多個(gè)init初始化函數(shù)
func init() { /* ... */ }
這樣的init初始化函數(shù)除了不能被調(diào)用或引用外,其他行為和普通函數(shù)類似。在每個(gè)文件中的init初始化函數(shù),在程序開始執(zhí)行時(shí)按照它們聲明的順序被自動調(diào)用。
每個(gè)包在解決依賴的前提下,以導(dǎo)入聲明的順序初始化,每個(gè)包只會被初始化一次。因此,如果一個(gè)p包導(dǎo)入了q包,那么在p包初始化的時(shí)候可以認(rèn)為q包必然已經(jīng)初始化過了。初始化工作是自下而上進(jìn)行的,main包最后被初始化。以這種方式,可以確保在main函數(shù)執(zhí)行之前,所有依賴的包都已經(jīng)完成初始化工作了。
下面的代碼定義了一個(gè)PopCount函數(shù),用于返回一個(gè)數(shù)字中含二進(jìn)制1bit的個(gè)數(shù)。它使用init初始化函數(shù)來生成輔助表格pc,pc表格用于處理每個(gè)8bit寬度的數(shù)字含二進(jìn)制的1bit的bit個(gè)數(shù),這樣的話在處理64bit寬度的數(shù)字時(shí)就沒有必要循環(huán)64次,只需要8次查表就可以了。(這并不是最快的統(tǒng)計(jì)1bit數(shù)目的算法,但是它可以方便演示init函數(shù)的用法,并且演示了如何預(yù)生成輔助表格,這是編程中常用的技術(shù))。
gopl.io/ch2/popcount
package popcount
// pc[i] is the population count of i.
var pc [256]byte
func init() {
for i := range pc {
pc[i] = pc[i/2] + byte(i&1)
}
}
// PopCount returns the population count (number of set bits) of x.
func PopCount(x uint64) int {
return int(pc[byte(x>>(0*8))] +
pc[byte(x>>(1*8))] +
pc[byte(x>>(2*8))] +
pc[byte(x>>(3*8))] +
pc[byte(x>>(4*8))] +
pc[byte(x>>(5*8))] +
pc[byte(x>>(6*8))] +
pc[byte(x>>(7*8))])
}
譯注:對于pc這類需要復(fù)雜處理的初始化,可以通過將初始化邏輯包裝為一個(gè)匿名函數(shù)處理,像下面這樣:
// pc[i] is the population count of i.
var pc [256]byte = func() (pc [256]byte) {
for i := range pc {
pc[i] = pc[i/2] + byte(i&1)
}
return
}()
要注意的是在init函數(shù)中,range循環(huán)只使用了索引,省略了沒有用到的值部分。循環(huán)也可以這樣寫:
for i, _ := range pc {
我們在下一節(jié)和10.5節(jié)還將看到其它使用init函數(shù)的地方。
練習(xí) 2.3: 重寫PopCount函數(shù),用一個(gè)循環(huán)代替單一的表達(dá)式。比較兩個(gè)版本的性能。(11.4節(jié)將展示如何系統(tǒng)地比較兩個(gè)不同實(shí)現(xiàn)的性能。)
練習(xí) 2.4: 用移位算法重寫PopCount函數(shù),每次測試最右邊的1bit,然后統(tǒng)計(jì)總數(shù)。比較和查表算法的性能差異。
練習(xí) 2.5: 表達(dá)式x&(x-1)
用于將x的最低的一個(gè)非零的bit位清零。使用這個(gè)算法重寫PopCount函數(shù),然后比較性能。
![]() |
![]() |
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: