這小節(jié)我們將要介紹如何定義變量、常量、Go內(nèi)置類型以及Go程序設(shè)計(jì)中的一些技巧。
Go語言里面定義變量有多種方式。
使用var關(guān)鍵字是Go最基本的定義變量方式,與C語言不同的是Go把變量類型放在變量名后面:
//定義一個(gè)名稱為“variableName”,類型為"type"的變量
var variableName type
定義多個(gè)變量
//定義三個(gè)類型都是“type”的變量
var vname1, vname2, vname3 type
定義變量并初始化值
//初始化“variableName”的變量為“value”值,類型是“type”
var variableName type = value
同時(shí)初始化多個(gè)變量
/*
定義三個(gè)類型都是"type"的變量,并且分別初始化為相應(yīng)的值
vname1為v1,vname2為v2,vname3為v3
*/
var vname1, vname2, vname3 type= v1, v2, v3
你是不是覺得上面這樣的定義有點(diǎn)繁瑣?沒關(guān)系,因?yàn)镚o語言的設(shè)計(jì)者也發(fā)現(xiàn)了,有一種寫法可以讓它變得簡單一點(diǎn)。我們可以直接忽略類型聲明,那么上面的代碼變成這樣了:
/*
定義三個(gè)變量,它們分別初始化為相應(yīng)的值
vname1為v1,vname2為v2,vname3為v3
然后Go會(huì)根據(jù)其相應(yīng)值的類型來幫你初始化它們
*/
var vname1, vname2, vname3 = v1, v2, v3
你覺得上面的還是有些繁瑣?好吧,我也覺得。讓我們繼續(xù)簡化:
/*
定義三個(gè)變量,它們分別初始化為相應(yīng)的值
vname1為v1,vname2為v2,vname3為v3
編譯器會(huì)根據(jù)初始化的值自動(dòng)推導(dǎo)出相應(yīng)的類型
*/
vname1, vname2, vname3 := v1, v2, v3
現(xiàn)在是不是看上去非常簡潔了?:=這個(gè)符號直接取代了var和type,這種形式叫做簡短聲明。不過它有一個(gè)限制,那就是它只能用在函數(shù)內(nèi)部;在函數(shù)外部使用則會(huì)無法編譯通過,所以一般用var方式來定義全局變量。
_(下劃線)是個(gè)特殊的變量名,任何賦予它的值都會(huì)被丟棄。在這個(gè)例子中,我們將值35賦予b,并同時(shí)丟棄34:
_, b := 34, 35
Go對于已聲明但未使用的變量會(huì)在編譯階段報(bào)錯(cuò),比如下面的代碼就會(huì)產(chǎn)生一個(gè)錯(cuò)誤:聲明了i但未使用。
package main
func main() {
var i int
}
所謂常量,也就是在程序編譯階段就確定下來的值,而程序在運(yùn)行時(shí)無法改變該值。在Go程序中,常量可定義為數(shù)值、布爾值或字符串等類型。
它的語法如下:
const constantName = value
//如果需要,也可以明確指定常量的類型:
const Pi float32 = 3.1415926
下面是一些常量聲明的例子:
const Pi = 3.1415926
const i = 10000
const MaxThread = 10
const prefix = "astaxie_"
Go 常量和一般程序語言不同的是,可以指定相當(dāng)多的小數(shù)位數(shù)(例如200位), 若指定給float32自動(dòng)縮短為32bit,指定給float64自動(dòng)縮短為64bit,詳情參考鏈接
在Go中,布爾值的類型為bool,值是true或false,默認(rèn)為false。
//示例代碼
var isActive bool // 全局變量聲明
var enabled, disabled = true, false // 忽略類型的聲明
func test() {
var available bool // 一般聲明
valid := false // 簡短聲明
available = true // 賦值操作
}
整數(shù)類型有無符號和帶符號兩種。Go同時(shí)支持int和uint,這兩種類型的長度相同,但具體長度取決于不同編譯器的實(shí)現(xiàn)。Go里面也有直接定義好位數(shù)的類型:rune, int8, int16, int32, int64和byte, uint8, uint16, uint32, uint64。其中rune是int32的別稱,byte是uint8的別稱。
需要注意的一點(diǎn)是,這些類型的變量之間不允許互相賦值或操作,不然會(huì)在編譯時(shí)引起編譯器報(bào)錯(cuò)。
如下的代碼會(huì)產(chǎn)生錯(cuò)誤:invalid operation: a + b (mismatched types int8 and int32)
var a int8
var b int32
c:=a + b
另外,盡管int的長度是32 bit, 但int 與 int32并不可以互用。
浮點(diǎn)數(shù)的類型有float32和float64兩種(沒有float類型),默認(rèn)是float64。
這就是全部嗎?No!Go還支持復(fù)數(shù)。它的默認(rèn)類型是complex128(64位實(shí)數(shù)+64位虛數(shù))。如果需要小一些的,也有complex64(32位實(shí)數(shù)+32位虛數(shù))。復(fù)數(shù)的形式為RE + IMi,其中RE是實(shí)數(shù)部分,IM是虛數(shù)部分,而最后的i是虛數(shù)單位。下面是一個(gè)使用復(fù)數(shù)的例子:
var c complex64 = 5+5i
//output: (5+5i)
fmt.Printf("Value is: %v", c)
我們在上一節(jié)中講過,Go中的字符串都是采用UTF-8字符集編碼。字符串是用一對雙引號("")或反引號(` `)括起來定義,它的類型是string。
//示例代碼
var frenchHello string // 聲明變量為字符串的一般方法
var emptyString string = "" // 聲明了一個(gè)字符串變量,初始化為空字符串
func test() {
no, yes, maybe := "no", "yes", "maybe" // 簡短聲明,同時(shí)聲明多個(gè)變量
japaneseHello := "Konichiwa" // 同上
frenchHello = "Bonjour" // 常規(guī)賦值
}
在Go中字符串是不可變的,例如下面的代碼編譯時(shí)會(huì)報(bào)錯(cuò):cannot assign to s[0]
var s string = "hello"
s[0] = 'c'
但如果真的想要修改怎么辦呢?下面的代碼可以實(shí)現(xiàn):
s := "hello"
c := []byte(s) // 將字符串 s 轉(zhuǎn)換為 []byte 類型
c[0] = 'c'
s2 := string(c) // 再轉(zhuǎn)換回 string 類型
fmt.Printf("%s\n", s2)
Go中可以使用+操作符來連接兩個(gè)字符串:
s := "hello,"
m := " world"
a := s + m
fmt.Printf("%s\n", a)
修改字符串也可寫為:
s := "hello"
s = "c" + s[1:] // 字符串雖不能更改,但可進(jìn)行切片操作
fmt.Printf("%s\n", s)
如果要聲明一個(gè)多行的字符串怎么辦?可以通過`來聲明:
m := `hello
world`
` 括起的字符串為Raw字符串,即字符串在代碼中的形式就是打印時(shí)的形式,它沒有字符轉(zhuǎn)義,換行也將原樣輸出。例如本例中會(huì)輸出:
hello
world
Go內(nèi)置有一個(gè)error類型,專門用來處理錯(cuò)誤信息,Go的package里面還專門有一個(gè)包errors來處理錯(cuò)誤:
err := errors.New("emit macho dwarf: elf header corrupted")
if err != nil {
fmt.Print(err)
}
下面這張圖來源于Russ Cox Blog中一篇介紹Go數(shù)據(jù)結(jié)構(gòu)的文章,大家可以看到這些基礎(chǔ)類型底層都是分配了一塊內(nèi)存,然后存儲(chǔ)了相應(yīng)的值。
http://wiki.jikexueyuan.com/project/go-web-programming/images/2.2.basic.png" alt="" />
圖2.1 Go數(shù)據(jù)格式的存儲(chǔ)
在Go語言中,同時(shí)聲明多個(gè)常量、變量,或者導(dǎo)入多個(gè)包時(shí),可采用分組的方式進(jìn)行聲明。
例如下面的代碼:
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
)
Go里面有一個(gè)關(guān)鍵字iota,這個(gè)關(guān)鍵字用來聲明enum的時(shí)候采用,它默認(rèn)開始值是0,const中每增加一行加1:
const(
x = iota // x == 0
y = iota // y == 1
z = iota // z == 2
w // 常量聲明省略值時(shí),默認(rèn)和之前一個(gè)值的字面相同。這里隱式地說w = iota,因此w == 3。其實(shí)上面y和z可同樣不用"= iota"
)
const v = iota // 每遇到一個(gè)const關(guān)鍵字,iota就會(huì)重置,此時(shí)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
)
除非被顯式設(shè)置為其它值或
iota,每個(gè)const分組的第一個(gè)常量被默認(rèn)設(shè)置為它的0值,第二及后續(xù)的常量被默認(rèn)設(shè)置為它前面那個(gè)常量的值,如果前面那個(gè)常量的值是iota,則它也被設(shè)置為iota。
Go之所以會(huì)那么簡潔,是因?yàn)樗幸恍┠J(rèn)的行為:
class中的帶public關(guān)鍵詞的公有函數(shù);小寫字母開頭的就是有private關(guān)鍵詞的私有函數(shù)。array就是數(shù)組,它的定義方式如下:
var arr [n]type
在[n]type中,n表示數(shù)組的長度,type表示存儲(chǔ)元素的類型。對數(shù)組的操作和其它語言類似,都是通過[]來進(jìn)行讀取或賦值:
var arr [10]int // 聲明了一個(gè)int類型的數(shù)組
arr[0] = 42 // 數(shù)組下標(biāo)是從0開始的
arr[1] = 13 // 賦值操作
fmt.Printf("The first element is %d\n", arr[0]) // 獲取數(shù)據(jù),返回42
fmt.Printf("The last element is %d\n", arr[9]) //返回未賦值的最后一個(gè)元素,默認(rèn)返回0
由于長度也是數(shù)組類型的一部分,因此[3]int與[4]int是不同的類型,數(shù)組也就不能改變長度。數(shù)組之間的賦值是值的賦值,即當(dāng)把一個(gè)數(shù)組作為參數(shù)傳入函數(shù)的時(shí)候,傳入的其實(shí)是該數(shù)組的副本,而不是它的指針。如果要使用指針,那么就需要用到后面介紹的slice類型了。
數(shù)組可以使用另一種:=來聲明
a := [3]int{1, 2, 3} // 聲明了一個(gè)長度為3的int數(shù)組
b := [10]int{1, 2, 3} // 聲明了一個(gè)長度為10的int數(shù)組,其中前三個(gè)元素初始化為1、2、3,其它默認(rèn)為0
c := [...]int{4, 5, 6} // 可以省略長度而采用`...`的方式,Go會(huì)自動(dòng)根據(jù)元素個(gè)數(shù)來計(jì)算長度
也許你會(huì)說,我想數(shù)組里面的值還是數(shù)組,能實(shí)現(xiàn)嗎?當(dāng)然咯,Go支持嵌套數(shù)組,即多維數(shù)組。比如下面的代碼就聲明了一個(gè)二維數(shù)組:
// 聲明了一個(gè)二維數(shù)組,該數(shù)組以兩個(gè)數(shù)組作為元素,其中每個(gè)數(shù)組中又有4個(gè)int類型的元素
doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}
// 上面的聲明可以簡化,直接忽略內(nèi)部的類型
easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}
數(shù)組的分配如下所示:
http://wiki.jikexueyuan.com/project/go-web-programming/images/2.2.array.png" alt="" />
圖2.2 多維數(shù)組的映射關(guān)系
在很多應(yīng)用場景中,數(shù)組并不能滿足我們的需求。在初始定義數(shù)組時(shí),我們并不知道需要多大的數(shù)組,因此我們就需要“動(dòng)態(tài)數(shù)組”。在Go里面這種數(shù)據(jù)結(jié)構(gòu)叫slice
slice并不是真正意義上的動(dòng)態(tài)數(shù)組,而是一個(gè)引用類型。slice總是指向一個(gè)底層array,slice的聲明也可以像array一樣,只是不需要長度。
// 和聲明array一樣,只是少了長度
var fslice []int
接下來我們可以聲明一個(gè)slice,并初始化數(shù)據(jù),如下所示:
slice := []byte {'a', 'b', 'c', 'd'}
slice可以從一個(gè)數(shù)組或一個(gè)已經(jīng)存在的slice中再次聲明。slice通過array[i:j]來獲取,其中i是數(shù)組的開始位置,j是結(jié)束位置,但不包含array[j],它的長度是j-i。
// 聲明一個(gè)含有10個(gè)元素元素類型為byte的數(shù)組
var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 聲明兩個(gè)含有byte的slice
var a, b []byte
// a指向數(shù)組的第3個(gè)元素開始,并到第五個(gè)元素結(jié)束,
a = ar[2:5]
//現(xiàn)在a含有的元素: ar[2]、ar[3]和ar[4]
// b是數(shù)組ar的另一個(gè)slice
b = ar[3:5]
// b的元素是:ar[3]和ar[4]
注意
slice和數(shù)組在聲明時(shí)的區(qū)別:聲明數(shù)組時(shí),方括號內(nèi)寫明了數(shù)組的長度或使用...自動(dòng)計(jì)算長度,而聲明slice時(shí),方括號內(nèi)沒有任何字符。
它們的數(shù)據(jù)結(jié)構(gòu)如下所示
http://wiki.jikexueyuan.com/project/go-web-programming/images/2.2.slice.png" alt="" />
圖2.3 slice和array的對應(yīng)關(guān)系圖
slice有一些簡便的操作
slice的默認(rèn)開始位置是0,ar[:n]等價(jià)于ar[0:n]slice的第二個(gè)序列默認(rèn)是數(shù)組的長度,ar[n:]等價(jià)于ar[n:len(ar)]slice,可以這樣ar[:],因?yàn)槟J(rèn)第一個(gè)序列是0,第二個(gè)是數(shù)組的長度,即等價(jià)于ar[0:len(ar)]下面這個(gè)例子展示了更多關(guān)于slice的操作:
// 聲明一個(gè)數(shù)組
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 聲明兩個(gè)slice
var aSlice, bSlice []byte
// 演示一些簡便操作
aSlice = array[:3] // 等價(jià)于aSlice = array[0:3] aSlice包含元素: a,b,c
aSlice = array[5:] // 等價(jià)于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j
aSlice = array[:] // 等價(jià)于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范圍內(nèi)擴(kuò)展,此時(shí)bSlice包含:d,e,f,g,h
bSlice = aSlice[:] // bSlice包含所有aSlice的元素: d,e,f,g
slice是引用類型,所以當(dāng)引用改變其中元素的值時(shí),其它的所有引用都會(huì)改變該值,例如上面的aSlice和bSlice,如果修改了aSlice中元素的值,那么bSlice相對應(yīng)的值也會(huì)改變。
從概念上面來說slice像一個(gè)結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體包含了三個(gè)元素:
slice指定的開始位置slice的長度最大長度,也就是slice開始位置到數(shù)組的最后位置的長度
Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
Slice_a := Array_a[2:5]
上面代碼的真正存儲(chǔ)結(jié)構(gòu)如下圖所示
http://wiki.jikexueyuan.com/project/go-web-programming/images/2.2.slice2.png" alt="" />
圖2.4 slice對應(yīng)數(shù)組的信息
對于slice有幾個(gè)有用的內(nèi)置函數(shù):
len 獲取slice的長度cap 獲取slice的最大容量append 向slice里面追加一個(gè)或者多個(gè)元素,然后返回一個(gè)和slice一樣類型的slicecopy 函數(shù)copy從源slice的src中復(fù)制元素到目標(biāo)dst,并且返回復(fù)制的元素的個(gè)數(shù)注:append函數(shù)會(huì)改變slice所引用的數(shù)組的內(nèi)容,從而影響到引用同一數(shù)組的其它slice。
但當(dāng)slice中沒有剩余空間(即(cap-len) == 0)時(shí),此時(shí)將動(dòng)態(tài)分配新的數(shù)組空間。返回的slice數(shù)組指針將指向這個(gè)空間,而原數(shù)組的內(nèi)容將保持不變;其它引用此數(shù)組的slice則不受影響。
從Go1.2開始slice支持了三個(gè)參數(shù)的slice,之前我們一直采用這種方式在slice或者array基礎(chǔ)上來獲取一個(gè)slice
var array [10]int
slice := array[2:4]
這個(gè)例子里面slice的容量是8,新版本里面可以指定這個(gè)容量
slice = array[2:4:7]
上面這個(gè)的容量就是7-2,即5。這樣這個(gè)產(chǎn)生的新的slice就沒辦法訪問最后的三個(gè)元素。
如果slice是這樣的形式array[:i:j],即第一個(gè)參數(shù)為空,默認(rèn)值就是0。
map也就是Python中字典的概念,它的格式為map[keyType]valueType
我們看下面的代碼,map的讀取和設(shè)置也類似slice一樣,通過key來操作,只是slice的index只能是`int`類型,而map多了很多類型,可以是int,可以是string及所有完全定義了==與!=操作的類型。
// 聲明一個(gè)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("第三個(gè)數(shù)字是: ", numbers["three"]) // 讀取數(shù)據(jù)
// 打印出來如:第三個(gè)數(shù)字是: 3
這個(gè)map就像我們平??吹降谋砀褚粯樱筮吜惺?code>key,右邊列是值
使用map過程中需要注意的幾點(diǎn):
map是無序的,每次打印出來的map都會(huì)不一樣,它不能通過index獲取,而必須通過key獲取map的長度是不固定的,也就是和slice一樣,也是一種引用類型len函數(shù)同樣適用于map,返回map擁有的key的數(shù)量map的值可以很方便的修改,通過numbers["one"]=11可以很容易的把key為one的字典值改為11map和其他基本型別不同,它不是thread-safe,在多個(gè)go-routine存取時(shí),必須使用mutex lock機(jī)制map的初始化可以通過key:val的方式初始化值,同時(shí)map內(nèi)置有判斷是否存在key的方式
通過delete刪除map的元素:
// 初始化一個(gè)字典
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }
// map有兩個(gè)返回值,第二個(gè)返回值,如果不存在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也是一種引用類型,如果兩個(gè)map同時(shí)指向一個(gè)底層,那么一個(gè)改變,另一個(gè)也相應(yīng)的改變:
m := make(map[string]string)
m["Hello"] = "Bonjour"
m1 := m
m1["Hello"] = "Salut" // 現(xiàn)在m["hello"]的值已經(jīng)是Salut了
make用于內(nèi)建類型(map、slice 和channel)的內(nèi)存分配。new用于各種類型的內(nèi)存分配。
內(nèi)建函數(shù)new本質(zhì)上說跟其它語言中的同名函數(shù)功能一樣:new(T)分配了零值填充的T類型的內(nèi)存空間,并且返回其地址,即一個(gè)*T類型的值。用Go的術(shù)語說,它返回了一個(gè)指針,指向新分配的類型T的零值。有一點(diǎn)非常重要:
new返回指針。
內(nèi)建函數(shù)make(T, args)與new(T)有著不同的功能,make只能創(chuàng)建slice、map和channel,并且返回一個(gè)有初始值(非零)的T類型,而不是*T。本質(zhì)來講,導(dǎo)致這三個(gè)類型有所不同的原因是指向數(shù)據(jù)結(jié)構(gòu)的引用在使用前必須被初始化。例如,一個(gè)slice,是一個(gè)包含指向數(shù)據(jù)(內(nèi)部array)的指針、長度和容量的三項(xiàng)描述符;在這些項(xiàng)目被初始化之前,slice為nil。對于slice、map和channel來說,make初始化了內(nèi)部的數(shù)據(jù)結(jié)構(gòu),填充適當(dāng)?shù)闹怠?/p>
make返回初始化后的(非零)值。
下面這個(gè)圖詳細(xì)的解釋了new和make之間的區(qū)別。
http://wiki.jikexueyuan.com/project/go-web-programming/images/2.2.makenew.png" alt="" />
圖2.5 make和new對應(yīng)底層的內(nèi)存分配
關(guān)于“零值”,所指并非是空值,而是一種“變量未填充前”的默認(rèn)值,通常為0。 此處羅列 部分類型 的 “零值”
int 0
int8 0
int32 0
int64 0
uint 0x0
rune 0 //rune的實(shí)際類型是 int32
byte 0x0 // byte的實(shí)際類型是 uint8
float32 0 //長度為 4 byte
float64 0 //長度為 8 byte
bool false
string ""