GO语言-第二节-顺序编程
目錄
2.1變量
2.1.1變量聲明
2.1.2 變量初始化
2.1.3 變量賦值
2.1.4 匿名變量
2.2 常量
2.2.1 字面常量
2.2.2 常量定義
2.2.3 預定義常量
2.2.4 枚舉
2.3 類型
2.3.1 布爾類型
2.3.2 整型
1. 類型表示
2. 數值運算
3. 比較運算
4. 位運算
2.3.3 浮點型
1. 浮點數表示
2. 浮點數比較
2.3.4 復數類型
1. 復數表示
2. 實部與虛部
2.3.5 字符串
1. 字符串操作
2. 字符串遍歷
2.3.6 字符類型
2.3.7 數組
1. 元素訪問
2. 值類型
2.3.8 數組切片
1. 創(chuàng)建數組切片
2. 元素遍歷
3. 動態(tài)增減元素
4. 基于數組切片創(chuàng)建數組切片
5. 內容復制
2.3.9 map
1. 變量聲明
2. 創(chuàng)建
3. 元素賦值
4. 元素刪除
5. 元素查找
2.4 流程控制
2.4.1 條件語句
2.4.2 選擇語句
2.4.3 循環(huán)語句
2.4.4 跳轉語句
2.5 函數
2.5.1 函數定義
2.5.2 函數調用
2.5.3 不定參數
1. 不定參數類型
2. 不定參數的傳遞
3. 任意類型的不定參數
2.5.4 多返回值
?2.5.5 匿名函數與閉包
1. 匿名函數
2. 閉包
2.6 錯誤處理
2.6.1 error接口
2.6.2 defer
2.6.3 panic()和recover()
2.7 完整示例
2.7.1 程序結構
2.7.2 主程序
1. 命令行參數
2. 讀取輸入文件
3. 寫到輸出文件
2.7.3 算法實現
1. 冒泡排序
2. 快速排序
2.7.4 主程序
2.7.5 構建與執(zhí)行
2.8 小結
2.1變量
? ? ? ? 變量是幾乎所有編程語言的組成元素,從根本上來說,變量相當于是對一塊數據儲存控件的命名,程序可以通過定義一個變量來申請一塊數據儲存控件,之后可以通過醫(yī)用變量名來使用這塊儲存空間。
? ? ? ? GO語言中的變量使用方式語C語言接近,但具備更大的靈活性。
2.1.1變量聲明
? ? ? ? GO語言的變量聲明方式與C和C++語言有明顯的不同。對于純粹的變量聲明,GO語言引入了關鍵字var,而類型信息放在變量名之后,
var v1 int var v2 string var v3 [10]int // 數組 var v4 []int // 數組切片 var v5 struct { f int } var v6 *int // 指針 var v7 map[string]int // map,key為string類型,value為int類型 var v8 func(a int) int????????變量聲明語句不需要使用分號作為結束符。與C語言相比,Go語言摒棄了語句必須以分號作 為語句結束標記的習慣。
???????? var關鍵字的另一種用法是可以將若干個需要聲明的變量放置在一起,免得程序員需要重復 寫var關鍵字,如下所示:
var ( v1 intv2 string )2.1.2 變量初始化
????????對于聲明變量時需要進行初始化的場景,var關鍵字可以保留,但不再是必要的元素,如下 所示:
var v1 int = 10 // 正確的使用方式1 var v2 = 10 // 正確的使用方式2,編譯器可以自動推導出v2的類型 v3 := 10 // 正確的使用方式3,編譯器可以自動推導出v3的類型????????以上三種用法的效果是完全一樣的。與第一種用法相比,第三種用法需要輸入的字符數大大 減少,是懶程序員和聰明程序員的最佳選擇。這里Go語言也引入了另一個C和C++中沒有的符號 (冒號和等號的組合:=),用于明確表達同時進行變量聲明和初始化的工作。
????????指定類型已不再是必需的,Go編譯器可以從初始化表達式的右值推導出該變量應該聲明為 哪種類型,這讓Go語言看起來有點像動態(tài)類型語言,盡管Go語言實際上是不折不扣的強類型語 言(靜態(tài)類型語言)
????????當然,出現在:=左側的變量不應該是已經被聲明過的,否則會導致編譯錯誤,比如下面這個 寫法:
var i int i := 2????????會導致類似如下的編譯錯誤:
no new variables on left side of :=2.1.3 變量賦值
????????在Go語法中,變量初始化和變量賦值是兩個不同的概念。下面為聲明一個變量之后的賦值 過程:
var v10 int v10 = 123????????Go語言的變量賦值與多數語言一致,但Go語言中提供了C/C++程序員期盼多年的多重賦值功 能,比如下面這個交換i和j變量的語句:
i, j = j, i????????在不支持多重賦值的語言中,交互兩個變量的內容需要引入一個中間變量:
t = i; i = j; j = t;????????多重賦值的特性在Go語言庫的實現中也被使用得相當充分,在介紹函數的多重返回值時, 將對其進行更加深入的介紹。總而言之,多重賦值功能讓Go語言與C/C++語言相比可以非常明顯 地減少代碼行數。
2.1.4 匿名變量
????????我們在使用傳統的強類型語言編程時,經常會出現這種情況,即在調用函數時為了獲取一個 值,卻因為該函數返回多個值而不得不定義一堆沒用的變量。在Go中這種情況可以通過結合使 用多重返回和匿名變量來避免這種丑陋的寫法,讓代碼看起來更加優(yōu)雅。
????????假 設GetName()函數的定義如下,它返回3個值,分別為firstName、lastName和 nickName:
func GetName() (firstName, lastName, nickName string) { return "May", "Chan", "Chibi Maruko" }????????若只想獲得nickName,則函數調用語句可以用如下方式編寫:
_, _, nickName := GetName()????????這種用法可以讓代碼非常清晰,基本上屏蔽掉了可能混淆代碼閱讀者視線的內容,從而大幅 降低溝通的復雜度和代碼維護的難度。
2.2 常量
????????在Go語言中,常量是指編譯期間就已知且不可改變的值。常量可以是數值類型(包括整型、 浮點型和復數類型)、布爾類型、字符串類型等。
2.2.1 字面常量
????????所謂字面常量(literal),是指程序中硬編碼的常量,如:
-12 3.14159265358979323846 // 浮點類型的常量 3.2+12i // 復數類型的常量 true // 布爾類型的常量 "foo" // 字符串常量????????在其他語言中,常量通常有特定的類型,比如?12在C語言中會認為是一個int類型的常量。 如果要指定一個值為?12的long類型常量,需要寫成?12l,這有點違反人們的直觀感覺。Go語言 的字面常量更接近我們自然語言中的常量概念,它是無類型的。只要這個常量在相應類型的值域 范圍內,就可以作為該類型的常量,比如上面的常量?12,它可以賦值給int、uint、int32、 int64、float32、float64、complex64、complex128等類型的變量。
2.2.2 常量定義
????????通過const關鍵字,你可以給字面常量指定一個友好的名字:
const Pi float64 = 3.14159265358979323846 const zero = 0.0 // 無類型浮點常量 const (size int64 = 1024 eof = -1 // 無類型整型常量 ) const u, v float32 = 0, 3 // u = 0.0, v = 3.0,常量的多重賦值 const a, b, c = 3, 4, "foo" // a = 3, b = 4, c = "foo", 無類型整型和字符串常量????????Go的常量定義可以限定常量類型,但不是必需的。如果定義常量時沒有指定類型,那么它 與字面常量一樣,是無類型常量。
???????? 常量定義的右值也可以是一個在編譯期運算的常量表達式,比如
????????由于常量的賦值是一個編譯期行為,所以右值不能出現任何需要運行期才能得出結果的表達 式,比如試圖以如下方式定義常量就會導致編譯錯誤:
const Home = os.GetEnv("HOME")????????原因很簡單,os.GetEnv()只有在運行期才能知道返回結果,在編譯期并不能確定,所以 無法作為常量定義的右值。
2.2.3 預定義常量
????????Go語言預定義了這些常量:true、false和iota。
????????iota比較特殊,可以被認為是一個可被編譯器修改的常量,在每一個const關鍵字出現時被 重置為0,然后在下一個const出現之前,每出現一次iota,其所代表的數字會自動增1。
????????從以下的例子可以基本理解iota的用法:
const ( // iota被重設為0c0 = iota // c0 == 0 c1 = iota // c1 == 1 c2 = iota // c2 == 2 ) const ( a = 1 << iota // a == 1 (iota在每個const開頭被重設為0) b = 1 << iota // b == 2 c = 1 << iota // c == 4 ) const ( u = iota * 42 // u == 0 v float64 = iota * 42 // v == 42.0 w = iota * 42 // w == 84 ) const x = iota // x == 0 (因為iota又被重設為0了) const y = iota // y == 0 (同上)????????如果兩個const的賦值語句的表達式是一樣的,那么可以省略后一個賦值表達式。因此,上 面的前兩個const語句可簡寫為:
const ( // iota被重設為0c0 = iota // c0 == 0 c1 // c1 == 1 c2 // c2 == 2 ) const ( a = 1 <<iota // a == 1 (iota在每個const開頭被重設為0) b // b == 2 c // c == 4 )2.2.4 枚舉
????????枚舉指一系列相關的常量,比如下面關于一個星期中每天的定義。通過上一節(jié)的例子,我們 看到可以用在const后跟一對圓括號的方式定義一組常量,這種定義法在Go語言中通常用于定義 枚舉值。Go語言并不支持眾多其他語言明確支持的enum關鍵字。
????????下面是一個常規(guī)的枚舉表示法,其中定義了一系列整型常量:
const ( Sunday = iotaMonday Tuesday Wednesday Thursday Friday Saturday numberOfDays // 這個常量沒有導出 )????????同Go語言的其他符號(symbol)一樣,以大寫字母開頭的常量在包外可見。
????????以上例子中numberOfDays為包內私有,其他符號則可被其他包訪問。
2.3 類型
????????Go語言內置以下這些基礎類型:
????????關于錯誤類型,我們會在“錯誤處理”一節(jié)中介紹;關于通道,我們會在4.5節(jié)中進一步介 紹;關于結構體和接口,我們則在第3章中進行詳細的闡述。
????????在這些基礎類型之上Go還封裝了下面這幾種類型:int、uint和uintptr等。這些類型的 特點在于使用方便,但使用者不能對這些類型的長度做任何假設。對于常規(guī)的開發(fā)來說,用int 和uint就可以了,沒必要用int8之類明確指定長度的類型,以免導致移植困難。
2.3.1 布爾類型
????????Go語言中的布爾類型與其他語言基本一致,關鍵字也為bool,可賦值為預定義的true和 false示例代碼如下:
var v1 bool v1 = true v2 := (1 == 2) // v2也會被推導為bool類型????????布爾類型不能接受其他類型的賦值,不支持自動或強制的類型轉換。以下的示例是一些錯誤 的用法,會導致編譯錯誤:
var b bool b = 1 // 編譯錯誤 b = bool(1) // 編譯錯誤以下的用法才是正確的:
var b bool b = (1!=0) // 編譯正確 fmt.Println("Result:", b) // 打印結果為Result: true2.3.2 整型
????????整型是所有編程語言里最基礎的數據類型。Go語言支持表2-1所示的這些整型類型。
| 類型 | 長度(字節(jié)) | 值范圍 |
| int8 | 1 | -128 ~ 127 |
| uint8(即byte) | 1 | 0 ~ 255 |
| int16 | 2 | ?32 768 ~ 32 767 |
| uint16 | 2 | 0 ~ 65 535 |
| int32 | 3 | -2 147 483 648 ~ 2 147 483 647 |
| uint32 | 3 | 0 ~ 4 294 967 295 |
| int64 | 8 | ?9 223 372 036 854 775 808 ~ 9 223 372 036 854 775 807 |
| uint64 | 8 | 0~ 18 446 744 073 709 551 615 |
| int | 平臺相關 | 平臺相關 |
| uint | 平臺相關 | 平臺相關 |
| uintptr | 同指針 | 在32位平臺下為4字節(jié),64位平臺下為8字節(jié) |
1. 類型表示
????????需要注意的是,int和int32在Go語言里被認為是兩種不同的類型,編譯器也不會幫你自動 做類型轉換,比如以下的例子會有編譯錯誤:
var value2 int32 value1 := 64 // value1將會被自動推導為int類型 value2 = value1 // 編譯錯誤編譯錯誤類似于:
cannot use value1 (type int) as type int32 in assignment。使用強制類型轉換可以解決這個編譯錯誤:
value2 = int32(value1) // 編譯通過????????當然,開發(fā)者在做強制類型轉換時,需要注意數據長度被截短而發(fā)生的數據精度損失(比如 將浮點數強制轉為整數)和值溢出(值超過轉換的目標類型的值范圍時)問題。
2. 數值運算
????????Go語言支持下面的常規(guī)整數運算:+、?、*、/和%。加減乘除就不詳細解釋了,需要說下的 是,% 和在C語言中一樣是求余運算,比如:
5 % 3 // 結果為:23. 比較運算
????????Go語言支持以下的幾種比較運算符:>、=、<=和!=。這一點與大多數其他語言相 同,與C語言完全一致。
????????下面為條件判斷語句的例子:
i, j := 1, 2 if i == j { fmt.Println("i and j are equal.") }????????兩個不同類型的整型數不能直接比較,比如int8類型的數和int類型的數不能直接比較,但 各種類型的整型變量都可以直接與字面常量(literal)進行比較,比如:
var i int32 var j int64 i, j = 1, 2 if i == j { // 編譯錯誤fmt.Println("i and j are equal.") } if i == 1 || j == 2 { // 編譯通過fmt.Println("i and j are equal.") }4. 位運算
????????Go語言支持表2-2所示的位運算符。
| 運算 | 含義 | 樣例 |
| x << y | 左移 | 124 << 2 // 結果為496 |
| x >> y | 右移 | 124 >> 2 // 結果為31 |
| x ^ y | 異或 | 124 ^ 2 // 結果為126 |
| x & y | 與 | 124 & 2 // 結果為0 |
| x | y | 或 | 124 | 2 // 結果為126 |
| ^x | 取反 | ^2 // 結果為-3 |
????????Go語言的大多數位運算符與C語言都比較類似,除了取反在C語言中是~x,而在Go語言中 是^x。
2.3.3 浮點型
????????浮點型用于表示包含小數點的數據,比如1.234就是一個浮點型數據。Go語言中的浮點類型 采用IEEE-754標準的表達方式。
1. 浮點數表示
????????Go語言定義了兩個類型float32和float64,其中float32等價于C語言的float類型, float64等價于C語言的double類型。
????????在Go語言里,定義一個浮點數變量的代碼如下:
var fvalue1 float32fvalue1 = 12 fvalue2 := 12.0 // 如果不加小數點,fvalue2會被推導為整型而不是浮點型????????對于以上例子中類型被自動推導的fvalue2,需要注意的是其類型將被自動設為float64, 而不管賦給它的數字是否是用32位長度表示的。因此,對于以上的例子,下面的賦值將導致編譯 錯誤:
fvalue1 = fvalue2而必須使用這樣的強制類型轉換:
fvalue1 = float32(fvalue2)2. 浮點數比較
????????因為浮點數不是一種精確的表達方式,所以像整型那樣直接用==來判斷兩個浮點數是否相等 是不可行的,這可能會導致不穩(wěn)定的結果。
????????下面是一種推薦的替代方案:
import "math" // p為用戶自定義的比較精度,比如0.00001 func IsEqual(f1, f2, p float64) bool { return math.Fdim(f1, f2) < p }2.3.4 復數類型
????????復數實際上由兩個實數(在計算機中用浮點數表示)構成,一個表示實部(real),一個表示 虛部(imag)。如果了解了數學上的復數是怎么回事,那么Go語言的復數就非常容易理解了。
1. 復數表示
????????復數表示的示例如下:
var value1 complex64 // 由2個float32構成的復數類型value1 = 3.2 + 12i value2 := 3.2 + 12i // value2是complex128類型 value3 := complex(3.2, 12) // value3結果同 value22. 實部與虛部
????????對于一個復數z = complex(x, y),就可以通過Go語言內置函數real(z)獲得該復數的實 部,也就是x,通過imag(z)獲得該復數的虛部,也就是y。
????????更多關于復數的函數,請查閱math/cmplx標準庫的文檔。
2.3.5 字符串
????????在Go語言中,字符串也是一種基本類型。相比之下, C/C++語言中并不存在原生的字符串 類型,通常使用字符數組來表示,并以字符指針來傳遞。
????????Go語言中字符串的聲明和初始化非常簡單,舉例如下:
var str string // 聲明一個字符串變量 str = "Hello world" // 字符串賦值 ch := str[0] // 取字符串的第一個字符 fmt.Printf("The length of \"%s\" is %d \n", str, len(str)) fmt.Printf("The first character of \"%s\" is %c.\n", str, ch)輸出結果為:
The length of "Hello world" is 11 The first character of "Hello world" is H.????????字符串的內容可以用類似于數組下標的方式獲取,但與數組不同,字符串的內容不能在初始 化后被修改,比如以下的例子:
str := "Hello world" // 字符串也支持聲明時進行初始化的做法 str[0] = 'X' // 編譯錯誤????????編譯器會報類似如下的錯誤:
cannot assign to str[0]????????在這個例子中我們使用了一個Go語言內置的函數len()來取字符串的長度。這個函數非常有 用,我們在實際開發(fā)過程中處理字符串、數組和切片時將會經常用到。
????????本節(jié)中我們還順便示范了Printf()函數的用法。有C語言基礎的讀者會發(fā)現,Printf()函 數的用法與C語言運行庫中的printf()函數如出一轍。讀者在以后學習更多的Go語言特性時, 可以配合使用Println()和Printf()來打印各種自己感興趣的信息,從而讓學習過程更加直 觀、有趣。
????????Go編譯器支持UTF-8的源代碼文件格式。這意味著源代碼中的字符串可以包含非ANSI的字 符,比如“Hello world. 你好,世界!”可以出現在Go代碼中。但需要注意的是,如果你的Go代 碼需要包含非ANSI字符,保存源文件時請注意編碼格式必須選擇UTF-8。特別是在Windows下一 般編輯器都默認存為本地編碼,比如中國地區(qū)可能是GBK編碼而不是UTF-8,如果沒注意這點在 編譯和運行時就會出現一些意料之外的情況。
????????字符串的編碼轉換是處理文本文檔(比如TXT、XML、HTML等)非常常見的需求,不過可 惜的是Go語言僅支持UTF-8和Unicode編碼。對于其他編碼,Go語言標準庫并沒有內置的編碼轉 換支持。不過,所幸的是我們可以很容易基于iconv庫用Cgo包裝一個。這里有一個開源項目: https://github.com/xushiwei/go-iconv。
1. 字符串操作
平時常用的字符串操作如表2-3所示。
| 運算 | 含義 | 樣例 |
| x + y | 字符串連接 | "Hello" + "123" // 結果為Hello123 |
| len(s) | 字符串長度 | len("Hello") // 結果為5 |
| s[i] | 取字符 | "Hello" [1] // 結果為'e' |
????????更多的字符串操作,請參考標準庫strings包。
2. 字符串遍歷
Go語言支持兩種方式遍歷字符串。一種是以字節(jié)數組的方式遍歷:
str := "Hello,世界" n := len(str) for i := 0; i < n; i++ { ch := str[i] // 依據下標取字符串中的字符,類型為byte fmt.Println(i, ch) }這個例子的輸出結果為:
0 72 1 101 2 108 3 108 4 111 5 44 6 32 7 228 8 184 9 150 10 231 11 149 12 140????????可以看出,這個字符串長度為13。盡管從直觀上來說,這個字符串應該只有9個字符。這是 因為每個中文字符在UTF-8中占3個字節(jié),而不是1個字節(jié)。
????????另一種是以Unicode字符遍歷:
str := "Hello,世界" for i, ch := range str { fmt.Println(i, ch)//ch的類型為rune }輸出結果為:
0 72 1 101 2 108 3 108 4 111 5 44 6 32 7 19990 10 30028????????以Unicode字符方式遍歷時,每個字符的類型是rune(早期的Go語言用int類型表示Unicode 字符),而不是byte。
2.3.6 字符類型
????????在Go語言中支持兩個字符類型,一個是byte(實際上是uint8的別名),代表UTF-8字符串的單個字節(jié)的值;另一個是rune,代表單個Unicode字符。
????????關于rune相關的操作,可查閱Go標準庫的unicode包。另外unicode/utf8包也提供了 UTF8和Unicode之間的轉換。
????????出于簡化語言的考慮,Go語言的多數API都假設字符串為UTF-8編碼。盡管Unicode字符在標 準庫中有支持,但實際上較少使用。
2.3.7 數組
????????數組是Go語言編程中最常用的數據結構之一。顧名思義,數組就是指一系列同一類型數據 的集合。數組中包含的每個數據被稱為數組元素(element),一個數組包含的元素個數被稱為數 組的長度。
????????以下為一些常規(guī)的數組聲明方法:
[32]byte // 長度為32的數組,每個元素為一個字節(jié) [2*N] struct { x, y int32 } // 復雜類型數組 [1000]*float64 // 指針數組 [3][5]int // 二維數組 [2][2][2]float64 // 等同于[2]([2]([2]float64))????????從以上類型也可以看出,數組可以是多維的,比如[3][5]int就表達了一個3行5列的二維整 型數組,總共可以存放15個整型元素。
????????在Go語言中,數組長度在定義后就不可更改,在聲明時長度可以為一個常量或者一個常量 表達式(常量表達式是指在編譯期即可計算結果的表達式)。數組的長度是該數組類型的一個內 置常量,可以用Go語言的內置函數len()來獲取。下面是一個獲取數組arr元素個數的寫法:
arrLength := len(arr)1. 元素訪問
????????可以使用數組下標來訪問數組中的元素。與C語言相同,數組下標從0開始,len(array)-1 則表示最后一個元素的下標。下面的示例遍歷整型數組并逐個打印元素內容:
for i := 0; i < len(array); i++ { fmt.Println("Element", i, "of array is", array[i]) }????????Go語言還提供了一個關鍵字range,用于便捷地遍歷容器中的元素。當然,數組也是range 的支持范圍。上面的遍歷過程可以簡化為如下的寫法:
for i, v := range array { fmt.Println("Array element[", i, "]=", v) }????????在上面的例子里可以看到,range具有兩個返回值,第一個返回值是元素的數組下標,第二 個返回值是元素的值。
2. 值類型
????????需要特別注意的是,在Go語言中數組是一個值類型(value type)。所有的值類型變量在賦值和作為參數傳遞時都將產生一次復制動作。如果將數組作為函數的參數類型,則在函數調用時該 參數將發(fā)生數據復制。因此,在函數體中無法修改傳入的數組的內容,因為函數內操作的只是所 傳入數組的一個副本。
????????下面用例子來說明這一特點:
package main import "fmt" func modify(array [10]int) { array[0] = 10 // 試圖修改數組的第一個元素fmt.Println("In modify(), array values:", array) } func main() { array := [5]int{1,2,3,4,5} // 定義并初始化一個數組modify(array) // 傳遞給一個函數,并試圖在函數體內修改這個數組內容fmt.Println("In main(), array values:", array) }????????該程序的執(zhí)行結果為:
In modify(), array values: [10 2 3 4 5] In main(), array values: [1 2 3 4 5]????????從執(zhí)行結果可以看出,函數modify()內操作的那個數組跟main()中傳入的數組是兩個不同的實 例。那么,如何才能在函數內操作外部的數據結構呢?我們將在2.3.6節(jié)中詳細介紹如何用數組切 片功能來達成這個目標。
2.3.8 數組切片
????????在前一節(jié)里我們已經提過數組的特點:數組的長度在定義之后無法再次修改;數組是值類型, 每次傳遞都將產生一份副本。顯然這種數據結構無法完全滿足開發(fā)者的真實需求。
????????不用失望,Go語言提供了數組切片(slice)這個非常酷的功能來彌補數組的不足。
????????初看起來,數組切片就像一個指向數組的指針,實際上它擁有自己的數據結構,而不僅僅是 個指針。數組切片的數據結構可以抽象為以下3個變量:
?????????從底層實現的角度來看,數組切片實際上仍然使用數組來管理元素,因此它們之間的關系讓 C++程序員們很容易聯想起STL中std::vector和數組的關系。基于數組,數組切片添加了一系 列管理功能,可以隨時動態(tài)擴充存放空間,并且可以被隨意傳遞而不會導致所管理的元素被重復 復制。
1. 創(chuàng)建數組切片
????????創(chuàng)建數組切片的方法主要有兩種——基于數組和直接創(chuàng)建,下面我們來簡要介紹一下這兩種 方法。
- 基于數組
????????數組切片可以基于一個已存在的數組創(chuàng)建。數組切片可以只使用數組的一部分元素或者整個 數組來創(chuàng)建,甚至可以創(chuàng)建一個比所基于的數組還要大的數組切片。代碼清單2-1演示了如何基 于一個數組的前5個元素創(chuàng)建一個數組切片。
package main import "fmt" func main() { // 先定義一個數組var myArray [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} // 基于數組創(chuàng)建一個數組切片var mySlice []int = myArray[:5] fmt.Println("Elements of myArray: ") for _, v := range myArray { fmt.Print(v, " ") } fmt.Println("\nElements of mySlice: ") for _, v := range mySlice { fmt.Print(v, " ") } fmt.Println() }運行結果為:
Elements of myArray: 1 2 3 4 5 6 7 8 9 10 Elements of mySlice: 1 2 3 4 5????????讀者應該已經注意到,Go語言支持用myArray[first:last]這樣的方式來基于數組生成一 個數組切片,而且這個用法還很靈活,比如下面幾種都是合法的。
????????基于myArray的所有元素創(chuàng)建數組切片:
mySlice = myArray[:]????????基于myArray的前5個元素創(chuàng)建數組切片:
mySlice = myArray[:5]基于從第5個元素開始的所有元素創(chuàng)建數組切片:
mySlice = myArray[5:]- 直接創(chuàng)建
????????并非一定要事先準備一個數組才能創(chuàng)建數組切片。Go語言提供的內置函數make()可以用于 靈活地創(chuàng)建數組切片。下面的例子示范了直接創(chuàng)建數組切片的各種方法。
????????創(chuàng)建一個初始元素個數為5的數組切片,元素初始值為0:
mySlice1 := make([]int, 5)????????創(chuàng)建一個初始元素個數為5的數組切片,元素初始值為0,并預留10個元素的存儲空間:
mySlice2 := make([]int, 5, 10)????????直接創(chuàng)建并初始化包含5個元素的數組切片:
mySlice3 := []int{1, 2, 3, 4, 5}????????當然,事實上還會有一個匿名數組被創(chuàng)建出來,只是不需要我們來操心而已。
2. 元素遍歷
????????操作數組元素的所有方法都適用于數組切片,比如數組切片也可以按下標讀寫元素,用len() 函數獲取元素個數,并支持使用range關鍵字來快速遍歷所有元素。
????????傳統的元素遍歷方法如下:
for i := 0; i <len(mySlice); i++ { fmt.Println("mySlice[", i, "] =", mySlice[i]) }????????使用range關鍵字可以讓遍歷代碼顯得更整潔。range表達式有兩個返回值,第一個是索引, 第二個是元素的值:
for i, v := range mySlice { fmt.Println("mySlice[", i, "] =", v) }????????對比上面的兩個方法,我們可以很容易地看出使用range的代碼更簡單易懂。
3. 動態(tài)增減元素
????????可動態(tài)增減元素是數組切片比數組更為強大的功能。與數組相比,數組切片多了一個存儲能 力(capacity)的概念,即元素個數和分配的空間可以是兩個不同的值。合理地設置存儲能力的 值,可以大幅降低數組切片內部重新分配內存和搬送內存塊的頻率,從而大大提高程序性能。
????????假如你明確知道當前創(chuàng)建的數組切片最多可能需要存儲的元素個數為50,那么如果你設置的 存儲能力小于50,比如20,那么在元素超過20時,底層將會發(fā)生至少一次這樣的動作——重新分 配一塊“夠大”的內存,并且需要把內容從原來的內存塊復制到新分配的內存塊,這會產生比較 明顯的開銷。給“夠大”這兩個字加上引號的原因是系統并不知道多大才是夠大,所以只是一個 簡單的猜測。比如,將原有的內存空間擴大兩倍,但兩倍并不一定夠,所以之前提到的內存重新 分配和內容復制的過程很有可能發(fā)生多次,從而明顯降低系統的整體性能。但如果你知道最大是 50并且一開始就設置存儲能力為50,那么之后就不會發(fā)生這樣非常耗費CPU的動作,從而達到空間換時間的效果。
????????數組切片支持Go語言內置的cap()函數和len()函數,代碼清單2-2簡單示范了這兩個內置 函數的用法。可以看出,cap()函數返回的是數組切片分配的空間大小,而len()函數返回的是 數組切片中當前所存儲的元素個數。
package main import "fmt" func main() { mySlice := make([]int, 5, 10) fmt.Println("len(mySlice):", len(mySlice)) fmt.Println("cap(mySlice):", cap(mySlice)) }該程序的輸出結果為:
len(mySlice): 5 cap(mySlice): 10????????如果需要往上例中mySlice已包含的5個元素后面繼續(xù)新增元素,可以使用append()函數。 下面的代碼可以從尾端給mySlice加上3個元素,從而生成一個新的數組切片:
mySlice = append(mySlice, 1, 2, 3)????????函數append()的第二個參數其實是一個不定參數,我們可以按自己需求添加若干個元素, 甚至直接將一個數組切片追加到另一個數組切片的末尾:
mySlice2 := []int{8, 9, 10} // 給mySlice后面添加另一個數組切片 mySlice = append(mySlice, mySlice2...)????????需要注意的是,我們在第二個參數mySlice2后面加了三個點,即一個省略號,如果沒有這個省 略號的話,會有編譯錯誤,因為按append()的語義,從第二個參數起的所有參數都是待附加的 元素。因為mySlice中的元素類型為int,所以直接傳遞mySlice2是行不通的。加上省略號相 當于把mySlice2包含的所有元素打散后傳入。
????????上述調用等同于:
mySlice = append(mySlice, 8, 9, 10)????????數組切片會自動處理存儲空間不足的問題。如果追加的內容長度超過當前已分配的存儲空間 (即cap()調用返回的信息),數組切片會自動分配一塊足夠大的內存。
4. 基于數組切片創(chuàng)建數組切片
????????類似于數組切片可以基于一個數組創(chuàng)建,數組切片也可以基于另一個數組切片創(chuàng)建。下面的 例子基于一個已有數組切片創(chuàng)建新數組切片:
oldSlice := []int{1, 2, 3, 4, 5} newSlice := oldSlice[:3] // 基于oldSlice的前3個元素構建新數組切片????????有意思的是,選擇的oldSlicef元素范圍甚至可以超過所包含的元素個數,比如newSlice 可以基于oldSlice的前6個元素創(chuàng)建,雖然oldSlice只包含5個元素。只要這個選擇的范圍不超 過oldSlice存儲能力(即cap()返回的值),那么這個創(chuàng)建程序就是合法的。newSlice中超出 oldSlice元素的部分都會填上0。
5. 內容復制
????????數組切片支持Go語言的另一個內置函數copy(),用于將內容從一個數組切片復制到另一個 數組切片。如果加入的兩個數組切片不一樣大,就會按其中較小的那個數組切片的元素個數進行 復制。下面的示例展示了copy()函數的行為:
slice1 := []int{1, 2, 3, 4, 5} slice2 := []int{5, 4, 3} copy(slice2, slice1) // 只會復制slice1的前3個元素到slice2中 copy(slice1, slice2) // 只會復制slice2的3個元素到slice1的前3個位置2.3.9 map
????????在C++/Java中,map一般都以庫的方式提供,比如在C++中是STL的std::map<>,在C#中是 Dictionary<>,在Java中是Hashmap<>,在這些語言中,如果要使用map,事先要引用相應的 庫。而在Go中,使用map不需要引入任何庫,并且用起來也更加方便。
????????map是一堆鍵值對的未排序集合。比如以身份證號作為唯一鍵來標識一個人的信息,則這個 map可以定義為代碼清單 2-3所示的方式。
package main import "fmt" // PersonInfo是一個包含個人詳細信息的類型 type PersonInfo struct { ID stringName stringAddress string } func main() { var personDB map[string] PersonInfo personDB = make(map[string] PersonInfo) // 往這個map里插入幾條數據personDB["12345"] = PersonInfo{"12345", "Tom", "Room 203,..."} personDB["1"] = PersonInfo{"1", "Jack", "Room 101,..."} // 從這個map查找鍵為"1234"的信息person, ok := personDB["1234"] // ok是一個返回的bool型,返回true表示找到了對應的數據if ok { fmt.Println("Found person", person.Name, "with ID 1234.") } else { fmt.Println("Did not find person with ID 1234.") } }上面這個簡單的例子基本上已經覆蓋了map的主要用法,下面對其中的關鍵點進行細述。
1. 變量聲明
map的聲明基本上沒有多余的元素,比如:
var myMap map[string] PersonInfo其中,myMap是聲明的map變量名,string是鍵的類型,PersonInfo則是其中所存放的值類型。
2. 創(chuàng)建
????????我們可以使用Go語言內置的函數make()來創(chuàng)建一個新map。下面的這個例子創(chuàng)建了一個鍵 類型為string、值類型為PersonInfo的map:
myMap = make(map[string] PersonInfo)????????也可以選擇是否在創(chuàng)建時指定該map的初始存儲能力,下面的例子創(chuàng)建了一個初始存儲能力 為100的map:
myMap = make(map[string] PersonInfo, 100)????????關于存儲能力的說明,可以參見2.3.6節(jié)中的內容。
????????創(chuàng)建并初始化map的代碼如下:
myMap = map[string] PersonInfo{ "1234": PersonInfo{"1", "Jack", "Room 101,..."}, }3. 元素賦值
賦值過程非常簡單明了,就是將鍵和值用下面的方式對應起來即可:
myMap["1234"] = PersonInfo{"1", "Jack", "Room 101,..."}4. 元素刪除
????????Go語言提供了一個內置函數delete(),用于刪除容器內的元素。下面我們簡單介紹一下如 何用delete()函數刪除map內的元素:
delete(myMap, "1234")上面的代碼將從myMap中刪除鍵為“1234”的鍵值對。如果“1234”這個鍵不存在,那么這個調 用將什么都不發(fā)生,也不會有什么副作用。但是如果傳入的map變量的值是nil,該調用將導致 程序拋出異常(panic)。
5. 元素查找
????????在Go語言中,map的查找功能設計得比較精巧。而在其他語言中,我們要判斷能否獲取到一個值不是件容易的事情。判斷能否從map中獲取一個值的常規(guī)做法是:
????????(1) 聲明并初始化一個變量為空;
????????(2) 試圖從map中獲取相應鍵的值到該變量中;
????????(3) 判斷該變量是否依舊為空,如果為空則表示map中沒有包含該變量。
????????這種用法比較啰唆,而且判斷變量是否為空這條語句并不能真正表意(是否成功取到對應的 值),從而影響代碼的可讀性和可維護性。有些庫甚至會設計為因為一個鍵不存在而拋出異常, 讓開發(fā)者用起來膽戰(zhàn)心驚,不得不一層層嵌套try-catch語句,這更是不人性化的設計。在Go 語言中,要從map中查找一個特定的鍵,可以通過下面的代碼來實現:
value, ok := myMap["1234"] if ok { // 找到了// 處理找到的value }????????判斷是否成功找到特定的鍵,不需要檢查取到的值是否為nil,只需查看第二個返回值ok, 這讓表意清晰很多。配合:=操作符,讓你的代碼沒有多余成分,看起來非常清晰易懂。
2.4 流程控制
????????程序設計語言的流程控制語句,用于設定計算執(zhí)行的次序,建立程序的邏輯結構。可以說, 流程控制語句是整個程序的骨架。
????????從根本上講,流程控制只是為了控制程序語句的執(zhí)行順序,一般需要與各種條件配合,因此, 在各種流程中,會加入條件判斷語句。流程控制語句一般起以下3個作用:
????????在具體的應用場景中,為了滿足更豐富的控制需求,Go語言還添加了如下關鍵字:break、 continue和fallthrough。在實際的使用中,需要根據具體的邏輯目標、程序執(zhí)行的時間和空 間限制、代碼的可讀性、編譯器的代碼優(yōu)化設定等多種因素,靈活組合。
????????接下來簡要介紹一下各種流程控制功能的用法以及需要注意的要點.
2.4.1 條件語句
????????關于條件語句的樣例代碼如下:
if a < 5 { return 0 } else { return 1 }關于條件語句,需要注意以下幾點:
否則會編譯失敗:
function ends without a return statement失敗的原因在于,Go編譯器無法找到終止該函數的return語句。編譯失敗的案例如下:
func example(x int) int { if x == 0 { return 5 } else { return x } }2.4.2 選擇語句
????????根據傳入條件的不同,選擇語句會執(zhí)行不同的語句。下面的例子根據傳入的整型變量i的不 同而打印不同的內容:
switch i { case 0: fmt.Printf("0") case 1: fmt.Printf("1") case 2: fallthrough case 3: fmt.Printf("3") case 4, 5, 6: fmt.Printf("4, 5, 6") default: fmt.Printf("Default") }????????運行上面的案例,將會得到如下結果:
比較有意思的是,switch后面的表達式甚至不是必需的,比如下面的例子:
switch { case 0 <= Num && Num <= 3: fmt.Printf("0-3") case 4 <= Num && Num <= 6: fmt.Printf("4-6") case 7 <= Num && Num <= 9: fmt.Printf("7-9") }在使用switch結構時,我們需要注意以下幾點:
2.4.3 循環(huán)語句
????????與多數語言不同的是,Go語言中的循環(huán)語句只支持for關鍵字,而不支持while和do-while 結構。關鍵字for的基本使用方法與C和C++中非常接近:
sum := 0 for i := 0; i < 10; i++ { sum += i }????????可以看到比較大的一個不同在于for后面的條件表達式不需要用圓括號()包含起來。Go語言 還進一步考慮到無限循環(huán)的場景,讓開發(fā)者不用寫無聊的for (;;) {} 和 do {} while(1);, 而直接簡化為如下的寫法:
sum := 0 for { sum++ if sum > 100 { break } }在條件表達式中也支持多重賦值,如下所示:
a := []int{1, 2, 3, 4, 5, 6} for i, j := 0, len(a) – 1; i < j; i, j = i + 1, j – 1 { a[i], a[j] = a[j], a[i] }使用循環(huán)語句時,需要注意的有以下幾點。
break,可以選擇中斷哪一個循環(huán),如下例:
for j := 0; j < 5; j++ { for i := 0; i < 10; i++ { if i > 5 { break JLoop } fmt.Println(i) } } JLoop: // ...本例中,break語句終止的是JLoop標簽處的外層循環(huán)。
2.4.4 跳轉語句
????????goto語句被多數語言學者所反對,諄諄告誡不要使用。但對于Go語言這樣一個惜關鍵字如 金的語言來說,居然仍然支持goto關鍵字,無疑讓某些人跌破眼鏡。但就個人一年多來的Go語 言編程經驗來說,goto還是會在一些場合下被證明是最合適的。
????????goto語句的語義非常簡單,就是跳轉到本函數內的某個標簽,如:
func myfunc() { i := 0 HERE: fmt.Println(i) i++ if i < 10 { goto HERE } }2.5 函數
????????函數構成代碼執(zhí)行的邏輯結構。在Go語言中,函數的基本組成為:關鍵字func、函數名、參數列表、返回值、函數體和返回語句。
2.5.1 函數定義
前面我們已經大概介紹過函數,這里我們用一個最簡單的加法函數來進行詳細說明:
package mymath import "errors" func Add(a int, b int) (ret int, err error) { if a < 0 || b < 0 { // 假設這個函數只支持兩個非負數字的加法err= errors.New("Should be non-negative numbers!") return } return a + b, nil // 支持多重返回值 }????????如果參數列表中若干個相鄰的參數類型的相同,比如上面例子中的a和b,則可以在參數列表 中省略前面變量的類型聲明,如下所示:
func Add(a, b int)(ret int, err error) { // ... }如果返回值列表中多個返回值的類型相同,也可以用同樣的方式合并。 如果函數只有一個返回值,也可以這么寫:
func Add(a, b int) int { // ... }從其他語言轉過來的同學,可能更習慣這種寫法。
2.5.2 函數調用
????????函數調用非常方便,只要事先導入了該函數所在的包,就可以直接按照如下所示的方式調用 函數:
import "mymath"// 假設Add被放在一個叫mymath的包中// ... c := mymath.Add(1, 2)????????在Go語言中,函數支持多重返回值,這在之后的內容中會介紹。利用函數的多重返回值和 錯誤處理機制,我們可以很容易地寫出優(yōu)雅美觀的Go代碼。
????????Go語言中函數名字的大小寫不僅僅是風格,更直接體現了該函數的可見性,這一點尤其需 要注意。對于很多注意美感的程序員(尤其是工作在Linux平臺上的C程序員)而言,這里的函數 名的首字母大寫可能會讓他們感覺不太適應,在自己練習的時候可能會順手改成全小寫,比如寫 成add_xxx這樣的Linux風格。很不幸的是,如果這樣做了,你可能會遇到莫名其妙的編譯錯誤, 比如你明明導入了對應的包,Go編譯器還是會告訴你無法找到add_xxx函數。
????????因此需要先牢記這樣的規(guī)則:小寫字母開頭的函數只在本包內可見,大寫字母開頭的函數才 能被其他包使用。
????????這個規(guī)則也適用于類型和變量的可見性。
2.5.3 不定參數
????????在C語言時代大家一般都用過printf()函數,從那個時候開始其實已經在感受不定參數的 魅力和價值。如同C語言中的printf()函數,Go語言標準庫中的fmt.Println()等函數的實現 也嚴重依賴于語言的不定參數功能。
????????本節(jié)我們將介紹不定參數的用法。合適地使用不定參數,可以讓代碼簡單易用,尤其是輸入 輸出類函數,比如日志函數等。
1. 不定參數類型
????????不定參數是指函數傳入的參數個數為不定數量。為了做到這點,首先需要將函數定義為接受 不定參數類型:
func myfunc(args ...int) { for _, arg := range args { fmt.Println(arg) } }????????這段代碼的意思是,函數myfunc()接受不定數量的參數,這些參數的類型全部是int,所 以它可以用如下方式調用:
myfunc(2, 3, 4) myfunc(1, 3, 7, 13)????????形如...type格式的類型只能作為函數的參數類型存在,并且必須是最后一個參數。它是一 個語法糖(syntactic sugar),即這種語法對語言的功能并沒有影響,但是更方便程序員使用。通 常來說,使用語法糖能夠增加程序的可讀性,從而減少程序出錯的機會.
????????從內部實現機理上來說,類型...type本質上是一個數組切片,也就是[]type,這也是為 什么上面的參數args可以用for循環(huán)來獲得每個傳入的參數。
????????假如沒有...type這樣的語法糖,開發(fā)者將不得不這么寫:
func myfunc2(args []int) { for _, arg := range args { fmt.Println(arg) } }????????從函數的實現角度來看,這沒有任何影響,該怎么寫就怎么寫。但從調用方來說,情形則完 全不同:
myfunc2([]int{1, 3, 7, 13})????????你會發(fā)現,我們不得不加上[]int{}來構造一個數組切片實例。但是有了...type這個語法糖,我們就不用自己來處理了。
2. 不定參數的傳遞
假設有另一個變參函數叫做myfunc3(args ...int),下面的例子演示了如何向其傳遞變參:
func myfunc(args ...int) { // 按原樣傳遞myfunc3(args...) // 傳遞片段,實際上任意的int slice都可以傳進去myfunc3(args[1:]...) }3. 任意類型的不定參數
????????之前的例子中將不定參數類型約束為int,如果你希望傳任意類型,可以指定類型為 interface{}。下面是Go語言標準庫中fmt.Printf()的函數原型:
func Printf(format string, args ...interface{}) { // ... }????????用interface{}傳遞任意類型數據是Go語言的慣例用法。使用interface{}仍然是類型安 全的,這和 C/C++ 不太一樣。關于它的用法,可參閱3.5節(jié)的內容。代碼清單2-4示范了如何分派 傳入interface{}類型的數據。
package main import "fmt" func MyPrintf(args ...interface{}) { for _, arg := range args { switch arg.(type) { case int: fmt.Println(arg, "is an int value.") case string: fmt.Println(arg, "is a string value.") case int64: fmt.Println(arg, "is an int64 value.") default: fmt.Println(arg, "is an unknown type.") } } } func main() { var v1 int = 1 var v2 int64 = 234 var v3 string = "hello" var v4 float32 = 1.234MyPrintf(v1, v2, v3, v4) }該程序的輸出結果為:
1 is an int value.? 234 is an int64 value.? hello is a string value.? 1.234 is an unknown type.2.5.4 多返回值
????????與C、C++和Java等開發(fā)語言的一個極大不同在于,Go語言的函數或者成員的方法可以有多 個返回值,這個特性能夠使我們寫出比其他語言更優(yōu)雅、更簡潔的代碼,比如File.Read()函 數就可以同時返回讀取的字節(jié)數和錯誤信息。如果讀取文件成功,則返回值中的n為讀取的字節(jié) 數,err為nil,否則err為具體的出錯信息:
func (file *File) Read(b []byte) (n int, err Error)????????同樣,從上面的方法原型可以看到,我們還可以給返回值命名,就像函數的輸入參數一樣。 返回值被命名之后,它們的值在函數開始的時候被自動初始化為空。在函數中執(zhí)行不帶任何參數 的return語句時,會返回對應的返回值變量的值。
????????Go語言并不需要強制命名返回值,但是命名后的返回值可以讓代碼更清晰,可讀性更強, 同時也可以用于文檔。
?????????如果調用方調用了一個具有多返回值的方法,但是卻不想關心其中的某個返回值,可以簡單 地用一個下劃線“_”來跳過這個返回值,比如下面的代碼表示調用者在讀文件的時候不想關心 Read()函數返回的錯誤碼:
n, _ := f.Read(buf)?2.5.5 匿名函數與閉包
????????匿名函數是指不需要定義函數名的一種函數實現方式,它并不是一個新概念,最早可以回溯 到1958年的Lisp語言。但是由于各種原因,C和C++一直都沒有對匿名函數給以支持,其他的各 種語言,比如JavaScript、C#和Objective-C等語言都提供了匿名函數特性,當然也包含Go語言。
1. 匿名函數
????????在Go里面,函數可以像普通變量一樣被傳遞或使用,這與C語言的回調函數比較類似。不同 的是,Go語言支持隨時在代碼里定義匿名函數.
????????匿名函數由一個不帶函數名的函數聲明和函數體組成,如下所示:
func(a, b int, z float64) bool { return a*b <int(z) }????????匿名函數可以直接賦值給一個變量或者直接執(zhí)行:
f := func(x, y int) int { return x + y } func(ch chan int) { ch <- ACK } (reply_chan) // 花括號后直接跟參數列表表示函數調用2. 閉包
Go的匿名函數是一個閉包,下面我們先來了解一下閉包的概念、價值和應用場景。
- 基本概念
????????閉包是可以包含自由(未綁定到特定對象)變量的代碼塊,這些變量不在這個代碼塊內或者 任何全局上下文中定義,而是在定義代碼塊的環(huán)境中定義。要執(zhí)行的代碼塊(由于自由變量包含 在代碼塊中,所以這些自由變量以及它們引用的對象沒有被釋放)為自由變量提供綁定的計算環(huán) 境(作用域)。
- 閉包的價值
????????閉包的價值在于可以作為函數對象或者匿名函數,對于類型系統而言,這意味著不僅要表示 數據還要表示代碼。支持閉包的多數語言都將函數作為第一級對象,就是說這些函數可以存儲到 變量中作為參數傳遞給其他函數,最重要的是能夠被函數動態(tài)創(chuàng)建和返回。
- Go語言中的閉包
????????Go語言中的閉包同樣也會引用到函數外的變量。閉包的實現確保只要閉包還被使用,那么 被閉包引用的變量會一直存在.
package main import ( "fmt" ) func main() { var j int = 5 a := func()(func()) { var i int = 10 return func() { fmt.Printf("i, j: %d, %d\n", i, j) } }() a() j *= 2 a() }上述例子的執(zhí)行結果是:
i, j: 10, 5 i, j: 10, 10????????在上面的例子中,變量a指向的閉包函數引用了局部變量i和j,i的值被隔離,在閉包外不 能被修改,改變j的值以后,再次調用a,發(fā)現結果是修改過的值。
????????在變量a指向的閉包函數中,只有內部的匿名函數才能訪問變量i,而無法通過其他途徑訪問 到,因此保證了i的安全性
2.6 錯誤處理
????????錯誤處理是學習任何編程語言都需要考慮的一個重要話題。在早期的語言中,錯誤處理不是 語言規(guī)范的一部分,通常只作為一種編程范式存在,比如C語言中的errno。但自C++語言以來, 語言層面上會增加錯誤處理的支持,比如異常(exception)的概念和try-catch關鍵字的引入。 Go語言在此功能上考慮得更為深遠。漂亮的錯誤處理規(guī)范是Go語言最大的亮點之一
2.6.1 error接口
????????Go語言引入了一個關于錯誤處理的標準模式,即error接口,該接口的定義如下:
type error interface { Error() string }????????對于大多數函數,如果要返回錯誤,大致上都可以定義為如下模式,將error作為多種返回 值中的最后一個,但這并非是強制要求:
func Foo(param int)(n int, err error) { // ... }調用時的代碼建議按如下方式處理錯誤情況:
n, err := Foo(0) if err != nil { // 錯誤處理 } else { // 使用返回值n }????????下面我用Go庫中的實際代碼來示范如何使用自定義的error類型。
????????首先,定義一個用于承載錯誤信息的類型。因為Go語言中接口的靈活性,你根本不需要從 error接口繼承或者像Java一樣需要使用implements來明確指定類型和接口之間的關系,具體 代碼如下:
type PathError struct { Op stringPath string Err error }????????如果這樣的話,編譯器又怎能知道PathError可以當一個error來傳遞呢?關鍵在于下面的 代碼實現了Error()方法:
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }????????關于接口的更多細節(jié),可以參見3.5節(jié)。之后就可以直接返回PathError變量了,比如在下 面的代碼中,當syscall.Stat()失敗返回err時,將該err包裝到一個PathError對象中返回:
func Stat(name string) (fi FileInfo, err error) { var stat syscall.Stat_t err = syscall.Stat(name, &stat) if err != nil { return nil, &PathError{"stat", name, err} } return fileInfoFromStat(&stat, name), nil }????????如果在處理錯誤時獲取詳細信息,而不僅僅滿足于打印一句錯誤信息,那就需要用到類型轉 換知識了:
fi, err := os.Stat("a.txt") if err != nil { if e, ok := err.(*os.PathError); ok && e.Err != nil { // 獲取PathError類型變量e中的其他信息并處理} }????????這就是Go中error類型的使用方法。與其他語言中的異常相比,Go的處理相對比較直觀、 簡單。
????????關于類型轉換的更多知識,在第3章中也會有更進一步的闡述。
2.6.2 defer
????????關鍵字defer是Go語言引入的一個非常有意思的特性,相信很多C++程序員都寫過類似下面 這樣的代碼:
class file_closer { FILE _f; public: file_closer(FILE f) : _f(f) {} ~file_closer() { if (f) fclose(f); } };????????然后在需要使用的地方這么寫:
void f() { FILE f = open_file("file.txt"); // 打開一個文件句柄file_closer _closer(f); // 對f句柄進行操作 }????????為什么需要file_closer這么個包裝類呢?因為如果沒有這個類,代碼中所有退出函數的 環(huán)節(jié),比如每一個可能拋出異常的地方,每一個return的位置,都需要關掉之前打開的文件句 柄。即使你頭腦清晰,想明白了每一個分支和可能出錯的條件,在該關閉的地方都關閉了,怎么 保證你的后繼者也能做到同樣水平?大量莫名其妙的問題就出現了。
????????在C/C++中還有另一種解決方案。開發(fā)者可以將需要釋放的資源變量都聲明在函數的開頭部 分,并在函數的末尾部分統一釋放資源。函數需要退出時,就必須使用goto語句跳轉到指定位 置先完成資源清理工作,而不能調用return語句直接返回。
????????這種方案是可行的,也仍然在被使用著,但存在非常大的維護性問題。而Go語言使用defer 關鍵字簡簡單單地解決了這個問題,比如以下的例子:
func CopyFile(dst, src string) (w int64, err error) { srcFile, err := os.Open(src) if err != nil { return } defer srcFile.Close() dstFile, err := os.Create(dstName) if err != nil { return } defer dstFile.Close() return io.Copy(dstFile, srcFile) }????????即使其中的Copy()函數拋出異常,Go仍然會保證dstFile和srcFile會被正常關閉。
????????如果覺得一句話干不完清理的工作,也可以使用在defer后加一個匿名函數的做法:
defer func() { // 做你復雜的清理工作 } ()????????另外,一個函數中可以存在多個defer語句,因此需要注意的是,defer語句的調用是遵照 先進后出的原則,即最后一個defer語句將最先被執(zhí)行。只不過,當你需要為defer語句到底哪 個先執(zhí)行這種細節(jié)而煩惱的時候,說明你的代碼架構可能需要調整一下了。
2.6.3 panic()和recover()
????????Go語言引入了兩個內置函數panic()和recover()以報告和處理運行時錯誤和程序中的錯誤場景:
func panic(interface{}) func recover() interface{}????????當在一個函數執(zhí)行過程中調用panic()函數時,正常的函數執(zhí)行流程將立即終止,但函數中 之前使用defer關鍵字延遲執(zhí)行的語句將正常展開執(zhí)行,之后該函數將返回到調用函數,并導致 逐層向上執(zhí)行panic流程,直至所屬的goroutine中所有正在執(zhí)行的函數被終止。錯誤信息將被報 告,包括在調用panic()函數時傳入的參數,這個過程稱為錯誤處理流程。
????????從panic()的參數類型interface{}我們可以得知,該函數接收任意類型的數據,比如整 型、字符串、對象等。調用方法很簡單,下面為幾個例子:
panic(404) panic("network broken") panic(Error("file not exists"))????????recover()函數用于終止錯誤處理流程。一般情況下,recover()應該在一個使用defer 關鍵字的函數中執(zhí)行以有效截取錯誤處理流程。如果沒有在發(fā)生異常的goroutine中明確調用恢復 過程(使用recover關鍵字),會導致該goroutine所屬的進程打印異常信息后直接退出。
????????以下為一個常見的場景。
????????我們對于foo()函數的執(zhí)行要么心里沒底感覺可能會觸發(fā)錯誤處理,或者自己在其中明確加 入了按特定條件觸發(fā)錯誤處理的語句,那么可以用如下方式在調用代碼中截取recover():
defer func() { if r := recover(); r != nil { log.Printf("Runtime error caught: %v", r) } }() foo()????????無論foo()中是否觸發(fā)了錯誤處理流程,該匿名defer函數都將在函數退出時得到執(zhí)行。假 如foo()中觸發(fā)了錯誤處理流程,recover()函數執(zhí)行將使得該錯誤處理過程終止。如果錯誤處 理流程被觸發(fā)時,程序傳給panic函數的參數不為nil,則該函數還會打印詳細的錯誤信息。
2.7 完整示例
????????現在我們用從本章學到的知識來實現一個完整的程序。我們準備開發(fā)一個排序算法的比較程序, 從命令行指定輸入的數據文件和輸出的數據文件,并指定對應的排序算法。該程序的用法如下所示:
USAGE: sorter –i <in> –o <out> –a <qsort|bubblesort>????????一個具體的執(zhí)行過程如下:
$ ./sorter –I in.dat –o out.dat –a qsort The sorting process costs 10us to complete.當然,如果輸入不合法,應該給出對應的提示,接下來我們一步步實現這個程序。
2.7.1 程序結構
????????我們將該函數分為兩類:主程序和排序算法函數。每個排序算法都包裝成一個靜態(tài)庫,雖然 現在看起來似乎有些多此一舉,但這只是為了順便演示包之間的依賴方法。
????????假設我們的程序根目錄為~/goyard/sorter,因此需要在環(huán)境變量GOPATH中添加這個路徑。根 目錄的結構如下:
<sorter> ├─<src> ├─<sorter> ├─sorter.go ├─<algorithms> ├─<qsort> ├─qsort.go ├─qsort_test.go ├─<bubblesort> ├─bubblesort.go ├─bubblesort_test.go ├─<pkg> ├─<bin>????????其中sorter.go是主程序,qsort.go用于實現快速排序,bubblesort.go用于實現冒泡排序。
????????下面我們先定義一下排序算法函數的函數原型:
func QuickSort(in []int)[]int func BubbleSort(in []int)[]int2.7.2 主程序
我們的主程序需要做的工作包含以下幾點:
接下來我們一步步地編寫程序。
1. 命令行參數
????????Go語言標準庫提供了用于快迅解析命令行參數的flag包。對于本示例的參數需求,我們可 以利用flag包進行實現,
package main import "flag" import "fmt" var infile *string = flag.String("i", "infile", "File contains values for sorting") var outfile *string = flag.String("o", "outfile", "File to receive sorted values") var algorithm *string = flag.String("a", "qsort", "Sort algorithm") func main() { flag.Parse() if infile != nil { fmt.Println("infile =", *infile, "outfile =", *outfile, "algorithm =", *algorithm) } }????????因為這個程序需要輸入參數,所以我們不能直接用go run來跑,而是需要先編譯出二進制 程序。可以用go build來完成這個過程:
$ go build sorter.go $ ./sorter -i unsorted.dat -o sorted.dat -a bubblesort infile = unsorted.dat outfile = sorted.dat algorithm = bubblesort????????可以看到,傳入的各個命令行參數已經被正確讀取到各個變量中。flag包使用起來非常方 便,大大簡化了C語言時代解析命令行參數的過程。
2. 讀取輸入文件
????????我們需要先從一個文件中把包含的內容讀取到數組中,將該數組排好序后再寫回到另一個文 件中,因此還需要學習如何在Go語言中操作文件。
????????我們先設計輸入文件的格式。輸入文件是一個純文本文件,每一行是一個需要被排序的數字。 下面是一個示例的unsorted.dat文件內容:
123 3064 3 64 490????????然后需要逐行從這個文件中讀取內容,并解析為int類型的數據,再添加到一個int類型的 數組切片中。接下來我們實現這部分功能.
package main import "bufio" import "flag" import "fmt" import "io" import "os" import "strconv" var infile *string = flag.String("i", "unsorted.dat", "File contains values for sorting") var outfile *string = flag.String("o", "sorted.dat", "File to receive sorted values") var algorithm *string = flag.String("a", "qsort", "Sort algorithm") func readValues(infile string)(values []int, err error) { file, err := os.Open(infile) if err != nil { fmt.Println("Failed to open the input file ", infile) return } defer file.Close() br := bufio.NewReader(file) values = make([]int, 0) for { line, isPrefix, err1 := br.ReadLine() if err1 != nil { if err1 != io.EOF { err = err1 } break } if isPrefix { fmt.Println("A too long line, seems unexpected.") return } str := string(line) // 轉換字符數組為字符串value, err1 := strconv.Atoi(str) if err1 != nil { err = err1 return } values = append(values, value) } return } func main() { flag.Parse() if infile != nil { fmt.Println("infile =", *infile, "outfile =", *outfile, "algorithm =", *algorithm) }values, err := readValues(*infile) if err == nil { fmt.Println("Read values:", values) } else { fmt.Println(err) } }????????在實現readValues()函數的過程中,我們用到了os、io、bufio和strconv等Go語言標 準庫中的包,用于文件讀寫和字符串處理。熟練掌握這些包的基本用法,將會大幅度提高使用 Go語言的工作效率。
????????我們還示范了數組切片的使用,并使用defer關鍵字以確保關閉文件句柄。
3. 寫到輸出文件
????????在數據處理結束后,我們需要將排序結果輸出到另一個文本文件。這個過程比較簡單,因此 這里我們只列出writeValues()函數的實現,讀者可以自行對照Go語言標準庫以熟悉相關包的 用法。
func writeValues(values []int, outfile string) error { file, err := os.Create(outfile) if err != nil { fmt.Println("Failed to create the output file ", outfile) return err } defer file.Close() for _, value := range values { str := strconv.Itoa(value) file.WriteString(str + "\n") } return nil }2.7.3 算法實現
????????接下來我們就實現排序算法。因為算法本身并不在本書討論的范疇,所以就不再解釋冒泡排 序和快速排序的算法原理。
????????冒泡排序算法位于bubblesort.go這個源文件中,快速排序算法則位于qsort.go文件中。對于這 種純算法的模塊,我們應該自然而然地為其編寫單元測試模塊。我們在第7章中將專門介紹單元 測試的相關內容。
1. 冒泡排序
在冒泡排序中,包含一個具體的算法實現源文件和一個單元測試文件,
// bubblesort.go package bubblesort func BubbleSort(values []int) { flag := truefor i := 0; i <len(values) - 1; i ++ { flag = truefor j := 0; j <len(values) - i - 1; j++ { if values[j] > values[j + 1] { values[j], values[j + 1] = values[j + 1], values[j] flag = false} // end if } // end for j = ... if flag == true { break } } // end for i = ... } // bubble_test.go package bubblesort import "testing" func TestBubbleSort1(t *testing.T) { values := []int{5, 4, 3, 2, 1} BubbleSort(values) if values[0] != 1 || values[1] != 2 || values[2] != 3 || values[3] != 4 || values[4] !=5 { t.Error("BubbleSort() failed. Got", values, "Expected 1 2 3 4 5") } } func TestBubbleSort2(t *testing.T) { values := []int{5, 5, 3, 2, 1} BubbleSort(values) if values[0] != 1 || values[1] != 2 || values[2] != 3 || values[3] != 5 || values[4] !=5 { t.Error("BubbleSort() failed. Got", values, "Expected 1 2 3 5 5") } } func TestBubbleSort3(t *testing.T) { values := []int{5} BubbleSort(values) if values[0] != 5 { t.Error("BubbleSort() failed. Got", values, "Expected 5") } }2. 快速排序
????????與冒泡排序相同,快速排序也包含一個具體的算法實現源文件和一個單元測試文件,
// qsort.go package qsort func quickSort(values []int, left, right int) { temp := values[left] p := left i, j := left, right for i <= j { for j >= p && values[j] >= temp { j-- } if j >= p { values[p] = values[j] p = j } if values[i] <= temp && i <= p { i++ } if i <= p { values[p] = values[i] p = i } } values[p] = temp if p - left > 1 { quickSort(values, left, p - 1) } if right - p > 1 { quickSort(values, p + 1, right) } } func QuickSort(values []int) { quickSort(values, 0, len(values) - 1) } // qsort_test.go package qsort import "testing" func TestQuickSort1(t *testing.T) { values := []int{5, 4, 3, 2, 1} QuickSort(values) if values[0] != 1 || values[1] != 2 || values[2] != 3 || values[3] != 4 || values[4] !=5 { t.Error("QuickSort() failed. Got", values, "Expected 1 2 3 4 5") } } func TestQuickSort2(t *testing.T) { values := []int{5, 5, 3, 2, 1} QuickSort(values) if values[0] != 1 || values[1] != 2 || values[2] != 3 || values[3] != 5 || values[4] !=5 { t.Error("QuickSort() failed. Got", values, "Expected 1 2 3 5 5") } } func TestQuickSort3(t *testing.T) { values := []int{5} QuickSort(values) if values[0] != 5 { t.Error("QuickSort() failed. Got", values, "Expected 5") } }2.7.4 主程序
????????現在我們可以在主程序加入對算法的調用以及函數的運行計時,最終版本的sorter.go
package main import "bufio" import "flag" import "fmt" import "io" import "os" import "strconv" import "time" import "algorithm/bubblesort" import "algorithm/qsort" var infile *string = flag.String("i", "unsorted.dat", "File contains values for sorting") var outfile *string = flag.String("o", "sorted.dat", "File to receive sorted values") var algorithm *string = flag.String("a", "qsort", "Sort algorithm") func readValues(infile string)(values []int, err error) { file, err := os.Open(infile) if err != nil { fmt.Println("Failed to open the input file ", infile) return } defer file.Close() br := bufio.NewReader(file) values = make([]int, 0) for { line, isPrefix, err1 := br.ReadLine() if err1 != nil { if err1 != io.EOF { err = err1 } break } if isPrefix { fmt.Println("A too long line, seems unexpected.") return } str := string(line) // 轉換字符數組為字符串value, err1 := strconv.Atoi(str) if err1 != nil { err = err1 return } values = append(values, value) } return } func writeValues(values []int, outfile string) error { file, err := os.Create(outfile) if err != nil { fmt.Println("Failed to create the output file ", outfile) return err }defer file.Close() for _, value := range values { str := strconv.Itoa(value) file.WriteString(str + "\n") } return nil } func main() { flag.Parse() if infile != nil { fmt.Println("infile =", *infile, "outfile =", *outfile, "algorithm =", *algorithm) } values, err := readValues(*infile)if err == nil { t1 := time.Now() switch *algorithm { case "qsort": qsort.QuickSort(values) case "bubblesort": bubblesort.BubbleSort(values) default:fmt.Println("Sorting algorithm", *algorithm, "is either unknown or unsupported.") } t2 := time.Now() fmt.Println("The sorting process costs", t2.Sub(t1), "to complete.") writeValues(values, *outfile) } else { fmt.Println(err) } }2.7.5 構建與執(zhí)行
????????至此,本章的示例已經全部完成。在確認已經設置好GOPATH后,我們可以直接運行以下命 令來構建和測試程序:
$ echo $GOPATH ~/goyard/sorter $ go build algorithm/qsort $ go build algorithm/bubblesort $ go test algorithm/qsort ok algorithm/qsort0.007s $ go test algorithm/bubblesort ok algorithm/bubblesort0.013s $ go install algorithm/qsort $ go install algorithm/bubblesort $ go build sorter $ go install sorter????????如果沒有出現任何問題,那么通過執(zhí)行這些命令,我們應該能夠在src的同一級目錄下看到兩 個目錄——bin和pkg,其中pkg目錄下放置的是bubblesort.a和qsort.a, bin目錄下放置的是sorter的二 進制可執(zhí)行文件。
????????因為sorter接受的是一個文件格式的輸入,所以需要準備這樣的一個文件。我們可以在sorter 所在的bin目錄內創(chuàng)建一個unsorted.dat文本文件,按一行一個整數的方式填入一些數據后保存。 sorted.dat會由程序自動創(chuàng)建,因此不需要事先創(chuàng)建。
????????接下來我們演示如何運行這個程序,并查看執(zhí)行的結果:
$ cd bin $ ls sorterunsorted.dat $ cat unsorted.dat 123 3064 3 64 490 1 23 5331 2 7 4 2 132 $ ./sorter -i unsorted.dat -o sorted.dat -a qsort infile = unsorted.dat outfile = sorted.dat algorithm = qsort The sorting process costs 3us to complete. $ ./sorter -i unsorted.dat -o sorted.dat -a bubblesort infile = unsorted.dat outfile = sorted.dat algorithm = bubblesort The sorting process costs 2us to complete. $ cat sorted.dat 1 2 2 3 4 7 23 64 123 132 490 3064 5331????????可以看到,結果已經被正確排序并寫入到sorted.dat文件中,至此我們的程序也算是完整 了。這個程序不僅僅演示了本章學到的大部分內容,還順便示范了Go語言標準庫中多個常用 包的用法。
????????相信讀者基于這個程序框架可以快速使用Go語言來解決自己在工作和學習中遇到的實際 問題。
2.8 小結
????????本章我們詳細講解了Go語言順序編程的相關語法,從這些語法特征可以很容易看出C語言的 影子(畢竟肯·湯普森也是C語言的設計者),但Go又利用一系列新增特性很好地讓Go程序員避 免重復之前C程序員面臨的眾多問題。看完這一章,你應該也可以理解為什么很多人評價Go語言 為“更好的C語言”。
?
總結
以上是生活随笔為你收集整理的GO语言-第二节-顺序编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 散不尽的迷雾柔情
- 下一篇: uniapp 简单表单布局1