{% raw %}
# 2.2 Go基礎
這小節我們將要介紹如何定義變量、常量、Go內置類型以及Go程序設計中的一些技巧。
## 定義變量
Go語言里面定義變量有多種方式。
使用`var`關鍵字是Go最基本的定義變量方式,與C語言不同的是Go把變量類型放在變量名后面:
//定義一個名稱為“variableName”,類型為"type"的變量
var variableName type
定義多個變量
//定義三個類型都是“type”的變量
var vname1, vname2, vname3 type
定義變量并初始化值
//初始化“variableName”的變量為“value”值,類型是“type”
var variableName type = value
同時初始化多個變量
/*
定義三個類型都是"type"的變量,并且分別初始化為相應的值
vname1為v1,vname2為v2,vname3為v3
*/
var vname1, vname2, vname3 type= v1, v2, v3
你是不是覺得上面這樣的定義有點繁瑣?沒關系,因為Go語言的設計者也發現了,有一種寫法可以讓它變得簡單一點。我們可以直接忽略類型聲明,那么上面的代碼變成這樣了:
/*
定義三個變量,它們分別初始化為相應的值
vname1為v1,vname2為v2,vname3為v3
然后Go會根據其相應值的類型來幫你初始化它們
*/
var vname1, vname2, vname3 = v1, v2, v3
你覺得上面的還是有些繁瑣?好吧,我也覺得。讓我們繼續簡化:
/*
定義三個變量,它們分別初始化為相應的值
vname1為v1,vname2為v2,vname3為v3
編譯器會根據初始化的值自動推導出相應的類型
*/
vname1, vname2, vname3 := v1, v2, v3
現在是不是看上去非常簡潔了?`:=`這個符號直接取代了`var`和`type`,這種形式叫做簡短聲明。不過它有一個限制,那就是它只能用在函數內部;在函數外部使用則會無法編譯通過,所以一般用`var`方式來定義全局變量。
`_`(下劃線)是個特殊的變量名,任何賦予它的值都會被丟棄。在這個例子中,我們將值`35`賦予`b`,并同時丟棄`34`:
_, b := 34, 35
Go對于已聲明但未使用的變量會在編譯階段報錯,比如下面的代碼就會產生一個錯誤:聲明了`i`但未使用。
package main
func main() {
var i int
}
## 常量
所謂常量,也就是在程序編譯階段就確定下來的值,而程序在運行時無法改變該值。在Go程序中,常量可定義為數值、布爾值或字符串等類型。
它的語法如下:
const constantName = value
//如果需要,也可以明確指定常量的類型:
const Pi float32 = 3.1415926
下面是一些常量聲明的例子:
const Pi = 3.1415926
const i = 10000
const MaxThread = 10
const prefix = "astaxie_"
Go 常量和一般程序語言不同的是,可以指定相當多的小數位數(例如200位),
若指定給float32自動縮短為32bit,指定給float64自動縮短為64bit,詳情參考[鏈接](http://golang.org/ref/spec#Constants)
## 內置基礎類型
### Boolean
在Go中,布爾值的類型為`bool`,值是`true`或`false`,默認為`false`。
//示例代碼
var isActive bool // 全局變量聲明
var enabled, disabled = true, false // 忽略類型的聲明
func test() {
var available bool // 一般聲明
valid := false // 簡短聲明
available = true // 賦值操作
}
### 數值類型
整數類型有無符號和帶符號兩種。Go同時支持`int`和`uint`,這兩種類型的長度相同,但具體長度取決于不同編譯器的實現。Go里面也有直接定義好位數的類型:`rune`, `int8`, `int16`, `int32`, `int64`和`byte`, `uint8`, `uint16`, `uint32`, `uint64`。其中`rune`是`int32`的別稱,`byte`是`uint8`的別稱。
>需要注意的一點是,這些類型的變量之間不允許互相賦值或操作,不然會在編譯時引起編譯器報錯。
>
>如下的代碼會產生錯誤:invalid operation: a + b (mismatched types int8 and int32)
>
>> var a int8
>> var b int32
>> c:=a + b
>
>另外,盡管int的長度是32 bit, 但int 與 int32并不可以互用。
浮點數的類型有`float32`和`float64`兩種(沒有`float`類型),默認是`float64`。
這就是全部嗎?No!Go還支持復數。它的默認類型是`complex128`(64位實數+64位虛數)。如果需要小一些的,也有`complex64`(32位實數+32位虛數)。復數的形式為`RE + IMi`,其中`RE`是實數部分,`IM`是虛數部分,而最后的`i`是虛數單位。下面是一個使用復數的例子:
var c complex64 = 5+5i
//output: (5+5i)
fmt.Printf("Value is: %v", c)
### 字符串
我們在上一節中講過,Go中的字符串都是采用`UTF-8`字符集編碼。字符串是用一對雙引號(`""`)或反引號(`` ` `` `` ` ``)括起來定義,它的類型是`string`。
//示例代碼
var frenchHello string // 聲明變量為字符串的一般方法
var emptyString string = "" // 聲明了一個字符串變量,初始化為空字符串
func test() {
no, yes, maybe := "no", "yes", "maybe" // 簡短聲明,同時聲明多個變量
japaneseHello := "Konichiwa" // 同上
frenchHello = "Bonjour" // 常規賦值
}
在Go中字符串是不可變的,例如下面的代碼編譯時會報錯:cannot assign to s[0]
var s string = "hello"
s[0] = 'c'
但如果真的想要修改怎么辦呢?下面的代碼可以實現:
s := "hello"
c := []byte(s) // 將字符串 s 轉換為 []byte 類型
c[0] = 'c'
s2 := string(c) // 再轉換回 string 類型
fmt.Printf("%s\n", s2)
Go中可以使用`+`操作符來連接兩個字符串:
s := "hello,"
m := " world"
a := s + m
fmt.Printf("%s\n", a)
修改字符串也可寫為:
s := "hello"
s = "c" + s[1:] // 字符串雖不能更改,但可進行切片操作
fmt.Printf("%s\n", s)
如果要聲明一個多行的字符串怎么辦?可以通過`` ` ``來聲明:
m := `hello
world`
`` ` `` 括起的字符串為Raw字符串,即字符串在代碼中的形式就是打印時的形式,它沒有字符轉義,換行也將原樣輸出。例如本例中會輸出:
hello
world
### 錯誤類型
Go內置有一個`error`類型,專門用來處理錯誤信息,Go的`package`里面還專門有一個包`errors`來處理錯誤:
err := errors.New("emit macho dwarf: elf header corrupted")
if err != nil {
fmt.Print(err)
}
### Go數據底層的存儲
下面這張圖來源于[Russ Cox Blog](http://research.swtch.com/)中一篇介紹[Go數據結構](http://research.swtch.com/godata)的文章,大家可以看到這些基礎類型底層都是分配了一塊內存,然后存儲了相應的值。

圖2.1 Go數據格式的存儲
## 一些技巧
### 分組聲明
在Go語言中,同時聲明多個常量、變量,或者導入多個包時,可采用分組的方式進行聲明。
例如下面的代碼:
import "fmt"
import "os"
const i = 100
const pi = 3.1415
const prefix = "Go_"
var i int
var pi float32
var prefix string
可以分組寫成如下形式:
import(
"fmt"
"os"
)
const(
i = 100
pi = 3.1415
prefix = "Go_"
)
var(
i int
pi float32
prefix string
)
### iota枚舉
Go里面有一個關鍵字`iota`,這個關鍵字用來聲明`enum`的時候采用,它默認開始值是0,const中每增加一行加1:
const(
x = iota // x == 0
y = iota // y == 1
z = iota // z == 2
w // 常量聲明省略值時,默認和之前一個值的字面相同。這里隱式地說w = iota,因此w == 3。其實上面y和z可同樣不用"= iota"
)
const v = iota // 每遇到一個const關鍵字,iota就會重置,此時v == 0
const (
e, f, g = iota, iota, iota //e=0,f=0,g=0 iota在同一行值相同
)
const (
a = iota a=0
b = "B"
c = iota //c=2
d,e,f = iota,iota,iota //d=3,e=3,f=3
g //g = 4
)
>除非被顯式設置為其它值或`iota`,每個`const`分組的第一個常量被默認設置為它的0值,第二及后續的常量被默認設置為它前面那個常量的值,如果前面那個常量的值是`iota`,則它也被設置為`iota`。
### Go程序設計的一些規則
Go之所以會那么簡潔,是因為它有一些默認的行為:
- 大寫字母開頭的變量是可導出的,也就是其它包可以讀取的,是公用變量;小寫字母開頭的就是不可導出的,是私有變量。
- 大寫字母開頭的函數也是一樣,相當于`class`中的帶`public`關鍵詞的公有函數;小寫字母開頭的就是有`private`關鍵詞的私有函數。
## array、slice、map
### array
`array`就是數組,它的定義方式如下:
var arr [n]type
在`[n]type`中,`n`表示數組的長度,`type`表示存儲元素的類型。對數組的操作和其它語言類似,都是通過`[]`來進行讀取或賦值:
var arr [10]int // 聲明了一個int類型的數組
arr[0] = 42 // 數組下標是從0開始的
arr[1] = 13 // 賦值操作
fmt.Printf("The first element is %d\n", arr[0]) // 獲取數據,返回42
fmt.Printf("The last element is %d\n", arr[9]) //返回未賦值的最后一個元素,默認返回0
由于長度也是數組類型的一部分,因此`[3]int`與`[4]int`是不同的類型,數組也就不能改變長度。數組之間的賦值是值的賦值,即當把一個數組作為參數傳入函數的時候,傳入的其實是該數組的副本,而不是它的指針。如果要使用指針,那么就需要用到后面介紹的`slice`類型了。
數組可以使用另一種`:=`來聲明
a := [3]int{1, 2, 3} // 聲明了一個長度為3的int數組
b := [10]int{1, 2, 3} // 聲明了一個長度為10的int數組,其中前三個元素初始化為1、2、3,其它默認為0
c := [...]int{4, 5, 6} // 可以省略長度而采用`...`的方式,Go會自動根據元素個數來計算長度
也許你會說,我想數組里面的值還是數組,能實現嗎?當然咯,Go支持嵌套數組,即多維數組。比如下面的代碼就聲明了一個二維數組:
// 聲明了一個二維數組,該數組以兩個數組作為元素,其中每個數組中又有4個int類型的元素
doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}
// 上面的聲明可以簡化,直接忽略內部的類型
easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}
數組的分配如下所示:

圖2.2 多維數組的映射關系
### slice
在很多應用場景中,數組并不能滿足我們的需求。在初始定義數組時,我們并不知道需要多大的數組,因此我們就需要“動態數組”。在Go里面這種數據結構叫`slice`
`slice`并不是真正意義上的動態數組,而是一個引用類型。`slice`總是指向一個底層`array`,`slice`的聲明也可以像`array`一樣,只是不需要長度。
// 和聲明array一樣,只是少了長度
var fslice []int
接下來我們可以聲明一個`slice`,并初始化數據,如下所示:
slice := []byte {'a', 'b', 'c', 'd'}
`slice`可以從一個數組或一個已經存在的`slice`中再次聲明。`slice`通過`array[i:j]`來獲取,其中`i`是數組的開始位置,`j`是結束位置,但不包含`array[j]`,它的長度是`j-i`。
// 聲明一個含有10個元素元素類型為byte的數組
var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 聲明兩個含有byte的slice
var a, b []byte
// a指向數組的第3個元素開始,并到第五個元素結束,
a = ar[2:5]
//現在a含有的元素: ar[2]、ar[3]和ar[4]
// b是數組ar的另一個slice
b = ar[3:5]
// b的元素是:ar[3]和ar[4]
>注意`slice`和數組在聲明時的區別:聲明數組時,方括號內寫明了數組的長度或使用`...`自動計算長度,而聲明`slice`時,方括號內沒有任何字符。
它們的數據結構如下所示

