go語言快速入門教程

zqlucky發表於2019-06-13

       go快速入門指南

                                                                                   by 小強,2019-06-13

      go語言是目前非常火熱的語言,廣泛應用於伺服器端,雲端計算,kubernetes容器編排等領域。它是一種開源的編譯型程式設計語言,支援併發、垃圾回收機制以提升應用程式效能。它既具有像c這種靜態編譯型語言的高效能,又具備像python這種動態語言的高效性。很多go程式設計師都是從C++,Java等面嚮物件語言因為工作的需要轉過來的,因此沒有必要從0開始學習go,當初自己的想法是找一篇半小時入門go的部落格,貌似沒有類似的好文章=_=。搜到的都是一些從小白入手的臃腫教程,學習起來太慢!!!so,打算寫這篇go語言快速入門的指南。

       本文寫作思路重點在於和C++語言的不同之處入手,強調go的特性,注重快速入門,因此不介紹一些非常基礎的知識,非常基礎的一些知識可以去看go語言聖經。關於go環境的安裝配置以及vscode編輯器的配置在之前的部落格已經介紹,請移步。本文整體組織結構如下:

  1. go程式開發的一般結構
  2. 基本資料型別
  3. 變數的宣告和賦值
  4. 運算子和指標
  5. 判斷語句if和迴圈語句for
  6. 陣列、切片、map
  7. 函式function
  8. 方法、介面、反射
  9. 併發

 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

  • 其本身並不是陣列,它指向底層的陣列,使用[]來做宣告
  • 作為變長陣列的替代方案,可以關聯底層陣列的區域性或全部
  • 屬於引用型別
  • 可以直接建立或從底層陣列獲取生成
  • 使用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位置

    常用基本操作:

   - 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 

  • 類似其他語言中的雜湊表或者字典,以key-value形式儲存資料

  • key必須是支援==或!=比較運算的型別,不可以是函式、map或者slice

  • map查詢比線性搜尋快很多,但比使用索引訪問資料的型別慢100倍

  • map使用make()建立,支援:=這種簡寫方式。

  • make([keyType]valueType,cap),cap表示容量,可省略

  • 超出容量時會自動擴容,但儘量提供一個合理的初始值

  • 使用len()獲取元素個數

  • 鍵值對不存在時自動新增,使用delete()刪除某鍵值對

  • 使用for range對map和slice進行迭代操作

delete(m,1)//map名稱加上需要刪除的下標位置

記住每個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呼叫的函式中有效

      defer,panic,recover

       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轉換而來。
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處理流程。
}
  • 超時機制
    超時機制是一種解決通訊死鎖問題的機制,通常會設定一個超時引數,通訊雙方如果在設定的時間內仍然沒有處理完任務,則該處理過程就會被終止,並返回對應的超時資訊。

 

相關文章