Go 語(yǔ)言 包和文件

2023-03-14 16:51 更新

原文鏈接:https://gopl-zh.github.io/ch2/ch2-06.html


2.6. 包和文件

Go語(yǔ)言中的包和其他語(yǔ)言的庫(kù)或模塊的概念類似,目的都是為了支持模塊化、封裝、單獨(dú)編譯和代碼重用。一個(gè)包的源代碼保存在一個(gè)或多個(gè)以.go為文件后綴名的源文件中,通常一個(gè)包所在目錄路徑的后綴是包的導(dǎo)入路徑;例如包gopl.io/ch1/helloworld對(duì)應(yīng)的目錄路徑是$GOPATH/src/gopl.io/ch1/helloworld。

每個(gè)包都對(duì)應(yīng)一個(gè)獨(dú)立的名字空間。例如,在image包中的Decode函數(shù)和在unicode/utf16包中的 Decode函數(shù)是不同的。要在外部引用該函數(shù),必須顯式使用image.Decode或utf16.Decode形式訪問(wèn)。

包還可以讓我們通過(guò)控制哪些名字是外部可見(jiàn)的來(lái)隱藏內(nèi)部實(shí)現(xiàn)信息。在Go語(yǔ)言中,一個(gè)簡(jiǎn)單的規(guī)則是:如果一個(gè)名字是大寫字母開(kāi)頭的,那么該名字是導(dǎo)出的(譯注:因?yàn)闈h字不區(qū)分大小寫,因此漢字開(kāi)頭的名字是沒(méi)有導(dǎo)出的)。

為了演示包基本的用法,先假設(shè)我們的溫度轉(zhuǎn)換軟件已經(jīng)很流行,我們希望到Go語(yǔ)言社區(qū)也能使用這個(gè)包。我們?cè)撊绾巫瞿兀?

讓我們創(chuàng)建一個(gè)名為gopl.io/ch2/tempconv的包,這是前面例子的一個(gè)改進(jìn)版本。(這里我們沒(méi)有按照慣例按順序?qū)舆M(jìn)行編號(hào),因此包路徑看起來(lái)更像一個(gè)真實(shí)的包)包代碼存儲(chǔ)在兩個(gè)源文件中,用來(lái)演示如何在一個(gè)源文件聲明然后在其他的源文件訪問(wèn);雖然在現(xiàn)實(shí)中,這樣小的包一般只需要一個(gè)文件。

我們把變量的聲明、對(duì)應(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è)源文件都是以包的聲明語(yǔ)句開(kāi)始,用來(lái)指明包的名字。當(dāng)包被導(dǎo)入的時(shí)候,包內(nèi)的成員將通過(guò)類似tempconv.CToF的形式訪問(wèn)。而包級(jí)別的名字,例如在一個(gè)文件聲明的類型和常量,在同一個(gè)包的其他源文件也是可以直接訪問(wèn)的,就好像所有代碼都在一個(gè)文件一樣。要注意的是tempconv.go源文件導(dǎo)入了fmt包,但是conv.go源文件并沒(méi)有,因?yàn)檫@個(gè)源文件中的代碼并沒(méi)有用到fmt包。

因?yàn)榘?jí)別的常量名都是以大寫字母開(kāi)頭,它們可以像tempconv.AbsoluteZeroC這樣被外部代碼訪問(wèn):

fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"

要將攝氏溫度轉(zhuǎn)換為華氏溫度,需要先用import語(yǔ)句導(dǎo)入gopl.io/ch2/tempconv包,然后就可以使用下面的代碼進(jìn)行轉(zhuǎn)換了:

fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"

在每個(gè)源文件的包聲明前緊跟著的注釋是包注釋(§10.7.4)。通常,包注釋的第一句應(yīng)該先是包的功能概要說(shuō)明。一個(gè)包通常只有一個(gè)源文件有包注釋(譯注:如果有多個(gè)包注釋,目前的文檔工具會(huì)根據(jù)源文件名的先后順序?qū)⑺鼈冩溄訛橐粋€(gè)包注釋)。如果包注釋很大,通常會(huì)放到一個(gè)獨(dú)立的doc.go文件中。

練習(xí) 2.1: 向tempconv包添加類型、常量和函數(shù)用來(lái)處理Kelvin絕對(duì)溫度的轉(zhuǎn)換,Kelvin 絕對(duì)零度是?273.15°C,Kelvin絕對(duì)溫度1K和攝氏度1°C的單位間隔是一樣的。

2.6.1. 導(dǎo)入包