圖2.3 slice和array的對應關系圖
slice有一些簡便的操作
- `slice`的默認開始位置是0,`ar[:n]`等價于`ar[0:n]`
- `slice`的第二個序列默認是數組的長度,`ar[n:]`等價于`ar[n:len(ar)]`
- 如果從一個數組里面直接獲取`slice`,可以這樣`ar[:]`,因為默認第一個序列是0,第二個是數組的長度,即等價于`ar[0:len(ar)]`
下面這個例子展示了更多關于`slice`的操作:
// 聲明一個數組
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 聲明兩個slice
var aSlice, bSlice []byte
// 演示一些簡便操作
aSlice = array[:3] // 等價于aSlice = array[0:3] aSlice包含元素: a,b,c
aSlice = array[5:] // 等價于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j
aSlice = array[:] // 等價于aSlice = array[0:10] 這樣aSlice包含了全部的元素
// 從slice中獲取slice
aSlice = array[3:7] // aSlice包含元素: d,e,f,g,len=4,cap=7
bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f
bSlice = aSlice[:3] // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f
bSlice = aSlice[0:5] // 對slice的slice可以在cap范圍內擴展,此時bSlice包含:d,e,f,g,h
bSlice = aSlice[:] // bSlice包含所有aSlice的元素: d,e,f,g
`slice`是引用類型,所以當引用改變其中元素的值時,其它的所有引用都會改變該值,例如上面的`aSlice`和`bSlice`,如果修改了`aSlice`中元素的值,那么`bSlice`相對應的值也會改變。
從概念上面來說`slice`像一個結構體,這個結構體包含了三個元素:
- 一個指針,指向數組中`slice`指定的開始位置
- 長度,即`slice`的長度
- 最大長度,也就是`slice`開始位置到數組的最后位置的長度
Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
Slice_a := Array_a[2:5]
上面代碼的真正存儲結構如下圖所示

