1. 概述
Go 是一個開源的程式設計語言,它能讓構造簡單、可靠且高效的軟體變得容易。Go是從2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持開發,後來還加入了Ian Lance Taylor, Russ Cox等人,並最終於2009年11月開源,在2012年早些時候發布了Go 1穩定版本。現在Go的開發已經是完全開放的,並且擁有一個活躍的社區。
2. 優缺點
2.1 優點
- 開源
- 編譯性語言, 運行高速
- 文法簡潔
- 平行處理封裝
- 記憶體管理、數組安全
2.2 缺點
- 作為編譯性語言調試不如指令碼方便
- 在資料分析上沒有指令碼適用
- 對底層的控制沒有基礎語言靈活
3. 用途
常應用於搭載 Web 服務器,儲存叢集或類似用途的巨型中央伺服器的系統程式設計語言。對於高效能分布式系統領域而言,Go 語言無疑比大多數其它語言有著更高的開發效率。它提供了海量並行的支援,在服務端的開發優勢較大。
4. 語言基礎
4.1 基礎資料型別 (Elementary Data Type)
4.1.1 變數和常量
普通賦值:
// var 變數名稱 變數類型 = 值var num int = 1
平行賦值
var num1,num2 int = 1, 2
多行賦值
var ( num1 int = 1 num2 int = 2)
4.1.2 整數類型的命名和寬度
Go的整數類型一共有10個其中計算架構相關的整數類型有兩個:
- 有符號的整數類型 int
- 無符號的整數類型 uint
在不同計算架構的電腦上,它們體現的寬度(儲存某個類型的值所需要的空間)是不一樣的。空間的單位可以是bit也可以是位元組byte。
image
除了這兩個計算架構相關的整數類型之外,還有8個可以顯式表達自身寬度的整數類型:
image
4.1.2 整數類型值的標記法
image
如果以8進位為變數num賦值:
num = 039 // 用"0"作為首碼以表明這是8進位標記法
如果以16進位為變數num賦值:
num = 0x39
4.1.3 浮點數類型
浮點數類型有兩個:float32/float64 浮點數類型的值一般由整數部分、小數點"."和小數部分組成。另外一種表示方法是在其中加入指數部分。指數部分由"E"或"e"以及帶加號或減號的10進位整數表示。例:3.9E-2表示浮點數0.039。3.9E+1表示浮點數39。
有時候浮點數類型值也可以被簡化。比如39.0可以被簡化為39。0.039可以被簡化為.039。 在Go中浮點數的相關部分只能由10進位標記法表示。
4.1.4 複數類型
複數類型有兩個:complex64和complex128。實際上,complex64類型的值會由兩個float32類型的值分別表示複數的實數部分和虛數部分。而complex128類型的值會由兩個float64類型的值表示複數的實數部分和虛數部分。
負數類型的值一般由浮點數表示的實數部分、加號"+"、浮點數表示的虛數部分以及小寫字母"i"組成,比如3.9E+1 + 9.99E-2i。
4.1.5 byte與rune
byte與rune都屬於別名資料型別。byte是uint8的別名資料型別,而rune是int32的別名資料型別。
一個rune的類型值即可表示一個Unicode字元。一個Unicode代碼點通常由"U+"和一個以十六進位標記法表示的整數表示,例如英文字母'A'的Unicode代碼點為"U+0041”。
rune類型的值需要由單引號"'"包裹,不過我們還可以用另外幾種方式表示:
image
另外在rune類型值的表示中支援幾種特殊的字元序列,即:轉義符。如:
image
4.1.6 字串類型
字串的標記法有兩種,即:原生標記法和解釋型標記法。原生標記法,需用用反引號"`"把字元序列包起來,如果用解釋型標記法,則需要用雙引號"""包裹字元序列。
var str1 string = “str”var str1 string = `str`
二者的區別是,前者表示的是所見即所得 (WYSIWYG)的(除了斷行符號符)。後者所表示的值中轉義符會起作用。
字串值是不可變的,如果我們建立了一個此類型的值,就不可能再對它本身做任何修改。
4.1.7 數群組類型
一個數組是可以容納若干相同類型的元素的容器。數組的長度是固定的。如下聲明一個數群組類型:
type MyNumbers [3]int
型別宣告語句由關鍵字type、類型名稱和類型字面量組成
上面這條型別宣告語句實際上是為數群組類型[3]int聲明了一個別名資料型別。這使得我們可以把MyNumbers當作數群組類型[3]int來使用。
我們表示這樣一個數群組類型的值的時候。應該把該類型的類型字面量寫在最左邊,然後用花括弧包裹該值包含的若干元素,各元素之間以(英文半形)逗號分割,即:
[3]int{1,2,3}
現在我們把這個數組字面量賦給一個名為numbers的變數:
var numbers = [3]int{1,2,3}
這是一條變數聲明語句,它在聲明變數的同時為該變數賦值
另一種方式是在其中的類型字面量中省略代表其長度的數組,例:
var numbers = [...]int{1,2,3}
可以用如下方式訪問該變數中的任何一個元素。例:
numbers[0]numbers[1]numbers[2]
如果要修改數組值中的某一個元素值,可以:
numbers[1] = 4
可以用如下方式擷取數組長度:
var length = len(numbers)
如果一個數組沒有賦值,則它的預設值為
[length]type{0,0,0…}
4.1.8 切片類型
切片(slice)與數組一樣也是可以若干相同類型元素的容器。與數組不同的是切片類型的長度不確定。每個切片值都會將數組作為其底層資料結構。表示切片類型的字面量如:
[]int
或者是:
[]string
切片類型的聲明可以這樣:
type MySlice []int
對切片值的表示也與數組值相似
[]int{1,2,3}
運算元組值的方法同樣適用於切片值。還有一種運算元組的方式叫做“切片”,實施切片操作的方式就是切片運算式。例:
var number3 = [5]int{1,2,3,4,5}var slice1 = numbers3[1:4]
上例中切片運算式numbers3[1:4]的結果為[]int{2,3,4}很明顯被切下的部分不包含元素上界索引指向的元素。實際上slice1這個切片值的底層數組正是number3的值。
我們也可以在切片值上實施切片操作:
var slice2 = slice1[1:3]
除了長度切片值以及數組值還有另外一個屬性--容量。數組的容量總是等於其長度,而切片值的容量往往與其長度不同。如:
image
,一個切片值的容量即為它的第一個元素值在其底層數組中的索引值與該數組長度的差值的絕對值。可以使用cap()內建函數擷取數組、切片、通道類型的值的容量:
var capacity2 int = cap(slice2)
切片類型屬於參考型別,它的零值即為nil,即空值。如果我們只聲明了一個切片類型而不為它賦值,則它的預設值 nil。
切片的更多操作方法有些時候我們可以在方括弧中放入第三個正整數。如所示:
numbers3[1:4:4]
第三個正整數為容量上界索引,它意義在於可以把作為結果的切片值的容量設定的更小。它可以限制我們通過這個切片值對其底層數組中的更多元素的訪問。上節中numbers3和slice的指派陳述式如下:
var numbers3 = [5]int{1,2,3,4,5}var slice1 = numbers3[1:4]
這時,變數slice1的值是[]int{2,3,4}。但是我們可以通過如下操作將其長度延展與其容量相同:
slice1 = slice1[:cap(slice1)]
通過此操作,變數slice1的值變為了[]int{2,3,4,5},且其長度和容量均為4。現在number3的值中的索引值在(1,5)範圍內的元素都被體現在了slice1的值中。這是以number3的值是slice1的值的底層數組為前提的。這意味著我們可以輕而易舉地通過切片訪問其底層數組中對應索引值更大的更多元素。如果我們編寫的函數返回了這樣一個切片值,那麼得到它的程式很可能會通過這種技巧訪問到本不應該暴露給它的元素。
如果我們在切片中加入了第三個索引(即容量上限索引),如:
var slice1 = numbers3[1:4:4]
那麼在此之後,我們將無法通過slice1訪問到number3的值中的第五個元素。
雖然切片值在上述方面受到了其容量的限制。但是我們可以通過另外一種手段對其進行不受限制的擴充。這需要用到內建函數append。append會對切片值進行擴充並返回一個新的切片值,使用方法如下:
slice1 = append(slice1, 6, 7)
通過上述操作,slice1的值變為了[]int{2,3,4,6,7}。一旦擴充操作超出了被操作的切片值的容量,那麼該切片的底層數組就會被替換 最後一種操作切片的方式是“複製”。該操作的實施方法是調用copy函數。該函數接收兩個類型相同的切片值作為參數,並把第二個參數值中的元素複製到第一個參數值中的相應位置(索引值相同)上。這裡有兩點需要注意:
- 這種複製遵循最小複製原則,即:被複製的元素的個數總是等於長度較短的那個參值的長度。
- 與append函數不同,copy函數會直接對其第一個參數值進行修改。
例:
var slice4 = []int{0,0,0,0,0,0}copy(slice4, slice1)
通過上述複製操作,slice4會變成[]int{2,3,4,6,7,0,0}。
4.1.9 字典類型
Go語言的字典(Map)類型是一個雜湊表的實現。字典類型的字面量如下:
map[K]T
其中,"K"為鍵的類型,而"T"則代表元素(值)的類型。如果我們描述一個鍵類型為int,實值型別為string的字典類型的話:
map[int]string
字典的鍵類型必須是可比較的,否則會引起錯誤,即鍵不能是切片、字典、函數類型
字典值的字面量標記法實際上與數組的切片的字面量標記法很相似。最左邊仍然是類型字面量,右邊緊挨著由花括弧包裹且有英文逗號分隔的索引值對。每個索引值對的鍵和值之間由冒號分隔。以字典類型map[int]string為例。他的值的字面量可以是這樣的:
map[int]string{1:"a",2:"b"m,3:"c"}
我們可以把這個值賦給一個變數
mm := map[int]string{1:"a",2:"b",3:"c"}
可用索引運算式取出字典中的值:
b := mm[2]
可以用索引運算式賦值:
mm[2] = b + "2"
這樣mm中鍵為2的值變為了"b2"。可以用如下方式向字典中添加一個索引值對:
mm[4] = ""
對於字典值來說,如果指定鍵沒有對應的值則預設為該類型的空值。所以mm[5]會返回一個""。但是這樣的話我們就不知道mm[5]到底是""還是mm[5]沒有這個值。所以go提供了另外一種寫法:
e, ok := mm[5]
針對字典的索引運算式可以有兩個求職結果,第二個求職結果是bool類型的。它用於表明字典值中是否存在指定的索引值對。 從字典中刪除索引值對的方法非常簡單,僅僅是調用內建函數delete:
delete(mm, 4)
無論mm中是否存在以4為鍵的索引值對,delete都刪除。 字典類型屬於參考型別,它的零值即為nil
4.1.10 通道類型
通道(Channel)是Go語言中一種非常獨特的資料結構。它可用於在不同Goroutine之間傳遞類型化的資料。並且是並發安全的。相比之下,之前幾種資料類型都不是並發安全的。
Goroutine可以被看作是承載可被並發執行的代碼塊的載體。它們由Go語言的運行時系統調度,並依託作業系統線程(又稱核心線程)來並發地執行其中的代碼塊。
通道類型的表示方法很簡單,僅由兩部分組成:
chan T
在這個類型字面量中,左邊是代表通道類型的關鍵字chan,而右邊則是一個可變的部分,即代表該通道類型允許傳遞的資料的類型(或稱通道的元素類型)。
與其他的資料類型不同,我們無法表示一個通道類型的值,因此,我們無法用字面量來為通道類型的變數賦值。只能通過調用內建函數make來達到目的。make參數可接受兩個參數,第一個參數是代表了將被初始化的值的類型的字面量(例: chan int),而第二個參數則是值的長度,例如,若我們想要初始化一個長度為5且元素類型為int的通道值,則需要這樣寫:
make(chan int, 5)
make函數也可以被用來初始化切片類型或字典類型的值。
暫存在通道值中的資料是先進先出。下面,我們聲明一個通道類型的變數,並為其賦值:
ch1 := make(chan string, 5)
這樣一來,我們就可以使用接受操作符<-向通道值發送資料了。當然,也可以使用它從通道值接收資料,例如,如果我們要向通道ch1 發送字串"value1",那麼應該這樣做:
ch1 <- “value1"
如果我們從ch1那裡接收字串,則要這樣:
<- ch1
我們可以把接受到字串賦給一個變數,如:
value := <- ch1
與針對字典值的索引運算式一樣,針對通道值的接受操作也可以有第二個結果值:
value, ok := <- ch1
這裡的ok的值是bool類型的。它代表了通道值的狀態,true代表通道值有效,而false則代表通道值已無效(或稱已關閉),更深層次的原因是,如果在接受操作進行之前或過程中通道值被關閉了,則接收操作會立即結束並返回一個該通道值的元素類型的零值。
可以通過函數close來關閉通道:
close(ch1)
對通道值的重複關閉會引發運行時異常,會使程式崩潰。在通道值有效前提下,針對它的發送操作會在通道值已滿(其中緩衝的資料的個數已等於它的長度)時被阻塞。而向一個已被關閉的通道值發送資料會引發運行時異常。針對有效通道值的接收操作會在它已經為空白時被阻塞。通道類型屬於參考型別,它的零值為nil。
4.2 流程式控制制
4.2.1 條件陳述式
對應的關鍵字為if、 else和else if;
if a := 1; a >= 1 { fmt.Println("OK")}
4.2.2 選擇語句
對應的關鍵字為switch、 case和select
switch i { case 0: fmt.Printf("0") case 1: fmt.Printf("1") case 2: fallthrough case 3: fmt.Printf("3") case 4, 5, 6: fmt.Printf("4, 5, 6") default: fmt.Printf("Default")}
4.2.3 迴圈語句
對應的關鍵字為for和range;
sum := 0for i := 0; i < 10; i++ { sum += i}
4.2.4 跳躍陳述式
func myfunc() { i := 0 HERE: fmt.Println(i) i++ if i < 10 { goto HERE }}
4.3 函數
4.3.1 概述
首先函數的格式是固定的,func+函數名+ 參數 + 傳回值(可選) + 函數體。例 :
func main() {fmt.Println("Hello go")}
在golang中有兩個特殊的函數,main函數和init函數,main函數不用介紹在所有語言中都一樣,它作為一個程式的入口,只能有一個。init函數在每個package是可選的,可有可無,甚至可以有多個(但是強烈建議一個package中一個init函數),init函數在你匯入該package時程式會自動調用init函數,所以init函數不用我們手動調用,另外它只會被調用一次,因為當一個package被多次引用時,它只會被匯入一次。
4.3.2 參數傳遞
普通變數
使用普通變數作為函數參數的時候,在傳遞參數時只是對變數值得拷貝,即將實參的值複製給變參,當函數對變參進行處理時,並不會影響原來實參的值。
指標
函數的變數不僅可以使用普通變數,還可以使用指標變數,使用指標變數作為函數的參數時,在進行參數傳遞時將是一個地址看唄,即將實參的記憶體位址複製給變參,這時對變參的修改也將會影響到實參的值。
數組
和其他語言不同的是,go語言在將數組名作為函數參數的時候,參數傳遞即是對數組的複製。在形參中對數組元素的修改都不會影響到數組元素原來的值。
slice, map, chan
在使用slice, map, chan 作為函數參數時,進行參數傳遞將是一個地址拷貝,即將底層數組的記憶體位址複製給參數slice, map, chan 。這時,對slice, map, chan 元素的操作就是對底層數組元素的操作。
函數名字
在go語言中,函數也作為一種資料類型,所以函數也可以作為函數的參數來使用。
4.3.3 傳回值
go語言可以返回局部變數的指標,因為go語言的回收機制是發現有被引用的棧上臨時變數時,會自動存在堆上。
4.3.4 閉包
和其他語言類似,golang也支援閉包函數
package mainimport "fmt"func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum }}func main() { pos, neg := adder(), adder() for i := 0; i < 10; i++ { fmt.Println( pos(i), neg(-2*i), ) }}
4.4 類
4.4.1 概述
type Poem struct { Title string Author string intro string}
這樣就聲明了一個類,其中沒有public、protected、private的的聲明。golang用另外一種做法來實現屬性的存取權限:屬性的開頭字母是大寫的則在其它包中可以被訪問,否則只能在本包中訪問。類的聲明和方法亦是如此。
4.4.2 方法
func (poem *Poem) publish() { fmt.Println("poem publish")}
或者
func (poem Poem) publish() { fmt.Println("poem publish")}
和其它語言不一樣,golang聲明方法和普通方法一致,只是在func後增加了poem Poem這樣的聲明。加和沒有加*的區別在於一個是傳遞指標對象,一個是傳遞值對象。
注意當建立了方法1後將會預設建立出方法2,反之則不會。
4.4.3 類的執行個體化
執行個體化對象有好幾種方式
poem := &Poem{}poem.Author = "Heine"poem2 := &Poem{Author: "Heine"}poem3 := new(Poem)poem3.Author = "Heine"poem4 := Poem{}poem4.Author = "Heine"poem5 := Poem{Author: "Heine"}
執行個體化的時候可以初始化屬性值,如果沒有指明則預設為系統預設值。
4.4.5 偽繼承
golang中不存在繼承,但可以使用組合的方式達到類似的效果.
func (e *Poem) ShowTitle() { fmt.Printf(e.Title)}type Poem struct { Title string Author string intro string}type ProsePoem struct { Poem Author string}
ProsePoem屬性中聲明了Poem,表示組合了Poem的屬性和方法(屬性和方法都會被繼承)。
prosePoem := &ProsePoem{ Poem: Poem{ Title: "Jack", Author: "slow", intro: "simple", }, Author: "test", }
如果其中屬性有衝突,則以外圍的為主,也就是說會被覆蓋。
type ProsePoem struct { Poem Author string}
當訪問Author的時候預設為ProsePoem的Author,如果需要訪問Poem的Author屬性可以使用
prosePoem.Poem.Author來存取方法同理。
prosePoem := &ProsePoem{}prosePoem.Author = "Shelley"prosePoem.Poem.Author = "Heine"fmt.Println(prosePoem)
從輸出中可以很直觀看到這一點。
&{{ Heine } Shelley}
4.5 介面
Golang的interface是方法的集合也是一種類型
package mainimport "fmt"type Animal interface { Speak() string}type Cat struct{}func (c Cat) Speak() string { return "cat"}type Dog struct{}func (d Dog) Speak() string { return "dog"}func Test(params interface{}) { fmt.Println(params)}func main() { animals := []Animal{Cat{}, Dog{}} for _, animal := range animals { fmt.Println(animal.Speak()) } Test("string") Test(123) Test(true)}
如上所示,將Struct 向 Interface變數賦值,實際作用是Interface擷取了Struct中方法的實現集合。
4.6 斷言和反射
斷言是預判確認變數的類型
func main() { v := "hello world" fmt.Println(typeof(v))}func typeof(v interface{}) string { switch t := v.(type) { case int: return "int" case float64: return "float64" //... etc default: _ = t return "unknown" }}
反射則是因為interface中實際記錄的有儲存物件的類型,可以提取出變數的類型
import ( "reflect" "fmt")func main() { v := "hello world" fmt.Println(typeof(v))}func typeof(v interface{}) string { return reflect.TypeOf(v).String()}
4.7 回收處理
在golang當中,defer代碼塊會在函數調用鏈表中增加一個函數調用。這個函數調用不是普通的函數調用,而是會在函數正常返回,也就是return之後添加一個函數調用。因此,defer通常用來釋放函數內部變數。
如下樣本
func CopyFile(dstName, srcName string) (written int64, err error) {src, err := os.Open(srcName)if err != nil {return}defer src.Close()dst, err := os.Create(dstName)if err != nil {return}defer dst.Close()return io.Copy(dst, src)}
通過defer,我們可以在代碼中優雅的關閉/清理代碼中所使用的變數。defer作為golang清理變數的特性,有其專屬且明確的行為。以下是defer三條使用規則。
- 當defer被聲明時,其參數就會被即時解析
即defer的參數的變數值,在代碼中defer使用後的位置改變並不會對改變defer用到的值
- defer執行順序為先進後出
在函數中,先定義的defer將會後執行
- defer可以讀取有名傳回值
defer代碼塊的範圍仍然在函數之內,因此defer仍然可以讀取函數內的變數
4.8 make和new的區別
new 的作用是初始化一個指向類型的指標(*T),make 的作用是為 slice,map 或 chan 初始化並返回引用(T)。
4.9 異常處理
對於異常處理,golang提供了recover和panic。
如下樣本
package main import ( "fmt") func main() { defer func() { fmt.Println("1") }() defer func() { if err := recover(); err != nil { fmt.Println(err) } }() panic("fault") fmt.Println("2")} 運行結果:fault1
程式首先運行panic,出現故障,此時跳轉到包含recover()的defer函數執行,recover捕獲panic,此時panic就不繼續傳遞.但是recover之後,程式並不會返回到panic那個點繼續執行以後的動作,而是在recover這個點繼續執行以後的動作,即執行上面的defer函數,輸出1.
利用recover處理panic指令,必須利用defer在panic之前聲明,否則當panic時,recover無法捕獲到panic,無法防止panic擴散.
4.10 協程
Go調度的幾個概念:
M:核心線程;
G:go routine,並發的最小邏輯單元,由程式員建立;
P:處理器,執行G的上下文環境,每個P會維護一個本地的go routine隊列;
image.png
除了每個P擁有一個本地的go routine隊列外,還存在一個全域的go routine隊列。
具體調度原理:
- P的數量在初始化由GOMAXPROCS決定;
- 我們要做的就是添加G;
- G的數量超出了M的處理能力,且還有空餘P的話,runtime就會自動建立新的M;
- M拿到P後才能幹活,取G的順序:本地隊列>全域隊列>其他P的隊列,如果所有隊列都沒有可用的G,M會歸還P並進入休眠;
一個G如果發生阻塞等事件會進行阻塞,如:
image.png
G發生環境切換條件:
- 系統調用;
- 讀寫channel;
- gosched主動放棄,會將G扔進全域隊列;
如,一個G發生阻塞時,M0讓出P,由M1接管其任務隊列;當M0執行的阻塞調用返回後,再將G0扔到全域隊列,自己則進入睡眠(沒有P了無法幹活);
4.11 包管理
4.12 發布
5. 連絡方式
文檔有誤可發郵箱 xiyanxiyan10@hotmail.com 糾正補充。