在Go語(yǔ)言程序中,每個(gè)包都有一個(gè)全局唯一的導(dǎo)入路徑。導(dǎo)入語(yǔ)句中類似"gopl.io/ch2/tempconv"的字符串對(duì)應(yīng)包的導(dǎo)入路徑。Go語(yǔ)言的規(guī)范并沒(méi)有定義這些字符串的具體含義或包來(lái)自哪里,它們是由構(gòu)建工具來(lái)解釋的。當(dāng)使用Go語(yǔ)言自帶的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)入語(yǔ)句將導(dǎo)入的包綁定到一個(gè)短小的名字,然后通過(guò)該短小的名字就可以引用包中導(dǎo)出的全部?jī)?nèi)容。上面的導(dǎo)入聲明將允許我們以tempconv.CToF的形式來(lái)訪問(wèn)gopl.io/ch2/tempconv包中的內(nèi)容。在默認(rèn)情況下,導(dǎo)入的包綁定到tempconv名字(譯注:指包聲明語(yǔ)句指定的名字),但是我們也可以綁定到另一個(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è)包,但是又沒(méi)有使用該包將被當(dāng)作一個(gè)編譯錯(cuò)誤處理。這種強(qiáng)制規(guī)則可以有效減少不必要的依賴,雖然在調(diào)試期間可能會(huì)讓人討厭,因?yàn)閯h除一個(gè)類似log.Print("got here!")的打印語(yǔ)句可能導(dǎo)致需要同時(shí)刪除log包導(dǎo)入聲明,否則,編譯器將會(huì)發(fā)出一個(gè)錯(cuò)誤。在這種情況下,我們需要將不必要的導(dǎo)入刪除或注釋掉。

不過(guò)有更好的解決方案,我們可以使用golang.org/x/tools/cmd/goimports導(dǎo)入工具,它可以根據(jù)需要自動(dòng)添加或刪除導(dǎo)入的包;許多編輯器都可以集成goimports工具,然后在保存文件的時(shí)候自動(dòng)運(yùn)行。類似的還有g(shù)ofmt工具,可以用來(lái)格式化Go源文件。

練習(xí) 2.2: 寫一個(gè)通用的單位轉(zhuǎn)換程序,用類似cf程序的方式從命令行讀取參數(shù),如果缺省的話則是從標(biāo)準(zhǔn)輸入讀取參數(shù),然后做類似Celsius和Fahrenheit的單位轉(zhuǎn)換,長(zhǎng)度單位可以對(duì)應(yīng)英尺和米,重量單位可以對(duì)應(yīng)磅和公斤等。

2.6.2. 包的初始化

包的初始化首先是解決包級(jí)變量的依賴順序,然后按照包級(jí)變量聲明出現(xiàn)的順序依次初始化:

var a = b + c // a 第三個(gè)初始化, 為 3
var b = f()   // b 第二個(gè)初始化, 為 2, 通過(guò)調(diào)用 f (依賴c)
var c = 1     // c 第一個(gè)初始化, 為 1

func f() int { return c + 1 }

如果包中含有多個(gè).go源文件,它們將按照發(fā)給編譯器的順序進(jìn)行初始化,Go語(yǔ)言的構(gòu)建工具首先會(huì)將.go文件根據(jù)文件名排序,然后依次調(diào)用編譯器編譯。

對(duì)于在包級(jí)別聲明的變量,如果有初始化表達(dá)式則用表達(dá)式初始化,還有一些沒(méi)有初始化表達(dá)式的,例如某些表格數(shù)據(jù)初始化并不是一個(gè)簡(jiǎn)單的賦值過(guò)程。在這種情況下,我們可以用一個(gè)特殊的init初始化函數(shù)來(lái)簡(jiǎn)化初始化工作。每個(gè)文件都可以包含多個(gè)init初始化函數(shù)

func init() { /* ... */ }

這樣的init初始化函數(shù)除了不能被調(diào)用或引用外,其他行為和普通函數(shù)類似。在每個(gè)文件中的init初始化函數(shù),在程序開(kāi)始執(zhí)行時(shí)按照它們聲明的順序被自動(dòng)調(diào)用。

每個(gè)包在解決依賴的前提下,以導(dǎo)入聲明的順序初始化,每個(gè)包只會(huì)被初始化一次。因此,如果一個(gè)p包導(dǎo)入了q包,那么在p包初始化的時(shí)候可以認(rèn)為q包必然已經(jīng)初始化過(guò)了。初始化工作是自下而上進(jìn)行的,main包最后被初始化。以這種方式,可以確保在main函數(shù)執(zhí)行之前,所有依賴的包都已經(jīng)完成初始化工作了。

下面的代碼定義了一個(gè)PopCount函數(shù),用于返回一個(gè)數(shù)字中含二進(jìn)制1bit的個(gè)數(shù)。它使用init初始化函數(shù)來(lái)生成輔助表格pc,pc表格用于處理每個(gè)8bit寬度的數(shù)字含二進(jìn)制的1bit的bit個(gè)數(shù),這樣的話在處理64bit寬度的數(shù)字時(shí)就沒(méi)有必要循環(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))])
}

譯注:對(duì)于pc這類需要復(fù)雜處理的初始化,可以通過(guò)將初始化邏輯包裝為一個(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)只使用了索引,省略了沒(méi)有用到的值部分。循環(huán)也可以這樣寫:

for i, _ := range pc {

我們?cè)谙乱还?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ù),每次測(cè)試最右邊的1bit,然后統(tǒng)計(jì)總數(shù)。比較和查表算法的性能差異。

練習(xí) 2.5: 表達(dá)式x&(x-1)用于將x的最低的一個(gè)非零的bit位清零。使用這個(gè)算法重寫PopCount函數(shù),然后比較性能。



以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)