Go 基礎
這小節我們將要介紹如何定義變數、常數、Go 內建型別以及 Go 程式設計中的一些技巧。
定義變數
Go 語言裡面定義變數有多種方式。
使用 var
關鍵字是 Go 最基本的定義變數方式,與 C 語言不同的是 Go 把變數型別放在變數名後面:
定義多個變數
定義變數並初始化值
同時初始化多個變數
你是不是覺得上面這樣的定義有點繁瑣?沒關係,因為 Go 語言的設計者也發現了,有一種寫法可以讓它變得簡單一點。我們可以直接忽略型別宣告,那麼上面的程式碼變成這樣了:
你覺得上面的還是有些繁瑣?好吧,我也覺得。讓我們繼續簡化:
現在是不是看上去非常簡潔了?:=
這個符號直接取代了 var
和type
,這種形式叫做簡短宣告。不過它有一個限制,那就是它只能用在函式內部;在函式外部使用則會無法編譯透過,所以一般用 var
方式來定義全域性變數。
_
(下劃線)是個特殊的變數名,任何賦予它的值都會被丟棄。在這個例子中,我們將值 35
賦予b
,並同時丟棄34
:
Go 對於已宣告但未使用的變數會在編譯階段報錯,比如下面的程式碼就會產生一個錯誤:宣告了 i
但未使用。
常數
所謂常數,也就是在程式編譯階段就確定下來的值,而程式在執行時無法改變該值。在 Go 程式中,常數可定義為數值、布林值或字串等型別。
它的語法如下:
下面是一些常數宣告的例子:
Go 常數和一般程式語言不同的是,可以指定相當多的小數位數(例如 200 位), 若指定給 float32 自動縮短為 32bit,指定給 float64 自動縮短為 64bit,詳情參考 連結
內建基礎型別
Boolean
在 Go 中,布林值的型別為bool
,值是 true
或false
,預設為false
。
數值型別
整數型別有無符號和帶符號兩種。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
是虛數單位。下面是一個使用複數的例子:
字串
我們在上一節中講過,Go 中的字串都是採用UTF-8
字符集編碼。字串是用一對雙引號(""
)或反引號(`
`
)括起來定義,它的型別是string
。
在 Go 中字串是不可變的,例如下面的程式碼編譯時會報錯:cannot assign to s[0]
但如果真的想要修改怎麼辦呢?下面的程式碼可以實現:
Go 中可以使用+
運算子來連線兩個字串:
修改字串也可寫為:
如果要宣告一個多行的字串怎麼辦?可以透過`
來宣告:
`
括起的字串為 Raw 字串,即字串在程式碼中的形式就是列印時的形式,它沒有字元轉義,換行也將原樣輸出。例如本例中會輸出:
錯誤型別
Go 內建有一個 error
型別,專門用來處理錯誤資訊,Go 的 package
裡面還專門有一個套件 errors
來處理錯誤:
Go 資料底層的儲存
下面這張圖來源於Russ Cox Blog中一篇介紹 Go 資料結構的文章,大家可以看到這些基礎型別底層都是分配了一塊記憶體,然後儲存了相應的值。
圖 2.1 Go 資料格式的儲存
一些技巧
分組宣告
在 Go 語言中,同時宣告多個常數、變數,或者匯入多個套件時,可採用分組的方式進行宣告。
例如下面的程式碼:
可以分組寫成如下形式:
iota 列舉
Go 裡面有一個關鍵字 iota
,這個關鍵字用來宣告 enum
的時候採用,它預設開始值是 0,const 中每增加一行加 1:
除非被明確的設定為其它值或
iota
,每個const
分組的第一個常數被預設設定為它的 0 值,第二及後續的常數被預設設定為它前面那個常數的值,如果前面那個常數的值是iota
,則它也被設定為iota
。
Go 程式設計的一些規則
Go 之所以會那麼簡潔,是因為它有一些預設的行為:
大寫字母開頭的變數是可匯出的,也就是其它套件可以讀取的,是公有變數;小寫字母開頭的就是不可匯出的,是私有變數。
大寫字母開頭的函式也是一樣,相當於
class
中的帶public
關鍵詞的公有函式;小寫字母開頭的就是有private
關鍵詞的私有函式。
array、slice、map
array
array
就是陣列,它的定義方式如下:
在 [n]type
中,n
表示陣列的長度,type
表示儲存元素的型別。對陣列的操作和其它語言類似,都是透過 []
來進行讀取或賦值:
由於長度也是陣列型別的一部分,因此 [3]int
與[4]int
是不同的型別,陣列也就不能改變長度。陣列之間的賦值是值的賦值,即當把一個陣列作為參數傳入函式的時候,傳入的其實是該陣列的副本,而不是它的指標。如果要使用指標,那麼就需要用到後面介紹的 slice
型別了。
陣列可以使用另一種 :=
來宣告
也許你會說,我想陣列裡面的值還是陣列,能實現嗎?當然囉,Go 支援巢狀陣列,即多維陣列。比如下面的程式碼就宣告了一個二維陣列:
陣列的分配如下所示:
圖 2.2 多維陣列的對映關係
slice
在很多應用場景中,陣列並不能滿足我們的需求。在初始定義陣列時,我們並不知道需要多大的陣列,因此我們就需要“動態陣列”。在 Go 裡面這種資料結構叫slice
slice
並不是真正意義上的動態陣列,而是一個參考型別。slice
總是指向一個底層array
,slice
的宣告也可以像 array
一樣,只是不需要長度。
接下來我們可以宣告一個slice
,並初始化資料,如下所示:
slice
可以從一個陣列或一個已經存在的 slice
中再次宣告。slice
透過 array[i:j]
來取得,其中 i
是陣列的開始位置,j
是結束位置,但不包含array[j]
,它的長度是j-i
。
注意
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
的操作:
slice
是參考型別,所以當參考改變其中元素的值時,其它的所有參考都會改變該值,例如上面的 aSlice
和bSlice
,如果修改了 aSlice
中元素的值,那麼 bSlice
相對應的值也會改變。
從概念上面來說 slice
像一個結構體,這個結構體包含了三個元素:
一個指標,指向陣列中
slice
指定的開始位置長度,即
slice
的長度最大長度,也就是
slice
開始位置到陣列的最後位置的長度
上面程式碼的真正儲存結構如下圖所示
圖 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
這個例子裡面 slice 的容量是 8,新版本里面可以指定這個容量
上面這個的容量就是7-2
,即 5。這樣這個產生的新的 slice 就沒辦法存取最後的三個元素。
如果 slice 是這樣的形式array[:i:j]
,即第一個參數為空,預設值就是 0。
map
map
也就是 Python 中字典的概念,它的格式為map[keyType]valueType
我們看下面的程式碼,map
的讀取和設定也類似 slice
一樣,透過 key
來操作,只是 slice
的index
只能是`int`型別,而 map
多了很多型別,可以是int
,可以是 string
及所有完全定義了 ==
與!=
操作的型別。
這個 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
的元素:
上面說過了,map
也是一種參考型別,如果兩個 map
同時指向一個底層,那麼一個改變,另一個也相應的改變:
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。 此處羅列 部分類型 的 “零值”
links
Last updated