圖2.4 slice對應數組的信息
對于`slice`有幾個有用的內置函數:
- `len` 獲取`slice`的長度
- `cap` 獲取`slice`的最大容量
- `append` 向`slice`里面追加一個或者多個元素,然后返回一個和`slice`一樣類型的`slice`
- `copy` 函數`copy`從源`slice`的`src`中復制元素到目標`dst`,并且返回復制的元素的個數
注:`append`函數會改變`slice`所引用的數組的內容,從而影響到引用同一數組的其它`slice`。
但當`slice`中沒有剩余空間(即`(cap-len) == 0`)時,此時將動態分配新的數組空間。返回的`slice`數組指針將指向這個空間,而原數組的內容將保持不變;其它引用此數組的`slice`則不受影響。
從Go1.2開始slice支持了三個參數的slice,之前我們一直采用這種方式在slice或者array基礎上來獲取一個slice
var array [10]int
slice := array[2:4]
這個例子里面slice的容量是8,新版本里面可以指定這個容量
slice = array[2:4:7]
上面這個的容量就是`7-2`,即5。這樣這個產生的新的slice就沒辦法訪問最后的三個元素。
如果slice是這樣的形式`array[:i:j]`,即第一個參數為空,默認值就是0。
### map
`map`也就是Python中字典的概念,它的格式為`map[keyType]valueType`
我們看下面的代碼,`map`的讀取和設置也類似`slice`一樣,通過`key`來操作,只是`slice`的`index`只能是`int`類型,而`map`多了很多類型,可以是`int`,可以是`string`及所有完全定義了`==`與`!=`操作的類型。
// 聲明一個key是字符串,值為int的字典,這種方式的聲明需要在使用之前使用make初始化
var numbers map[string]int
// 另一種map的聲明方式
numbers := make(map[string]int)
numbers["one"] = 1 //賦值
numbers["ten"] = 10 //賦值
numbers["three"] = 3
fmt.Println("第三個數字是: ", numbers["three"]) // 讀取數據
// 打印出來如:第三個數字是: 3
這個`map`就像我們平常看到的表格一樣,左邊列是`key`,右邊列是值
使用map過程中需要注意的幾點:
- `map`是無序的,每次打印出來的`map`都會不一樣,它不能通過`index`獲取,而必須通過`key`獲取
- `map`的長度是不固定的,也就是和`slice`一樣,也是一種引用類型
- 內置的`len`函數同樣適用于`map`,返回`map`擁有的`key`的數量
- `map`的值可以很方便的修改,通過`numbers["one"]=11`可以很容易的把key為`one`的字典值改為`11`
- `map`和其他基本型別不同,它不是thread-safe,在多個go-routine存取時,必須使用mutex lock機制
`map`的初始化可以通過`key:val`的方式初始化值,同時`map`內置有判斷是否存在`key`的方式
通過`delete`刪除`map`的元素:
// 初始化一個字典
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }
// map有兩個返回值,第二個返回值,如果不存在key,那么ok為false,如果存在ok為true
csharpRating, ok := rating["C#"]
if ok {
fmt.Println("C# is in the map and its rating is ", csharpRating)
} else {
fmt.Println("We have no rating associated with C# in the map")
}
delete(rating, "C") // 刪除key為C的元素
上面說過了,`map`也是一種引用類型,如果兩個`map`同時指向一個底層,那么一個改變,另一個也相應的改變:
m := make(map[string]string)
m["Hello"] = "Bonjour"
m1 := m
m1["Hello"] = "Salut" // 現在m["hello"]的值已經是Salut了
### make、new操作
`make`用于內建類型(`map`、`slice` 和`channel`)的內存分配。`new`用于各種類型的內存分配。
內建函數`new`本質上說跟其它語言中的同名函數功能一樣:`new(T)`分配了零值填充的`T`類型的內存空間,并且返回其地址,即一個`*T`類型的值。用Go的術語說,它返回了一個指針,指向新分配的類型`T`的零值。有一點非常重要:
>`new`返回指針。
內建函數`make(T, args)`與`new(T)`有著不同的功能,make只能創建`slice`、`map`和`channel`,并且返回一個有初始值(非零)的`T`類型,而不是`*T`。本質來講,導致這三個類型有所不同的原因是指向數據結構的引用在使用前必須被初始化。例如,一個`slice`,是一個包含指向數據(內部`array`)的指針、長度和容量的三項描述符;在這些項目被初始化之前,`slice`為`nil`。對于`slice`、`map`和`channel`來說,`make`初始化了內部的數據結構,填充適當的值。
>`make`返回初始化后的(非零)值。
下面這個圖詳細的解釋了`new`和`make`之間的區別。

