這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
本文翻譯自Dr.Dobb's的"Getting Going with Go"。
本文是有關Google新的系統原生語言的五周教程的第一部分,這裡將先向大家展示如何建立Go語言開發環境以及構建程式,然後帶領大家瀏覽 一些代碼範例來著重瞭解一下這門語言的一些有趣的特性。
這個教程系列將連續刊登五周。在今天這一部分中,Go語言專家Mark Summerfield將講解如何建立Go語言開發環境,提供兩個Go語言範例並給予深度解析。這些範例程式會向大家局部地展示了Go語言的一些關鍵特性 以及包。接下來幾周將展示其餘的關鍵特性,並特別為C、C++和Java程式員們深入研究那些Go語言專屬的特性。
正如本周主編文章中所解釋的那樣,Go語言擁有許多獨一無二的特性,因此它也許可以被稱為二十一世紀的C語言。而且考慮到Ken Thompson也是該語言的設計者之一,這兩種語言的確是有共同的祖先。
開始
Go是編譯型語言,而不是解釋型的。Go的編譯速度非常快– 甚至遠遠快過其他同類語言- 知名的如C和C++。
標準Go語言編譯器被稱為gc,與其相關的工具鏈包括用於編譯的5g、6g和8g;用於連結的5l、6l和8l以及用於查看Go語言文檔的 godoc(在Windows平台上這些程式為5g.exe、6g.exe等等諸如此類)。這些奇怪的名字遵循了Plan 9作業系統編譯器的命名方式,即用數字表示處理器體系("5"代表ARM,"6"代表AMD64-包括Intel 64位處理器- "8"代表Intel 386)。幸運的是,我們無需對此產生憂慮,Go語言提供了一個更進階別的Go語言構建工具,這個工具可以為我們處理編譯和連結任務。
本文中的所有代碼使用的都是Go 1版本文法,並在Linux、Mac OS X以及Windows上用gc測試通過了。Go語言的開發人員計劃讓隨後所有Go 1.x版本支援Go 1向後相容,因此這裡的代碼和例子將適用於所有1.x系列版本。
要下載和安裝Go,請訪問golang.org/doc/install.html,那裡提供了下載連結與安裝指令。Go 1為FreeBSD 7+、Linux 2.6+、Mac OS X (Snow Leopard和Lion)以及Windows 2000+提供源碼包以及二進位形式安裝包,可以支援所有Intel 32位和AMD 64位處理器體系。Go還支援ARM處理器版本的Linux,為Ubuntu Linux發布版提供預建go包。當你讀到這裡時,也許已經有其他Linux發布版的Go安裝包了。
使用gc編譯器的程式使用了一種特定的調用慣例(call convention)。這意味著使用gc編譯的程式只可以與使用同樣調用慣例編譯的外部庫進行連結 – 除非使用某適合的工具消除這些差異。使用cgo工具Go可以支援在Go程式中使用外部C代碼,並且至少在Linux和BSD系統上,通過SWIG工具我們 可以將C和C++代碼用於Go程式中。
除了gc,還有一種編譯器稱為gccgo。它是Gcc的一個Go特定前端,Gcc 4.6及以後版本才能支援。像gc一樣,gccgo也許內建在一些Linux發行版中。構建和安裝gccgo的指令在Go主要站台上可以找到。
Go文檔
Go的官方網站上維護著一份最新的Go文檔。"Packages"連結提供了有關所有Go標準庫包的訪問方式- 以及它們的源碼,這些源碼在文檔還很稀缺時十分有用。通過"Commands"連結你可以找到與Go一起發布的相關程式的文檔(諸如編譯器,構建工具 等)。通過"Specification"連結,你可以找到一份非正式,但很全面的Go語言規範。通過"Effective Go"連結,你可以找到一份介紹Go最佳實務的文檔。
該網站還提供了一個沙箱特性,用於線上編寫、編譯以及運行Go小程式(稍有限制)。這個特性十分有用,便於初學者實驗一些古怪的文法。Go網站上 的搜尋方塊只能用於在Go文檔中搜尋;如果要對Go的資源進行全面搜尋,請訪問http://go-lang.cat-v.org/go- search。
Go的文檔也可以在本地瀏覽,例如在Web瀏覽器中。如果要這樣做,可運行Go的godoc工具,並通過傳入命令列參數告知它以一個web伺服器 的方式運行。下面是在一個Unix或Windows控制台中進行這個操作的方法,假設PATH環境變數中已經設定了godoc:
$ godoc -http=:8000
這個例子中的連接埠號碼可以是任意的- 如果它與一個已存在的伺服器連接埠衝突,可以使用其他任一個連接埠號碼。
要想查看文檔,開啟一個瀏覽器,輸入地址http://localhost:8000。一個類似golang.org首頁的頁面將會呈現在你的面 前。"Package"連結將指向Go標準庫以及安裝在GOROOT環境變數下的第三方包的文檔。如果你定義了GOPATH環境變數(比如,為本 地程式和包),一個連結將會出現在"Packages"連結旁,通過這個連結你可以訪問其他相關文檔。(本文後續會討論GOROOT和 GOPATH環境變數)
編輯,編譯和運行
Go程式用UTF-8編碼的普通的Unicode文本編寫。絕大多數現代文字編輯器都可以自動處理這些代碼,並且一些最流行的編輯器可以支援Go 源碼的文法色彩高亮以及自動縮排。如果你的編輯器不支援Go,可以嘗試在Go搜尋引擎中輸入你的編輯器的名字,查看一下是否有合適的外掛程式。作為編 輯慣例,Go所有的關鍵字和操作符都使用ASCII字母;然而,Go的標識符可以以任意Unicode字母作為起始,後面可以跟著任意 Unicode字母或數字,因此Go開發人員可以自由使用他們的母語。
為了掌握如何編輯、編譯以及運行一個Go程式,我開始會用經典的"Hello World"程式作為例子- 然而我編寫這個程式比尋常的稍複雜些。
如果你已經用二進位包或通過源碼安裝了Go,並且是以root或管理員權限安裝的,你應該至少設定一個環境變數:GOROOT,該變數指示Go的 安裝路徑資訊,你的PATH變數現在應該包含$GOROOT/bin或%GOROOT%\bin。為了檢查Go安裝地是否正確,可在控制台下輸入 下面命令:
$ go version
如果你得到"command not found"或"'go' is not recognized…"的錯誤資訊,那就意味著在PATH變數配置的路徑下沒有Go。
編譯與連結
構建一個Go程式需要兩步:編譯和連結。(由於我們假設使用gc編譯器,使用gccgo編譯器的讀者需要遵循golang.org/doc /gccgo_install.html中描述的編譯和連結過程,同樣,使用其他編譯器的讀者需要根據其編譯器的指令進行編譯和連結)。編譯和鏈 接過程都由工具go處理,它不僅可以構建本地程式和包,還能夠擷取、構建以及安裝第三方程式和包。
要想go能夠編譯本地程式和包,有三個要求。第一,Go的bin目錄($GOROOT/bin或%GOROOT%\bin)必須在PATH環境變 量下。第二,必須存在一個目錄,該目錄下包含一個src目錄,本地程式和包的源碼就駐留在src目錄下。例如,例子代碼會解包到goeg/src /hello、goeg/src/bigdigits下等。最後,包含src的那個目錄必須在GOPATH環境變數中設定。例如,要使用go工具 構建hello這個例子,我們必須這麼做:
$ export GOPATH=$HOME/goeg
$ cd $GOPATH/src/hello
$ go build
兩個例子中,我們都假設PATH環境變數中包含了$GOROOT/bin或%GOROOT%\bin。一旦go編譯器完畢,我們就可以運行這個 程式了。預設情況下,go會用可執行檔所在目錄的名字來命名該檔案(例如,在類Unix系統上是hello,而在Windows上為 hello.exe)。一旦構建完畢,我們就可以按常規方式運行它。
$ ./hello
Hello World!
注意,我們不需要編譯或顯式地連結其他包(即便後續我們將看到,hello.go使用了三個標準庫的包)。這也是Go程式編譯如此之快的一個原 因。
如果我們有多個Go程式,若他們的可執行檔能夠放在同一個目錄下將會非常方便,後續只需將這個目錄加入到PATH環境變數中。幸運地是go支援 這樣的情況:
$ export GOPATH=$HOME/goeg
$ cd $GOPATH/src/hello
$ go install
go install命令除了做了go build所做的事情之外,還將可執行檔放在標準位置($GOPATH/bin或%GOPATH%\bin)。這意味著將一個單一路徑($GOPATH /bin或%GOPATH>%\bin)加入到PATH環境變數中,我們安裝的所有Go程式就可以方便地被加入到PATH中。
除了這裡給出的例子外,我們很可能想要在我們自己的目錄下開發我們自己的Go程式和包。通過為GOPATH環境變數設定兩個(或更多)冒號分隔的 路徑(在Windows上用分號分隔)我們可以很容易解決這個問題。
雖然Go使用go工具作為標準構建工具,但你仍然可以使用make或其他現代構建工具,或使用其他Go專用的構建工具,或一些流行IDE的外掛程式。
和誰打招呼(Hello)?
既然我們已經看到了如何構建一個Hello程式,接下來我們來看看其原始碼。下面是hello程式的完整源碼(在檔案 hello/hello.go中):
// hello.go
package main
import (
"fmt"
"os"
"strings"
)
func main() {
who := "World!"
if len(os.Args) > 1 { /* os.Args[0] is "hello" or "hello.exe" */
who = strings.Join(os.Args[1:], " ")
}
fmt.Println("Hello", who)
}
Go用C++風格的注釋符號//作為單行注釋,用/* … */作為多行注釋符號。依照慣例,Go中多使用單行注釋,多行注釋常用於在開發中注釋掉代碼塊。
每段Go代碼都存在於一個包內,並且每個Go程式必須具有一個包含main()函數的main包,其中main函數會作為程式執行的進入點,即這 個函數首先執行。事實上,Go包也可以定義在main函數之前執行的init函數。值得注意的是包名和函數名之間不會存在衝突的情況。
Go的操作是以包為單位的,而不是檔案。這意味著我們可以根據需要任意地將一個包拆分到多個檔案中。如果多個檔案具有相同的包聲明,Go語言認為 這些檔案都是同一個包的組成部分,與所有內容在單一檔案中無異。當然,我們也可以將應用的功能分解到許多當地套件中,這樣可以保持代碼整潔地模組 化。
import語句從標準庫匯入三個包。fmt包提供格式化文本以及讀取格式化文本的函數;os包提供平台無關的作業系統變數以及函 數;strings包提供操作字串的函數。
Go的基本類型支援普通操作符(例如,+可用於數值加法以及字串串連),Go的標準庫通過提供操作基本類型的函數包補充功能,例如這裡匯入的 strings包。我們可以在基本類型的基礎上建立我們自訂的類型並為它們定義相關方法- 自訂動作屬性類別型的函數。
讀者也許已經注意到了Go源碼中沒有分號、import的包無需逗號分隔以及if條件陳述式不需要括弧。在Go中,塊(block),包括函數體以 及控制結構體(如for、if語句以及for迴圈),使用括弧界定。縮排只是單純用於提高代碼的可讀性。技術上而言,Go語句是用分號分隔的,但 這些分號由編譯器插入,我們自己無需關心,除非我們要將多個語句放在同一行中時。沒有分號、很少的逗號以及括弧讓Go程式看起來更簡潔,需要的輸 入也更少。
Go使用func關鍵字定義函數(function)和方法(method)。main包的main()函數總是具有相同的函數簽名 – 沒有參數、沒有傳回值。當main.main()結束時,程式將終止並返回0給作業系統。當然,我們可以在任意時刻返回並選擇我們自己的傳回值。
main()函數中的第一個語句(使用:=操作符)在Go的術語裡被稱為一個短變數聲明。這個語句在同一時間聲明並初始化一個變數。此外,我們無 需指定變數的類型,因為Go可根據初始值推匯出變數的類型。因此在這個例子中,我們聲明了一個string類型的變數who,感謝Go的強型別機 制,我們只需將字串賦值給who即可。
os.Args變數是字串的一個slice(片段)。Go使用array(數組)、slice和其他集合資料類型,但在這些例子中,知道下面這 些即可:使用內建的len()函數擷取一個slice的長度以及通過[]下標操作符訪問其中的元素。特別是,slice[n]返回slice的第 n個元素(從0起始),slice[:n]返回另外一個slice,這個新slice由原slice的第n個到最後一個之間的元素組成。在集合一 章,我們將看到有關這方面的一般性的Go文法。就這裡的os.Args來說,這個slice總是應該至少在位置0處具有一個字串(程式的名 字)。(所有Go的下標索引都是從0開始)
如果輸入一個或更多命令列參數,if條件將被滿足,我們將所有參數拼接成一個單一的字串並賦值給who。在這裡例子中,我們使用賦值操作符 (=),如果我們使用:=,我們將聲明和初始化一個新的who變數,其影響範圍將局限在if語句塊中。strings.Join函數接受一個字元 串slice和一個分割符(可以為空白,即"")作為參數,並返回一個由所有slice的字串元素組成的,由分隔字元分隔的單一字串。這裡我們用 空格分隔這些字串。
最後,在最後一個語句中,我們輸出Hello,一個空格,who變數中的字串以及一個新行(newline)。fmt包中擁有許多不同的列印輸 出變體,一些像fmt.Println()的將整齊地輸出任何傳入的值,其他的諸如fmt.Printf將使用預留位置以提供更佳的格式控制。
另外一個例子 – 二維slices
下一個例子bigdigits程式從命令列(以一個字串形式)讀取一個數,並在控制台上使用"大號字型"輸出這個數。早在二十世紀,在很多使用者 共用一台高速行印表機的場合,按慣例將使用這種技術在每個使用者的列印工作之前放置一個封面,該封面可以展示用於識別的細節,諸如使用者名稱以及將被打 印的檔案的名字。
我將分三段回顧這個程式的代碼:首先是import部分,然後是待用資料,最後是處理過程。不過現在讓我們來看看一個範例,瞭解一下這個程式是如 何工作的吧:
$ ./bigdigits 290175493
每位元字都由一個字串slice表示,所有數字一起由一個元素為字串slice的slice表示。在查看資料之前,這裡我們展示一下如何聲明 和初始化一個一維的字串和數字slice:
longWeekend := []string{"Friday", "Saturday", "Sunday", "Monday"}
var lowPrimes = []int{2, 3, 5, 7, 11, 13, 17, 19}
slice用[]Type表示,如果我們要初始化它們,我們可以直接在後面跟上一個用大括弧包裹、逗號分隔的對應類型的元素列表。我們本可以用同 樣的變數聲明文法來聲明這兩個變數,但在這裡我們為展示兩種文法的差異以及一個稍後即將說明的原因,lowPrimes slice使用了一個更長形式的聲明。由於slice類型可以作為slice類型的元素類型,因此我們可以很容易地建立多維集合(slice的slice 等)。
bigdigits程式只需要匯入四個包。
import (
"fmt"
"log"
"os"
"path/filepath"
)
fmt包提供了用於文字格式設定化和讀取格式化文本的函數。log包提供了日誌記錄函數。os包提供了平台無關的作業系統變數以及函數,其中就包括持 有命令列參數值的[]string類型的os.Args變數。path包下面的filepath包提供跨平台操作檔案名稱和路徑的相關函數。注意對 於邏輯上存在內含項目關聯性的包(譯註:如path/filepath)來說,我們在代碼中使用時只指明其名字的最後部分(這裡是filepath)。
對於bigdigits這個程式,我們需要一個二維的資料(一個字串slice的slice)。下面就是建立它的方法,通過代表數字0的字串 布局我們可以看出這個數字對應的字串在輸出時相應的行。3到8的數字對應的字串這裡省略了。
var bigDigits = [][]string{
{" 000 ",
" 0 0 ",
"0 0",
"0 0",
"0 0",
"0 0",
" 000 "},
{" 1 ", "11 ", " 1 ", " 1 ", " 1 ", " 1 ", "111"},
{"222","2 2"," 2"," 2 ","2 ","2 ","22222"},
// … 3 to 8 …
{" 9999", "9 9", "9 9", " 9999", " 9", " 9", " 9"},
在函數或方法外面聲明的變數不可以使用:=操作符,但我們可以使用長聲明形式(使用關鍵字var)以及賦值操作符(=)來得到同樣的效果,就如我 們這裡為bigDigits程式中的變數所做的那樣(前面對lowPrime變數的聲明)。我們仍然無需指定bigDigits變數的類型,Go 可以從賦值中推斷出其類型。
我們把計算工作留給了Go編譯器,因此也沒有必要指出slice的維數。Go的一個方便之處就是它對使用了括弧的符合字面值的良好支援,這樣我們 無需在一個地方聲明一個資料變數,然後在另外一個地方用資料給它賦值了。
main()函數讀取命令列,並使用這些資料產生輸出,這個函數只有20行。
func main() {
if len(os.Args) == 1 {
fmt.Printf("usage: %s \n", filepath.Base(os.Args[0]))
os.Exit(1)
}
stringOfDigits := os.Args[1]
for row := range bigDigits[0] {
line := ""
for column := range stringOfDigits {
digit := stringOfDigits[column] – '0'
if 0 <= digit && digit <= 9 {
line += bigDigits[digit][row] + " "
} else {
log.Fatal("invalid whole number")
}
}
fmt.Println(line)
}
}
程式一開始檢查是否有任何命令列參數。如果沒有,len(os.Args)的值將為1(回憶一下,os.Args[0]中存放的是程式名,因此這 個slice的長度至少是1)。如果這個if語句條件成立,我們將使用fmt.Printf輸出一條適當程式用法資訊,該Printf函數使用類 似C/C++中printf()或Python中%操作符的%預留位置。
path/filepath包提供路徑操作函數- 比如,filepath.Base()函數返回給定路徑的基本名(basename,即檔案名稱)。在輸出這條資訊後,程式使用os.Exit()函數結束 程式,並返回1給作業系統。在類Unix系統中,一個值為0的傳回值用於表示成功,非0值標識用法錯誤或失敗。
filepath.Base()函數的使用向我們說明了Go的一個美妙的特性:當一個包被匯入時,無論它是頂層的包還是邏輯上內建於其他包中的包 (例如:path/filepath),我們總是可以只通過其名字的最後部分(即filepath)來引用它。我們還可以給包賦予本地名字以避免 名字衝突。
如果至少傳入了一個命令列參數,第一個參數將被拷貝到stringOfDigits變數(string類型)中。要想將使用者輸入的數字轉換成大數 字,我們必須迭代處理bigDigits slice的每一行,即每個數位第一行(最上面的一行),接下來第二行,依次類推。我們假設所有bigDigits的slice都具有相同數量的行,這 樣我們可以從第一個slice那裡得到行數。Go的for迴圈對不同情境有不同的應對文法;在這個例子中,for…range迴圈返回 slice中每個元素的索引位置資訊。
行和列的迴圈部分的代碼可以這樣來寫:
for row := 0; row < len(bigDigits[0]); row++ {
line := ""
for column := 0; column < len(stringOfDigits); column++ {
…
這是一個C、C++和Java程式員都熟悉的文法形式,在Go中它也是有效。(與C、C++和Java不同在於,在Go中,++和–操作符只 能用作語句,而不能用作運算式。此外,它們只能被用作尾碼操作符,而不能作為首碼操作符。這意味著求值順序導致的相關問題在Go中不會發生- 謝天謝地,像f(i++)和a[i] = b[++i]這樣的運算式在Go中是非法的。) 然而,for…range文法更加短小,也更加方便。
在每次行迭代時,代碼會將行的line賦值為空白字串。接下來,我們做迭代處理從使用者那裡擷取的stringOFDigits中的列(即,字 符)。Go的字串使用UTF-8字元,因此本質上一個字元很可能用兩個或更多位元組表示。但這不是這裡要討論的話題,因為我們只關心數值0、 1、…、9,這些數值用UTF-8字元表示時只需一個位元組,與用7位元ASCII字元表示所使用的位元組值相同。
當我們索引字串中的某個特定位置時,我們擷取了那個位置的位元組值。(在Go中byte類型是uint8類型的同義字。)因此我們擷取到命令列字 符串特定列上的位元組值,減去數字0的位元組值後,得到它表示的數字。在UTF-8(以及7位元ASCII)中,字元'0'是碼點(字元)十進位值 48,字元'1'是碼點十進位值49,依次類推。這樣舉例,如果我們有字元'3'(碼點1),我們可以通過做減法'3' – '0'(即51-48)的結果得到其整型值3。
Go使用單引號表示字元字面值,一個字元字面值是一個與Go任何整數型別都相容的整數。Go的強型別意味著如果不進行顯式轉型,我們無法將一個 int32類型的數與一個int16類型的數相加,不過,Go中的數值常量和字面值自適應於其上下文,這樣一來,這裡的'0'被認為是一個位元組。
如果這個數字(byte類型)在範圍內,我們會將對應的字串加到line變數中。(在if語句中,常量0和9被認為是byte類型,因為它們是 數實值型別,但如果數值是一個不同的類型,比如說,int,它們將會被當作新類型對待。)雖然Go中的字串是不可改變的,Go仍然支援+=附加操 作符以提供一個便於使用的文法。(它通過在後台替換掉原先的字串。)Go同樣支援+字串串連操作符,該操作將返回一個由左右字串運算元串連 而成的新字串。
為了擷取對應的字串,我們根據這個數值訪問bigDigits slice,然後訪問其中我們需要的行(字串)。
如果數值超出了範圍(比如,由於stringOfDigits包含了一個非數值),我們調用log.Fatal()函數記錄一條錯誤資訊。如果沒有顯式指 定其他日誌輸出目標,這個函數會在os.Stderr中記錄下日期、時間和錯誤資訊。然後該函數調用os.Exit(1)結束程式。還有 一個名為log.Fatalf()的函數可以做同樣的事情,但它接受%預留位置。我們沒有在第一個if語句中使用log.Fatal()函數,因為 我們想輸出程式的使用方法資訊,但不要log.Fatal()預設輸出的日期和時間資訊。
一旦給定行的所有數位字串都累加完畢,完整的一行就被輸出。在這裡,我們輸出了7行,因為每個bigDigits slice中的數字由七個字串表示。
最後一點是聲明和定義的順序無關緊要。因此在bigdigits/bigdigits.go檔案中,我們可在main()函數前聲明bigDigits變 量,也可在後面聲明。在這個例子裡,我們將main()函數放在前面,對於這篇文章中的例子,我們通常更傾向於自頂向下的排序。
這裡的兩個例子已經涵蓋了大量特性,但它們所展示的資料與其他主流語言甚為相似,即便文法稍有不同。下周的文章將檢視Go語言的其餘特性,包含一些進階方面的特性。
2012, bigwhite. 著作權.