2.Go語言基本詞法
Go語言的語言符號又稱為詞法元素,共包括5類:標識符(identifier)、關鍵字(keyword)、操作符(operator)、分隔字元(delimiter)、以及字面量(literal)。一般情況下,空格符、水平定位字元、斷行符號符和分行符號都會被忽略,除非它們作為多個語言符號之間的分隔字元的一部分。在Go語言中不需要顯示地插入分號,在必要時,Go語言會自動為代碼插入分號以進行語句分隔。
Go語言代碼由若干個Unicode字元組成,Go語言的所有原始碼都必須由Unicode編碼規範的UTF-8編碼格式進行編碼(也就是說編寫的Go語言源碼檔案必須是UTF-8編碼格式的)。
2.1 標識符
Go語言的標識符是由若干字母(由Unicode編碼即可)、底線和數字組成的字元序列;該字元序列的第一個字元必須為字母。
注意: 在Go語言代碼中,每一個標識符都必須在使用前進行聲明。 一個聲明將一個非空的標識符與一個常量、類型、變數、函數或程式碼封裝綁定在一起。 在同一個代碼塊中,不允許重複聲明同一個標識符(除了指派陳述式例外)。 在一個源碼檔案和一個程式碼封裝中的標識符都需要遵循此規則。 一個已被聲明的標識符的範圍與其直接所屬的代碼塊的範圍相同。
嚴格來講,程式碼封裝聲明語句並不算是一個聲明。因為程式碼封裝名稱並不會出現在任何一個範圍中。程式碼封裝聲明語句的目的是為了鑒別若干源碼檔案是否屬於同一個程式碼封裝,或者指定匯入程式碼封裝時的預設程式碼封裝引用名稱。
限定標識符用來訪問其他程式碼封裝中的變數或類型。例如,當我需要存取碼包os中名為O_RDONLY的常量時,需要這樣寫os.O_RDONLY。
限定標識符能夠使用,需要滿足兩個前提條件: 要訪問的程式碼封裝必須被事先匯入; 這個程式碼封裝中的標識符必須是可匯出的。
一個可匯出的標識符也需要滿足兩個前提條件: 標識符名稱中的第一個字元必須為大寫(Go語言根據標識符名稱中的第一個字元的大小寫來確定這個標識符的存取權限的,當標識符名稱的第一個字元為大寫時,其存取權限為“公開的”,也就是該標識符可以被任何程式碼封裝中的任何代碼通過限定標識符訪問到;當標識符的第一個字元為小寫時,其存取權限就是”包級私人的”,也就是只有與該標識符同在一個程式碼封裝的代碼才能夠訪問到它); 標識符必須是被聲明在一個程式碼封裝中的變數或者類型的名稱,或者是屬於某個結構體類型的欄位名稱或方法的名稱。
Go語言的預定義標識符: 所有基礎資料型別 (Elementary Data Type)的名稱。 介面類型error 常量true,false和iota 所有內建函數的名稱,即append、cap、close、complex、copy、delete、imag、len、make、new、panic、print、println、real和recover。
Go語言中有一個空標識符,它由一個底線表示,一般用於一個不需要引入一個新綁定的聲明中。例如,當我們只想執行一下某個程式碼封裝中的初始化函數,而不需要使用這個程式碼封裝中的任何程式實體的時候,可以編寫如下匯入語句:
import _ "runtime/cgo"
其中,”runtime/cgo”代表了一個標準庫程式碼封裝的標識符。
2.2 關鍵字
關鍵字(也稱為保留字)是被程式設計語言保留而不讓編程人員作為標識符使用的字元序列。
類別 |
關鍵字 |
程式聲明 |
import, package |
程式實體聲明和定義 |
chan, const, func, interface, map, struct, type, var |
程式控制流程程 |
go, select, break, case, continue, default, defer, else, fallthrough, for, goto, if, range, return, switch |
在Go語言中,程式實體的聲明和定義是建立在其資料類型的體系之上的。例如關鍵字chan、func、interface、map和struct,分別於Go語言的複合資料型別Channel(通道)、Function(函數)、Interface(介面)、Map(字典)和Struct(結構體)相對應。
程式控制流程程的關鍵字,一共15個。其中go和select,這兩個主要用於Go語言並發編程。
2.3 字面量
Go語言代碼中用到的字面量有以下3類: 表示基礎資料類型值的各種字面量。例如,表示浮點數類型值的12E-3。
構造各種自訂的複合資料型別的類型字面量。例如,下面表示一個名稱為Person的自訂結構體類型:
type Person struct { Name string Age uint8 Address string}
表示複合資料型別的值的複合字面量,被用來構造類型Struct(結構體)、Array(數組)、Slice(切片)和Map(字典)的值。例如,下面的字面量用於表示上面名稱為Person的結構體類型的值:
Person { Name:"Huazie", Age: "21", Address: "Nanjing, China"}
注意:
對複合字面量的每次求值都會導致一個新的值被建立。因此,如上該複合字面量每被求值一次就會建立一個新的Person類型的值。
Go語言不允許在一個此類的複合字面變數中,出現重複的鍵。如下都是錯誤,無法通過編譯,因為鍵都有重複。
//表示結構體類型值,有重複的鍵 NamePerson {Name: "Huazie",Age: "21", Name: "Unknown"}//表示字典類型值,有重複的鍵 Agemap[string]string{ Name: "Huazie",Age: "21", Age: "21"}//表示切片類型值,有重複的鍵 0[]string{0: "0", 1: "1", 0: "-1"}
2.4 類型
一個類型確定了一類值的集合,以及可以在這些值上施加的操作。類型可以由類型名稱或者類型字面量指定,分為基本類型和複合類型,基本類型的名稱可以代表其自身。
var bookName string
如上聲明了一個類型為string(基本類型中的一個)、名稱為bookName的變數。
其他基本類型(預定義類型)有bool、byte、rune、int/uint、int8/uint8、int16/uint16、int32/uint32、int64/uint64、float32、float64、complex64和complex128。除了bool和string之外的其他基本類型也叫做數實值型別。
複合類型一般由若干(也包括零)個其他已被定義的類型組合而成。複合類型有Channel(通道)、Function(函數)、Interface(介面)、Map(字典)、Struct(結構體)、Slice(切片)、Array(數組)和Pointer(指標)。
Go語言中的類型又可以分為靜態類型和動態類型。一個變數的靜態類型是指在變數聲明中給出的那個類型。絕大多數類型的變數都只有靜態類型。唯獨介面類型的變數例外,它除了擁有靜態類型之外,還擁有動態類型(介面類型在後面會講到)。
每一個類型都會有一個潛在類型。如果這個類型是一個預定義類型(也就是基本類型),或者是一個由類型字面量構造的複合類型,那麼它的潛在類型就是它自身。如string類型的潛在類型就是string類型,上面自訂的Person類型的潛在類型就是Person。如果一個類型並不屬於上述情況,那麼這個類型的潛在類型就是型別宣告中的那個類型的潛在類型。
如下聲明一個自訂類型
type MyString string
如上可以把類型MyString看作string類型的一個別名資料型別,那麼MyString類型的潛在類型就是string類型。Go語言基礎資料型別 (Elementary Data Type)中的rune類型可以看作是uint32類型的一個別名資料型別,其潛在類型就是uint32。
注意: 類型MyString和類型string是兩個不相同的類型。不能將其中一個類型的值賦給另一個類型的變數。 別名資料型別與它的源類型的不同僅僅體現在名稱上,它們的內部結構是一致的;下面的類型轉換的運算式都是合法的:MyString(“ABC”) 和string(MyString(“ABC”))。這種類型轉換並不會建立新的值。
一個類型的潛在類型具有可傳遞性,如下:
type iString MyString
則類型isString的潛在類型就是string類型。
這裡聲明一個類型,如下:
type MyStrings [3]string
注意:類型MyStrings的潛在類型並不是[3]string。[3]string既不是一個預定義的類型,也不是一個由類型字面量構造的複合類型,而是一個元素類型為string的數群組類型。
根據上面的定義可知類型MyStrings的潛在類型就是[3]string的潛在類型string。
Go語言規定,一個數群組類型的潛在類型決定了在該類型的變數中可以存放哪一個類型的元素。
2.5 操作符
操作符就是用於執行特定算術運算或邏輯操作的符號。(這裡不詳細講解了,跟C語言的操作符類似),不過Go語言中沒有三元操作符,所以除了一元操作符以外都必定是二元操作符。Go語言一共有21個操作符,包括算術操作符、比較操作符、邏輯操作符、地址操作符和接收操作符。
符號 |
說明 |
樣本 |
|| |
邏輯或操作。二元,邏輯操作符 |
true || false //運算式結果是true |
&& |
邏輯與操作。二元,邏輯操作符 |
true && false //運算式結果是false |
== |
相等判斷操作。二元,比較操作符 |
“abc” == “abc”//結果是true |
!= |
不等判斷操作。二元,比較操作符 |
“abc” != “Abc”//結果是true |
< |
小於判斷操作。二元,比較操作符 |
1 < 2 //運算式結果是true |
<= |
小於或等於。二元,比較操作符 |
1 <= 2 //運算式結果是true |
> |
大於判斷操作。二元,比較操作符 |
3 > 2 //運算式結果是true |
>= |
大於或等於。二元,比較操作符 |
3 >= 2 //運算式結果是true |
+ |
表示求和,一元又是二元,算術操作符 |
+1 //結果為1 (1+2) //結果是3 |
- |
表示求差,一元又是二元,算術操作符 |
-1 //結果為-1 (1 – 2) //結果是-1 |
| |
按位或操作,二元,算術操作符 |
5 | 11 //運算式的結果是15 |
^ |
按位異或,一元又是二元,算術操作符 |
5^11//結果是14(^5)//結果是-6 |
* |
求積或取值,一元,二元,算術,地址 |
*p //取值操作 |
/ |
求商操作,二元,算術操作符 |
10 / 5 //運算式的結果為2 |
% |
求餘數操作,二元,算術操作符 |
12 % 5 //運算式的結果為2 |
<< |
按位左移操作,二元,算術操作符 |
4 << 2 //運算式的結果為16 |
>> |
按位右移操作,二元,算術操作符 |
4 >> 2 //運算式的結果為1 |
& |
按位與操作,一元,二元,算術,地址 |
&v //取地址操作 |
&^ |
按位清除操作,二元,算術操作符 |
5 &^ 11 //運算式的結果為4 |
! |
邏輯非操作,一元,邏輯操作符 |
!b //若b為true,結果為false |
<- |
接收操作,一元,接收操作符 |
<- ch |
注意:假設上面的ch 代表了元素類型為 byte的通道類型值,則<- ch表示從ch中接收byte類型值的操作。
重點講解3個操作符: &^ 實現了按位清除操作,按位清除就是根據第二個運算元的二進位值對第一個運算元的二進位值進行相應的清零操作,如果第二個運算元的某個二進位位上的數組為1,就把第一個運算元的對應二進位位上的數值設定為0。否則,第一個運算元的對應二進位位上的數值不變。這樣的操作並不會改變第一個運算元的原值,只會根據兩個運算元的二進位值計算出結果值。這樣就可以理解上面的5 &^ 11的結果為4了。
^ 作為一元操作符,分兩種情況:
(1). 運算元是無符號的整數類型,使用這一個操作就相當於對這個運算元和其整數類型的最大值進行二元的按位異或操作,如下:
^uint8(1) = 254 //不帶正負號的整數的一元按位異或操作00000001 ^ 11111111 = 11111110//對應的位元運算
如上,內建函數uint8會將一個整數字面量轉換為一個uint8類型的值,這保證了一元操作符^的唯一運算元一定是一個不帶正負號的整數類型的值。
(2). 操作是有符號的整數類型,這一操作就相當於對這個運算元和-1進行二元按位異或操作。例如:
^1 = -2 //有符號整數的一元按位異或操作00000001 ^ 11111111 = 11111110//對應的二進位運算
注意:以上的運算元的二進位值都是以補碼形式表示;預設情況下整數字面量是有符號的,所以(2)中運算元1不需要顯示使用內建函數int8 。
<- 接收操作符,只作用於通道類型的值。使用時,需要注意兩點:
(1). 從一個通道類型的空值(即nil)接收值的運算式將會永遠被阻塞。
(2). 從一個已被關閉的通道類型值接收值會永遠成功並立即返回一個其元素類型的零值。
一個由接收操作符和通道類型的運算元所組成的運算式可以直接被用於變數賦值或初始化,如下所示(在指派陳述式講解時,再細說)
v1 := <-chv2 = <-ch
特殊標記 = 用於將一個值賦給一個已被聲明的變數或常量。
特殊標記 := 則用於在聲明一個變數的同時對這個變數進行賦值,且只能在函數體內使用。
又如下:
v, ok = <-chv, ok := <-ch
當同時對兩個變數進行賦值或初始化時,第二個變數將會是一個布爾類型的值。這個值代表了接收操作的成功與否。如果這個值為false,就說明這個通道已經被關閉了。(之後講解通道類型會詳細介紹)。
操作符優先順序
優先順序 |
操作符 |
5 |
* / % << >> & &^ |
4 |
+ - | ^ |
3 |
== != < <= > >= |
2 |
&& |
1 |
|| |
2.6 運算式
基本運算式
(1) 使用運算元來表示;
(2) 使用類型轉換來表示;
(3) 使用內建函數調用來表示;
(4) 一個基本運算式和一個選擇符號組成選擇運算式;
例如,如果在一個結構體類型中存在欄位f,我們就可以在這個結構體類型的變數x上應用一個選擇符號來訪問這個欄位f,即x.f。其中,.f就是一個選擇符號。注意:前提是這個變數x的值不能是nil。在Go語言中,nil用來表示空值。
(5) 一個基本運算式和一個索引符號組成索引運算式;
索引符號由狹義的運算式(僅由操作符和運算元組成)和外層的方括弧組成,例如[]int{1,2,3,4,5}[2]就是索引運算式。
Go語言允許如下的指派陳述式:
v, ok := a[x]
如上a為字典類型,x為字典的鍵。該索引運算式的結果是一對值,而不是單一值。第一個值的類型就是該字典類型的元素類型,而第二個值則是布爾類型。與變數ok綁定的布爾值代表了在字典類型a中是否包含了以x為鍵的索引值對。如果在a中包含這樣的索引值對,那麼賦給變數ok的值就是true,否則就為false。
注意:雖然當字典類型的變數a的值為nil時,求值運算式a[x]並不會發生任何錯誤,但是在這種情況下對a[x]進行賦值卻會引起一個運行時恐慌( Go語言異常)。
(6) 一個基本運算式和一個切片符號組成切片運算式;
切片符號由2個或3個狹義的運算式和外層的方括弧組成,這些運算式之間由冒號分隔。切片符號作用與索引符號類似,只不過索引符號針對的是一個點,切片符號針對的是一個範圍。例如,要取出一個切片[]int{1,2,3,4,5}的第二個到第四個元素,那麼可以使用切片符號的運算式[]int{1,2,3,4,5}[1:4],該結果還是一個切片。
切片運算式a[x:y:z],a是切片符號[x:y]的操作對象。其中,x代表了切片元素下界索引,y代表了切片的元素上界索引,而z則代表了切片的容量上界索引。約束如下:
0 <= 元素下界索引 <= 元素上界索引 <= 容量上界索引 <= 操作對象的容量
設a的值為[]int{1,2,3,4,5},則切片運算式a[:3]等同於a[0:3],這是因為切片符號的元素下界索引的預設值為0,相應的元素上界的索引的預設值為操作對象的長度值或容量值,即切片運算式a[3:]等同於a[3:5]。同樣,切片運算式a[:]等同於複製a所代表的值並將這個複製品作為運算式的求值結果。
注意: UTF-8 編碼格式會以3個位元組來表示一個中文字元,而切片操作是針對位元組進行的。
如果有“Go並發編程實戰”的字串類型的變數a,那麼切片運算式a[1:3]的結果不是“o並”,而a[1:5]的結果才是“o並”。
(7) 一個基本運算式和一個類型斷言符號組成;
類型斷言符號以一個英文句號為首碼,並後跟一個被圓括弧括起來的類型名稱或類型字面量。類型斷言符號用於判斷一個變數或常量是否為一個預期的類型,並根據判斷結果採取不同的響應。例如,如果要判斷一個int8類型的變數num是否是int類型,可以這樣編寫運算式:interface{}(num).(int)。
對於一個求值結果為介面類型值的運算式x和一個類型T,對應的類型斷言為:
x.(T)
該運算式的作用是判斷“x不為nil且儲存在其中的值是T類型的”是否成立。
如果T不是一個介面類型,那麼x.(T)會判斷類型T是否為x的動態類型(一個變數的動態類型就是在運行期間儲存在其中的值的實際類型);而這個實際類型必須是該變數聲明的那個類型的一個實作類別型,否則就根本不可能在該變數中儲存這一類型的值。所以類型T必須為x的類型的一個實作類別型,而在Go語言中只有介面類型可以被其他類型實現,所以x的求值結果必須是一個介面類型的值。
所以上面運算式interface{}(num).(int)中運算式interface{}(num)的含義就是將變數num轉換為interface{}類型的值(即它的結果值是介面類型的),而這剛好符合前面的定義。
知識點: interface{}是一個特殊的介面類型,代表空介面。所有類型都是它的實作類別型。
在對變數的賦值或初始化的時候,也可以使用類型斷言,如下:
v, ok := x.(T)
當使用類型斷言運算式同時對兩個變數進行賦值時,如果類型斷言成功,那麼賦給第一個變數的將會是已經被轉換為T類型的運算式x的求值結果,否則賦給第一個變數的就是類型T的零值。布爾類型會被賦給變數ok,它體現了類型斷言的成功(true)與否(false)。
注意: 在這種情境下,即使類型宣告失敗也不會引發運行時恐慌。
(8) 一個基本運算式和一個調用符號組成。
調用符號只針對於函數或者方法。與調用符號組合的基本運算式不是一個代表程式碼封裝名稱(或者其別名)的標識符就是一個代表結構體類型的方法的名稱的標識符。調用符號由一個英文句號為首碼和一個被圓括弧括起來的參數列表組成,多個參數列表之間用逗號分隔。例如,基本運算式os.Open(“/etc/profile”)表示對程式碼封裝os中的函數Open的調用。
可變長參數
如果函數f可以接受的參數的數量是不固定的,那麼函數f就是一個能夠接受可變長參數的函數,簡稱可變參函數。在Go語言中,在可變參函數的參數列表的最後總會出現一個可變長參數,這個可變長參數的型別宣告形如…T。Go語言會在每次調用函數f的時候建立一個切片類型值,並用它來存放這些實際函數。這個切片類型值的長度就是當前調用運算式中與可變長參數綁定的實際參數的數量。
可變參函數appendIfAbsent聲明如下(函數體省略):
func appendIfAbsent(s []string, t ...string) []string
針對此函數的調用運算式如下:
appendIfAbsent([]string(“A”,”B”,”C”),”C”,”B”,”A”)
其中,與可變參數t綁定的切片類型值為[]string{”C”,”B”,”A”},包含了實際參數”C”,”B”和”A”。
也可以直接把一個元素類型為T的切片類型值賦給…T類型的可變長參數,如下調用:
appendIfAbsent([]string(“A”,”B”,”C”), []string(”C”,”B”,”A”)...)
或者如果有一個元素類型為stirng的切片類型的變數s的話,如下調用:
appendIfAbsent([]string(“A”,”B”,”C”), s...)
對於將切片類型的變數賦給可變長參數的情況,Go語言不會專門建立一個切片類型值來儲存其中的實際參數。因為,這樣的切片類型值已經存在了,可變長參數t的值就是變數s的值。
關於Go語言基本詞法的講解就告一段落了,接下來要講Go語言的資料類型了。
最後附上知名的Go語言開源架構(每篇附上一個):
Beego: 一個國產的HTTP架構。我們可以用它來快速地開發各種應用程式。官網:http://beego.me。