圖2.5 make和new對應底層的內存分配
## 零值
關于“零值”,所指并非是空值,而是一種“變量未填充前”的默認值,通常為0。
此處羅列 部分類型 的 “零值”
int 0
int8 0
int32 0
int64 0
uint 0x0
rune 0 //rune的實際類型是 int32
byte 0x0 // byte的實際類型是 uint8
float32 0 //長度為 4 byte
float64 0 //長度為 8 byte
bool false
string ""
## links
* [目錄](<preface.md>)
* 上一章: [你好,Go](<02.1.md>)
* 下一節: [流程和函數](<02.3.md>)
{% endraw %}
- 目錄
- Go環境配置
- Go安裝
- GOPATH 與工作空間
- Go 命令
- Go開發工具
- 小結
- Go語言基礎
- 你好,Go
- Go基礎
- 流程和函數
- struct
- 面向對象
- interface
- 并發
- 小結
- Web基礎
- web工作方式
- Go搭建一個簡單的web服務
- Go如何使得web工作
- Go的http包詳解
- 小結
- 表單
- 處理表單的輸入
- 驗證表單的輸入
- 預防跨站腳本
- 防止多次遞交表單
- 處理文件上傳
- 小結
- 訪問數據庫
- database/sql接口
- 使用MySQL數據庫
- 使用SQLite數據庫
- 使用PostgreSQL數據庫
- 使用beedb庫進行ORM開發
- NOSQL數據庫操作
- 小結
- session和數據存儲
- session和cookie
- Go如何使用session
- session存儲
- 預防session劫持
- 小結
- 文本文件處理
- XML處理
- JSON處理
- 正則處理
- 模板處理
- 文件操作
- 字符串處理
- 小結
- Web服務
- Socket編程
- WebSocket
- REST
- RPC
- 小結
- 安全與加密
- 預防CSRF攻擊
- 確保輸入過濾
- 避免XSS攻擊
- 避免SQL注入
- 存儲密碼
- 加密和解密數據
- 小結
- 國際化和本地化
- 設置默認地區
- 本地化資源
- 國際化站點
- 小結
- 錯誤處理,調試和測試
- 錯誤處理
- 使用GDB調試
- Go怎么寫測試用例
- 小結
- 部署與維護
- 應用日志
- 網站錯誤處理
- 應用部署
- 備份和恢復
- 小結
- 如何設計一個Web框架
- 項目規劃
- 自定義路由器設計
- controller設計
- 日志和配置設計
- 實現博客的增刪改
- 小結
- 擴展Web框架
- 靜態文件支持
- Session支持
- 表單支持
- 用戶認證
- 多語言支持
- pprof支持
- 小結
- 參考資料