go快速入門指南
by 小強,2019-06-13
go語言是目前非常火熱的語言,廣泛應用於服務器端,雲計算,kubernetes容器編排等領域。它是一種開源的編譯型程序設計語言,支持並發、垃圾回收機制以提升應用程序性能。它既具有像c這種靜態編譯型語言的高性能,又具備像python這種動態語言的高效性。很多go程序員都是從C++,Java等面向對象語言因為工作的需要轉過來的,因此沒有必要從0開始學習go,當初自己的想法是找一篇半小時入門go的博客,貌似沒有類似的好文章=_=。搜到的都是一些從小白入手的臃腫教程,學習起來太慢!!!so,打算寫這篇go語言快速入門的指南。
本文寫作思路重點在於和C++語言的不同之處入手,強調go的特性,注重快速入門,因此不介紹一些非常基礎的知識,非常基礎的一些知識可以去看go語言聖經。關於go環境的安裝配置以及vscode編輯器的配置在之前的博客已經介紹,請移步。本文整體組織結構如下:
- go程序開發的一般結構
- 基本數據類型
- 變量的聲明和賦值
- 運算符和指針
- 判斷語句if和循環語句for
- 數組、切片、map
- 函數function
- 方法、接口、反射
- 並發
1.go程序開發的一般結構
在學習任何一門語言的時候,首先都是給出hello world的示例,因此本文也不免俗,看看第一個go語言程序:
/*1.1 template.go*/
//當前程序的包名 package main //導入其他的包 import ( "fmt" )//由main函數作為函數入口 func main () { fmt.Println("Hello World!") }
和python語言很像,go程序都必須包含在一個包package中,go程序一般由三部分組成:包聲明部分、第三方包導入部分和函數聲明部分。go語言使用關鍵字package聲明要創建的包;使用import導入第三方包;使用關鍵字func聲明要創建的函數。
按照慣例,處於同一個文件里的代碼文件,必須使用同一個包名,包和文件夾的名字相同。Go編譯器不允許聲明導入某個包卻不使用。使用下划線可以讓編譯器接收這類導入,並且調用對應包內的所有代碼文件中定義的init函數。 init函數會在main函數執行前執行。
1.1 編寫go程序步驟
初學者按照以下步驟編寫go程序:
1)在工作目錄(比如D:\go\development)的src文件夾中創建源文件helloworld.go;.
2)直接將helloworld.go拖入vscode進行編輯;
3)在vscode的終端輸入go run helloworld.go,程序就會輸出hello world!
1.2 go的語法要點
- go語言語句結束不使用分號,直接另起一行即可
- go語言規定,函數、控制結構(if,for,switch)等的左大括號“{”必須和函數聲明或控制結構放在同一行
- 可見性規則:go語言使用大小寫來決定常量、變量、類型、接口、結構或函數是否可以被外部包所調用。
- 函數名首字母小寫,即為private。
- 函數名首字母大寫,即為public。
2 .基本數據類型
- 布爾型bool長度為1字節,取值范圍true,false,不同點:注意不能使用數字0/1來代表true或者false。
- 整型:int/uint,根據運行平台可能是32或者64
- 8為整型:int8/uint8,長度是1字節,取值范圍:-128~127/0-255
- 計算方法,2^8 / 2給負數部分,剩下分一個給0,最后的部分給整數部分。
- int16/uint16,int32/uint32,int64/uint64
- 浮點型:float32/float64
- 長度:4/8字節,小數位,精確到7/15小數位
- 注意go沒有double類型。
- 復數:complex64/complex128
- 長度:8/16字節
- 足夠保存指針的32為或64為整數型:uintptr
- 其他值類型:array,struct,string
- 引用類型:slice(切片,特有類型),map(哈希表),chan(通道)
- 接口類型:interface
- 函數類型:func
類型的零值:就是默認值,int默認是0,bool默認是false,字符串默認是空字符串。
類型別名方法格式:
//type 別名 基本類型 type byte int8
3.變量的聲明和賦值
-
全局變量不能省略var關鍵字,函數內的變量聲明可以省略。 go語言中使用關鍵字func聲明函數,關鍵字后面跟函數名、參數名以及返回值。
- 全局變量的聲明可以使用var()的方式進行簡寫
- 全局變量的聲明不可以省略var,但是可使用並行方式
- 所有變量都可以使用類型推斷
- 局部變量不可以使用var()的方式簡寫,只能使用並行方式。
3.1 變量聲明
go語言使用關鍵字var來聲明變量,聲明的一般格式如下所示:
var <variableName> [varibleType] var count int
在聲明變量的同時可以使用=給變量賦初值:
var count int = 10
其中變量類型int也可以省略,編譯器會依據賦的初值自動推斷變量類型:
var count = 10
在聲明變量的同時還允許省略掉關鍵字“var”,使用":"取代。
count := 10
3.2 常量的聲明
常量的聲明格式如下所示:
const <constName> [constType] = <賦值表達式>
- 常量的值在編譯時就已經確定
- 常量的定義格式與變量基本相同
- 等號右側必須是常量或者常量表達式
- 常量表達式中的函數必須是內置函數
- 在定義常量組是,如果不提供初始值,則表示將使用上行的表達式
- 使用相同的表達式不代表具有相同的值
- iota是常量的計數器,從0開始,組中每定義一個常量自動遞增1
- 通過初始化規則與iota可以達到枚舉的效果
- 每遇到一個const關鍵字,iota就會重置為0
- 注意常量的定義必須是大寫字母。但是如果是大寫字母的話,就會變成public變量,為了不被包外部使用,一般在前面加_或者c。
const a = 1 const ( b, c = 2,3 ) const d,f = 4,5 const ( a = iota //0 b = iota //1 )
4.運算符和指針
4.1 運算符
go的運算符均是從左至右結合。
優先級(從高到底)
- ^ !(一元運算符)
- / % << >> & &^(二元運算符)
- == != < <= >= >
- <- (專門用於channel)
- && (好處是運行第一個不滿足要求就不在執行接下來的表達式)
- ||
其中位運算符介紹:
實際例子:
6: 0110 11:1011 ------------- &: 0010 |: 1111 ^: 1101 &^:0100
- & 與:都是1的時候結果才能是1
- | 或:只要有一個是1就是1
- ^ 兩個只能有一個是1才能是1
- &^第二個計算數是1的時候將第一個計算數對應位置為0,如果第二個計算數該位是0的話,對應第一個計算數的位不變。
4.2 指針
Go雖然保留了指針,但不同點在於go不支持指針運算以及->運算符,而直接采用.選擇符來操作指針目標對象的成員。
- 操作符&去變量地址,使用*通過指針間接訪問目標對象
- 默認值為nil而非NULL
- 遞增遞減語句
在go當中,++ 與--是作為語句而不是作為表達式。必須單獨作為一行,只能A++這種形式。
A++//只能作為單獨的語句放在一行,且只能++放在右邊 x, y := 1, 2 var p = [2]*int{&x, &y} fmt.Println(*p[0]) var arr = [2]int{x, y} pf := &arr fmt.Println(*pf)
其實就是將[]*int看成一個類型,后面的{}就是初始化操作。
5.判斷語句if和循環語句for
5.1 判斷語句if
- 條件表達式沒有括號
- 支持一個初始化表達式(可以是並行方式)
- 左大括號必須和條件語句或else在同一行
- 支持單行模式
- 初始化語句中的變量為block級別,同時隱藏外部同名變量
if a > 1 { fmt.Println(a) } if b :=1;b > 1 { }
注意else必須和if的右括號}在同一行才行,不然出錯。
if a { }else {}
if后面定義的變量,屬於if語句的局部變量,只能在對應的if-else中使用,不能在外部使用。之間通過;分割語句。
5.2 循環語句for
- go只有for一個循環語句關鍵字,但支持3種形式
- 初始化和步進表達式可以是多個值
- 條件語句每次循環都會被重新檢查,因此不建議在條件語句中使用函數,盡量提前計算好條件並以變量或常量代替。
- 左大括號必須和條件語句在同一行
1. for init; condition; post {} // 和c語言的for一樣 2. for condition {} //while 3. for {} //for(;;) init: 一般為賦值表達式,給控制變量賦初值;一定在這里賦初值,不然出錯 condition: 關系表達式或邏輯表達式,循環控制條件; post: 一般為賦值表達式,給控制變量增量或減量。
for語句執行過程如下:
1)先對表達式1賦初值;
2)判別賦值表達式init是否滿足給定條件,若其值為真,滿足循環條件,則執行循環體內語句,然后執行post,進入第二次循環,再判別condition;否則判斷condition的值為假,不滿足條件,就終止for循環,執行循環體外語句。
- for 循環的 range 格式可以對 slice、map、數組、字符串等進行迭代循環。格式如下:
for key, value := range oldMap { newMap[key] = value }
關鍵字range會返回兩個值,第一個值是當前迭代到的索引位置,第二個值是該位置對應元素值的一份副本。而不是返回對該元素的引用。
- switch語句
switch語句默認情況下case最后自帶break語句,匹配成功后就不會執行其他case,如果我們需要執行后面的case,可以使用fallthrough。
其中var1可以是任何類型,val1和val2可以是同類型的任意值,類型不被局限於常量或證書,但必須是相同的類型,或者最終結果為相同類型的表達式。可以同時測試多個可能符合條件的值,使用逗號分隔它們。例如 case val1,val2,val3。
switch var1 { case val1: ... case val2: ... default: ... }
跳轉語句goto,break,continue
- 三個語法都可以配合標簽使用。
- 標簽名區分大小寫,若不適用會造成編譯錯誤。
- Break與continue配合標簽可用於多層循環的跳出。
- Goto是調整執行位置,與其他2個語句配合標簽的結果並不相同。 標簽值是包含接下來的一個語句,continue是退出這個標簽的值。
6 .數組、切片、map
6.1 數組
定義數組的格式: var <varName> [n]<type> ,n >= 0(表示數組的元素個數)。
var a [2]int var b [1]int
記住ab是不同的類型,不能直接賦值,元素個數也是數組類型的一種。需要使用循環語句進行操作。 也可以不指定數組元素的個數。
a := [...]int{1,2,3} //元素個數為3個 a := [...]int{0:1,1:2}//位置0賦值為1,位置1賦值為2 a := new([10]int)
- 數組長度也是類型的一部分,因此具有不同長度的數組為不同類型。
- 注意區分指向數組的指針和指針數組
- 數組在go中為值類型,不是引用類型,會全部拷貝值
- 數組之間可以使用==或!=進行比較,但不可以使用<或>
- 可以使用new來創建數組,此方法返回一個指向數組的指針
- go支持多維數組。
需要注意的是所有值類型變量在賦值或作為參數傳遞的時候將產生一次復制操作。如果將數組作為函數的參數類型,則在函數調用是將該參數將發生數據復制,函數體內無法修改數組的內容,因為函數體內操作是變量的一個副本。
多維數組的聲明如下所示,其中第一維度行的數量是可以省略的,使用...代替。
arr := [2][3]int{ {1, 2, 3}, {2, 3, 4}} 表示2個元素,每個元素是一維數組,有三個元素。
6.2 切片slice
切片是數組的一個引用,它會生成一個指向數組的指針,並通過切片長度關聯到底層數組部分或者全部元素,還提供了一系列對數組的管理功能(append,copy),可以隨時動態的擴充存儲空間。屬於變長數組,相當於C++的vector。創建切片的格式如下:
var sliceName []dataType
創建切片時,不需要指定切片的長度。下面是一個具體的例子。
var slice1 []int
6.2.1 初始化方法
1)如果引用底層數組的元素,初始化方法如下:
slice1 = array[start : end] //以下是三種方式 slice1 = array1 slice1 = array1[ : ] slice1 = array[0 : len(array1)]
2)直接創建切片
即在定義的同時初始化切片元素,如下例:
var slice1 = []int{1,2,3,4,5}
3)使用make函數創建切片
下式表示,創建整型切片slice1,元素個數為5,元素初值為0,並預留10個元素的存儲空間。
var slice1 = make([]int, 5, 10)
對切片的訪問方式可以通過下標的方式訪問,也可以通過range關鍵字進行訪問,同數組。
- 其本身並不是數組,它指向底層的數組,使用[]來做聲明
- 作為變長數組的替代方案,可以關聯底層數組的局部或全部
- 屬於引用類型
- 可以直接創建或從底層數組獲取生成
- 使用len()獲取元素個數,cap()獲取容量
- 一般使用make()創建
- 如果多個slice指向相同底層數組,其中一個的值改變會影響全部
- make([]T,len,cap)
- 其中cap可以省略,則和len的值相同
- len表示存數的元素個數,cap表示容量
//從數組初始化 var arr = [...]int{1,2,3} var slice_a []int slice_a = arr[1:2]//下標位置,[1,2),包括首位置,不包含末尾的2位置
6.2.2 切片的操作
- Reslice:
- Reslice時索引以被slice的切片為准
- 索引不可以超過被slice的切片容量的cap()值
- 索引越界不會導致底層數組的重新分配而是引發錯誤
- Append
- 可以在slice尾部追加元素
- 可以將一個slice追加在另一個slice尾部
- 如果最終長度未超過追到到slice的容量則返回原始slice
- 如果超過追加到的slice的容量則將重新分配數組並拷貝原始數據
- 使用...運算符將一個切片的所有元素追加到另一個切片中
append(s1,s2...)
- copy
copy(s1,s2),必須保證s1有足夠的空間來存儲s2的值。
- 多維切片
slice := [][]int{{1, 2}, {3, 4}}
使用切片做值函數傳遞時,以值的形式傳遞切片,由於切片的尺寸很小,所以成本很低,與切片關聯的數據保存在底層數組中,不屬於切片本身,所以切片的效率很高。slice的拷貝可以使用 s2 := s1[:],拷貝首元素省略,拷貝末尾元素也可以省略,:表示拷貝全部元素。
6.3 map
map就是理解為C++里面的map,是key-value類型,也稱為字典或者哈希表。
6.3.1 聲明格式
var mapName map[keyType] valueType var map1 map[string] int
在該例中,聲明了一個鍵值類型為字符串,值類型為整型的字典map1。
6.3.2 字典的初始化和創建
使用“{ }”操作符對字典進行初始化操作,或者使用make()函數來創建字典。初始化或者創建后,就可以使用“=”操作符對字典動態的增添數據項了。
var map1 map[string] int {} map1["key1"] = 1
也可以使用下面的方式進行創建:
var map1 map[string] int map1 = make(map[string] int) map1["key1"] = 1
6.3.3 map的訪問和操作
map通過key來訪問value,訪問格式如下所示:
Value = mapName[Key]
map的查找:如果查找的key存在,則將key對應的value值賦予v,OK為true,反之,如果Key不存在,則v等於0,OK為false。
v,OK := mapName[Key]
map的刪除:
delete()用於刪除容器內的元素,也可以用於刪除map內的鍵值對,例如:
下面將從map1中刪除鍵值為key1的鍵值對,如果key1這個鍵不存在,那么這個調用將什么也不會發生。
delete(map1,“key1”)
-
類似其他語言中的哈希表或者字典,以key-value形式存儲數據
-
key必須是支持==或!=比較運算的類型,不可以是函數、map或者slice
-
map查找比線性搜索快很多,但比使用索引訪問數據的類型慢100倍
-
map使用make()創建,支持:=這種簡寫方式。
-
make([keyType]valueType,cap),cap表示容量,可省略
-
超出容量時會自動擴容,但盡量提供一個合理的初始值
-
使用len()獲取元素個數
-
鍵值對不存在時自動添加,使用delete()刪除某鍵值對
-
使用for range對map和slice進行迭代操作
-
記住每個map都必須進行單獨的初始化操作。 使用make進行初始化操作。有幾層map就需要使用幾次make進行初始化操作。
- map的迭代操作
for k,v := range m {}
7.函數function
- go函數不支持嵌套,重載和默認參數
- 但支持以下特性
- 無需聲明原型 、不定長度變參、多返回值、命名返回值參數
- 匿名函數、閉包
- 定義函數使用關鍵字func,且左大括號不能另起一行
- 函數也可以作為一種類型使用
- 函數聲明的基本結構如下:
func functionName(參數列表) 返回值 { functionBody . . . return 語句 }
- 不定長變參的使用
不定長變參使用...表示,要放在所有的參數最后面,傳入的a變為一個slice
func A (a ...int) {}
- 閉包closure
閉包就是在一個函數中聲明一個匿名函數,然后返回這個匿名函數。
func f(i int) func() int { return func() int { i++ return i } }
- defer
- 執行方式類似其他語言的析構函數,在函數體執行結束之后按照調用順序的相反順序逐個執行
- 即使函數發生嚴重錯誤也會執行
- 支持匿名函數的調用
- 常用於資源清理、文件關閉、解鎖以及記錄時間等操作
- 通過與匿名函數配合可在return之后修改函數計算結果
- 如果函數體內某個變量作為defer時匿名函數的參數,則在定義defer時即已經獲得了拷貝,否則則是引用某個變量的地址。
- go沒有異常機制,但有panic/recover模式來處理錯誤
- panic可以在任何地方引發,但recover只有在defer調用的函數中有效
go中可以拋出一個panic的異常,然后在defer中通過recover捕獲這個異常,然后正常處理
func B() { defer func() { if err := recover(); err != nil { fmt.Println("Recover in B") } }() panic("Panic in B") }
- 代碼分析
func main() { var fs = [4]func(){} for i := 0; i < 4; i++ { defer fmt.Println("defer i = ", i) defer func() { fmt.Println("defer_closure i = ", i) }() fs[i] = func() { fmt.Println("closure i = ", i) } } for _, f := range fs { f() } }
因為defer是逆序執行的,在i變為4之后,閉包中指向的是i的地址,所以閉包中的i的值都是指向i=4的地址。
8 方法、接口和反射
8.1 方法
go語言的method類似於一個函數,只是函數名前多了個綁定類型參數---receiver,基本格式如下:
func (recv receiver_type) methodName (參數列表)(返回值){...}
- 記住命名規范,在結構體中的字段名和方法名必須首字母大寫
method中的receiver可以是內置類型、自定義類型、結構體或指針類型。
- go雖沒有class,但是有method
- 通過顯示說明receiver來實現與某個類型的組合
- 只能為同一個包中的類型定義方法
- receiver可以是類型的值或者指針
- 不存在方法重載
- 可以使用值或指針來調用方法,編譯器會自動完成轉換
- 從某種意義來說,方法是函數的語法糖,因為receiver其實就是方法所接收的第一個采納數
- 如果外部結構和嵌入結構存在同名方法,則優先調用外部結構的方法
- 類型別名不會擁有底層類型所附帶的方法
- 方法可以調用結構中的非公開字段
不同包中大小寫變量方法才有權限的區別,同一個包中可以訪問private字段的內容,大寫的public權限可以被不同包之間訪問。type tz int,記住tz i和int i還是不同的類型,前面的i屬於tz類型。要兩者相加必須使用強制類型轉換。
8.2 接口
- 接口是一個或多個方法簽名的集合
- 只要某個類型擁有該接口的所有方法簽名,即算實現該接口,無需顯示聲明實現了哪個接口,這稱為Structural Typing
- 接口只有方法聲明,沒有實現,沒有數據字段
- 接口可以匿名嵌入其他接口,或嵌入到結構中
- 將對象賦值給接口時,會發生拷貝。而接口內部存儲的是指向這復制品的指針,既無法修改復制品的狀態,也無法獲取指針
- 只有當接口存儲的類型和對象都為nil時,接口才等於nil
- 接口調用不會做receiver的自動轉換
- 接口同樣支持匿名字段方法
- 接口也可實現類似oop中的多態
- 空接口可以作為任何類型數據的容器
接口是用來定義行為的類型,這些被定義的行為不由接口直接實現,而是通過方法由用戶定義的類型實現。
類型斷言
- 通過類型斷言的ok pattern可以判斷接口中的數據類型
- 使用type switch則可針對空接口進行比較全面的類型判斷
接口轉換
- 可以將擁有超集的接口轉換為子集的接口
8.3 反射reflection
- 反射可大大提高程序的靈活性,使得interface{}有更大的發揮余地
- 反射使用type()和valueof函數從接口中獲取目標對象信息
- 反射會將匿名字段作為獨立字段
- 想要利用反射修改對象狀態,前提是interface.data是settable,即pointer-interface
- 通過反射可以動態調用方法
MethodByName()方法使用原對象的方法名name獲取該方法的Value值,如果所訪問的方法不存在,MethodByName會返回0.
在go語言中傳遞給方法的參數要和方法定義的參數類型保持一致,為了處理變參這種復雜情況,傳遞給被調用方法的參數通常首先保存在一個Slice中,然后在復制到參數列表中。
9.並發
- 協程coroutine
協程本質是一種用戶態線程,不需要操作系統進行搶占性調度,而且在真正執行的時候中寄存於線程中。因此,協程系統開銷極小,可以有效提高線程任務的並發性,避免高並發模式下線程並發的缺點。協程最大的優勢在於輕量級,可以輕松創建上百萬個而不是導致系統資源衰竭,系統最多能創建的進程、線程的數量卻少的可憐。使用協程的優點是編程簡單,結果清晰。但是缺點就是需要語言的支持,如果語言不支持,則需要用戶在程序中自行進行調度。 - 定義普通函數之后,調用時候前面加上go關鍵字就可以了。
- 通道Channel
- Channel是goroutine溝通的橋梁,大都是阻塞同步的
- 通過make進行創建,close關閉
- channel是引用類型
- 可以使用for range來迭代不斷操作channel
- 可以設置單向或雙向通道
- 可以設置緩存大小,在未被填滿前不會發生阻塞
- Select
- 可處理一個或多個channel的發送與接收
- 同時有多個可用的channel時按隨機順序處理
- 可用空的select來阻塞main函數
- 可設置超時
var varName chan elementType var c chan int //和普通變量相比,只是增加了一個chan ch := make(chan int) // 使用make函數直接聲明並初始化channel
9.1 channel數據的接收和發送
Channel的主要用途是在不同的Goroutine之間傳遞數據,它使用通道運算符<-接收和發送數據,將一個數據發送(寫入)至channe的方法是 ch <- value
- 向Channel寫入數據通常會導致程序阻塞,知道其他Goroutine從這個Channe中讀取數據,從Channel中接收(讀取)數據的語法如下:
value := <- ch
如果沒有寫入數據,那么在讀取的時候也會阻塞,直到寫入數據為止。 - 關閉Channel的方法
close(chanName)
,在關閉一個channel之后,用戶還需要判斷Channel是否關閉,可以使用多重返回值的方法:
value,ok := <- ch
只需要看第二個bool返回值極客,如果返回值是false則表示Channel已關閉。
- 只有發送端(另一端正在等待接收)才能關閉Channel,只有接收端才能獲得關閉狀態,Close調用不是必需的,但是如果接收端使用range或者循環接收數據,就必須調用Close,否則就會導致“throw: all gorountines are asleep-deadlock”
- 單向Channel
- 只能接收的Channel變量定義格式
var chanName chan <- ElementType
- 只能發送的Channel變量定義格式
var chanName <- chan ElementType
在定義了Channel之后,還需要對其進行初始化操作,可以由一個已定義的雙向Channel轉換而來。
- 只能接收的Channel變量定義格式
ch := make(chan int) chRead := <- chan int(ch) chWrite := chan <- int(ch)
- 異步Channel
在Goroutine間傳輸大量數據的時候,可以使用異步通道(Asynchronous-channel),類比消息隊列的效果。
異步Channel就是給Channel設定一個buffer值,在buffer未寫滿的情況下,不阻塞發送操作。buffer指的是被緩沖的數據對象的數量,而不是指內存大小。
- 異步Channel的創建方法:
ch := nake(chan int,1024)
該例創建了一個1024的int類型的Channel。
9.2 select機制和超時機制
select機制每個case語句必須是一個I/O操作,其基本結構如下:
select { case <- chan1: //如果chan1成功讀取數據,則進行該case處理語句。 case <- chan2: //如果chan2成功讀取數據,則進行改該case處理語句。 default: // 如果上面都沒有成功,則進入default處理流程。 }
- 超時機制
超時機制是一種解決通信死鎖問題的機制,通常會設置一個超時參數,通信雙方如果在設定的時間內仍然沒有處理完任務,則該處理過程就會被終止,並返回對應的超時信息。