一天一語言:快速入門 Go 語言

因爲在看 docker 源代碼,必須需要了解 Go 語言,所以做了一些學習和記錄,主要記錄兩者不同的地方。根據實際代碼閱讀中的問題而來,省略了和 C 語言相同的部分, 乾貨滿滿。

Go 語言定義類型和變量名,方向和一般語言是反的,這點我覺得簡直是反人類,非要搞個不一樣的顯示自己多牛牪犇

關鍵字

GO 程序結構

|-- bin 編譯後的可執行文件

|-- pkg 編譯後的包文件 (.a)

|-- src 源代碼

一般來說, bin 和 pkg 不用創建, go 命令自動創建

編譯運行

兩種方式

  1. 直接執行
$go run hello.go  # 實際是編譯成A.OUT再執行
  1. 編譯執行
$go build hello.go
$./hello

關於分號

其實,和 C 一樣,Go 的正式的語法使用分號來終止語句。和 C 不同的是,這些分號由詞法分析器在掃描源代碼過程中使用簡單的規則自動插入分號,因此輸入源代碼多數時候就不需要分號了。

先來看一個最簡單的例子, 程序員都知道的 hello world

package main

import "fmt"

func main() {
   fmt.Println("Hello, World!")
}

package main 就定義了包名。你必須在源文件中非註釋的第一行指明這個文件屬於哪個包,如:package main。package main 表示一個可獨立執行的程序,每個 Go 應用程序都包含一個名爲 main 的包

import 特殊語法

加載自己寫的模塊:

import "./model"    # 當前文件同一個目錄下的model目錄
import "url/model"  # 加載GOPATH/src/url/model

點(.)操作

點(.)操作的含義是:點(.)標識的包導入後,調用該包中函數時可以省略前綴包名。

package main

import (
    . "fmt"
    "os"
)

func main() {
    for _, value := range os.Args {
        Println(value)
    }
}

別名操作

別名操作的含義是:將導入的包命名爲另一個容易記憶的別名

package main

import (
    f "fmt"
    "os"
)

func main() {
    for _, value := range os.Args {
        f.Println(value)
    }
}

下劃線(_)操作

下劃線()操作的含義是:導入該包,但不導入整個包,而是執行該包中的 init 函數,因此無法通過包名來調用包中的其他函數。使用下劃線()操作往往是爲了註冊包裏的引擎,讓外部可以方便地使用。

import _ "package1"
import _ "package2"
import _ "package3"
...

變量

變量定義

和 C 語言是反的

var variable_list data_type

也可以採用混合型

var a,b,c = 3,4,"foo"

:=

:= 表示聲明變量並賦值

d:=100        #系統自動推斷類型,不需要var關鍵字

特殊變量

""是特殊變量, 任何賦值給"" 的值都會被丟棄

指針

var var_name *var_type

nil 爲空指針

數組

var variable_name [size] variable_type

var balance [10] float32
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
var balance = []float32{1000.0, 2.0, 3.4, 7.0, 50.0}

數組的切片,和 python 的風格

a := [5]int{1, 2, 3, 4, 5}
 
b := a[2:4] // a[2] 和 a[3],但不包括a[4]
fmt.Println(b)
 
b = a[:4] // 從 a[0]到a[4],但不包括a[4]
fmt.Println(b)
 
b = a[2:] // 從 a[2]到a[4],且包括a[2]
fmt.Println(b)

循環與判斷

//經典的for語句 init; condition; post
for i := 0; i<10; i++{
     fmt.Println(i)
}
 
//精簡的for語句 condition
i := 1
for i<10 {
    fmt.Println(i)
    i++
}

函數

func function_name( [parameter list] ) [return_types]
{
   body of the function
}

函數可以有多個返回值

函數還可以輸入不定參數, 詳細用法見例子:

func sum(nums ...int) {
    fmt.Print(nums, " ")  //輸出如 [1, 2, 3] 之類的數組
    total := 0
    for _, num := range nums { //要的是值而不是下標
        total += num
    }
    fmt.Println(total)
}
func main() {
    sum(1, 2)
    sum(1, 2, 3)
 
    //傳數組
    nums := []int{1, 2, 3, 4}
    sum(nums...)
}

方法

一個方法就是一個包含了接受者的函數,接受者可以是命名類型或者結構體類型的一個值或者是一個指針。所有給定類型的方法屬於該類型的方法集。

語法:

func (variable_name variable_data_type) function_name() [return_type]{
   /* function body*/
}

下面定義一個結構體類型和該類型的一個方法:

type User struct {
  Name  string
  Email string
}
func (u User) Notify() error

首先我們定義了一個叫做 User 的結構體類型,然後定義了一個該類型的方法叫做 Notify,該方法的接受者是一個 User 類型的值。要調用 Notify 方法我們需要一個 User 類型的值或者指針:

// User 類型的值可以調用接受者是值的方法
damon := User{"AriesDevil", "ariesdevil@xxoo.com"}
damon.Notify()

// User 類型的指針同樣可以調用接受者是值的方法
alimon := &User{"A-limon", "alimon@ooxx.com"}
alimon.Notify()

注意,當接受者不是一個指針時,該方法操作對應接受者的值的副本 (意思就是即使你使用了指針調用函數,但是函數的接受者是值類型,所以函數內部操作還是對副本的操作,而不是指針操作), 當接受者是指針時,即使用值類型調用那麼函數內部也是對指針的操作

接口

Go 和傳統的面向對象的編程語言不太一樣, 沒有類和繼承的概念. 通過接口來實現面向對象.

語法:

type Namer interface {
    Method1(param_list) return_type
    Method2(param_list) return_type
   ...
}

實現某個接口的類型,除了實現接口的方法外,還可以有自己的方法。

package main

import "fmt"

type Shaper interface {
    Area() float64
    //  Perimeter() float64
}

type Rectangle struct {
    length float64
    width  float64
}

// 實現 Shaper 接口中的方法
func (r *Rectangle) Area() float64 {
    return r.length * r.width
}

// Set 是屬於 Rectangle 自己的方法
func (r *Rectangle) Set(l float64, w float64) {
    r.length = l
    r.width = w
}

func main() {
    rect := new(Rectangle)
    rect.Set(2, 3)
    areaIntf := Shaper(rect)
    fmt.Printf("The rect has area: %f\n", areaIntf.Area())
}

如果去掉 Shaper 中 Perimeter() float64 的註釋,編譯的時候報錯誤,這是因爲 Rectangle 沒有實現 Perimeter() 方法。

  • 多個類型可以實現同一個接口。

內存分配

有 new 和 make

錯誤處理 – Defer

爲了進行錯誤處理, 比如防止資源泄露, go 設計了一個 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)
}

Go 的 defer 語句預設一個函數調用(延期的函數),該調用在函數執行 defer 返回時立刻運行。該方法顯得不同常規,但卻是處理上述資源泄露情況很有效,無論函數怎樣返回,都必須進行資源釋放。

再看一個列子

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

被延期的函數以後進先出(LIFO)的順行執行,因此以上代碼在返回時將打印 4 3 2 1 0

協程 goroutine

先來複習下, 進程, 線程和協程的概念, GoRoutine 就是 Go 的協程

goroutine 基本概念

GoRoutine 主要是使用 go 關鍵字來調用函數,你還可以使用匿名函數;

注意, go routine 被調度的的先後順序是沒法保證的

package main
import "fmt"

func f(msg string) {
    fmt.Println(msg)
}

func main(){
    go f("goroutine")

    go func(msg string) {
        fmt.Println(msg)
    }("going")
}

下面來看一個常見的錯誤用法

array := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"}
var i = 0
for index, item := range array {
  go func() {
      fmt.Println("index:", index, "item:", item)
      i++
  }()
}
time.Sleep(time.Second * 1)
fmt.Println("------------------")
//output:
------------------
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
index: 8 item: i
------------------

最初的意圖是 index 與 item 每次爲 1,a;2,b;3,c;…. 這樣, 結果卻不是這樣, 到底什麼原因呢?

這裏的 go func 每個 index 與 item 是共享的,並不是局部的,由於 for 循環的執行是很快的,每次循環啓動一個 go routine,在 for 循環結束之後(此時 index 與 item 的值分別變成了 8 與 e),但是這個時候第一個啓動的 goroutine 可能還沒有開始執行,由於它們是共享變量的,之後所有輸出的 index 與 item 都是 8 與 e 於是出現了上面的效果。

將原來程序做些修改就能滿足要求了:

for i = 0; i < length; i++ {
  go func(index int) {
  //這裏如果打印 array[i]的話 就會index out of range了因爲i是全局的(在執行到打印語句的時候i的值已經變成了length+1了)不是新啓動的這個goroutine的
  //新啓動的goroutine與原來的main routine 是共享佔空間的 因此 這個i也是共享的
  fmt.Println("index:", index, "item:", array[index])
  }(i)

goroutine 併發

goroutine 有個特性,如果一個 goroutine 沒有被阻塞,那麼別的 goroutine 就不會得到執行, 這並不是真正的併發,如果你要真正的併發,你需要在你的 main 函數的第一行加上下面的這段代碼:

import "runtime"
...
runtime.GOMAXPROCS(4)

goroutine 併發安全性問題, 需要注意:

var mutex = &sync.Mutex{} //可簡寫成:var mutex sync.Mutex
mutex.Lock()
...
mutex.Unlock()
import "sync/atomic"
......
atomic.AddUint32(&cnt, 1)
......
cntFinal := atomic.LoadUint32(&cnt)//取數據

Channel 信道

Channel 的基本概念

Channal 就是用來通信的,像 Unix 下的管道一樣,

它的操作符是箭頭 "<-" , 箭頭的指向就是數據的流向

ch <- v // 發送值v到Channel ch中
v := <-ch // 從Channel ch中接收數據,並將數據賦值給v

下面的程序演示了一個 goroutine 和主程序通信的例程。

package main

import "fmt"

func main() {
    //創建一個string類型的channel
    channel := make(chan string)

    //創建一個goroutine向channel裏發一個字符串
    go func() { channel <- "hello" }()

    msg := <- channel
    fmt.Println(msg)
}

chan 爲先入先出的隊列, 有三種類型, 雙向, 只讀, 只寫, 分別爲 "chan","chan<-","<-chan"

初始化時候, 可以指定容量make(chanint,100); 容量 (capacity) 代表 Channel 容納的最多的元素的數量

Channel 的阻塞

channel 默認上是阻塞的,也就是說,如果 Channel 滿了,就阻塞寫,如果 Channel 空了,就阻塞讀。於是,我們就可以使用這種特性來同步我們的發送和接收端。

package main

import "fmt"
import "time"

func main() {

    channel := make(chan string) //注意: buffer爲1

    go func() {
        channel <- "hello"
        fmt.Println("write \"hello\" done!")

        channel <- "World" //Reader在Sleep,這裏在阻塞
        fmt.Println("write \"World\" done!")

        fmt.Println("Write go sleep...")
        time.Sleep(3*time.Second)
        channel <- "channel"
        fmt.Println("write \"channel\" done!")
    }()

    time.Sleep(2*time.Second)
    fmt.Println("Reader Wake up...")

    msg := <-channel
    fmt.Println("Reader: ", msg)

    msg = <-channel
    fmt.Println("Reader: ", msg)

    msg = <-channel //Writer在Sleep,這裏在阻塞
    fmt.Println("Reader: ", msg)
}

結果爲

Reader Wake up...
Reader:  hello
write "hello" done!
write "World" done!
Write go sleep...
Reader:  World
write "channel" done!
Reader:  channel

多個 Channel 的 select

package main
import "time"
import "fmt"

func main() {
    //創建兩個channel - c1 c2
    c1 := make(chan string)
    c2 := make(chan string)

    //創建兩個goruntine來分別向這兩個channel發送數據
    go func() {
        time.Sleep(time.Second * 1)
        c1 <- "Hello"
    }()
    go func() {
        time.Sleep(time.Second * 1)
        c2 <- "World"
    }()

    //使用select來偵聽兩個channel
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }
}

定時器

Go 語言中可以使用 time.NewTimer 或 time.NewTicker 來設置一個定時器,這個定時器會綁定在你的當前 channel 中,通過 channel 的阻塞通知機器來通知你的程序。

package main

import "time"
import "fmt"

func main() {
    timer := time.NewTimer(2*time.Second)

    <- timer.C
    fmt.Println("timer expired!")
}

關閉 channel

使用 close 命令

close(channel)

系統調用

Go 語言主要是通過兩個包完成的。一個是 os 包,一個是 syscall 包。

這兩個包裏提供都是 Unix-Like 的系統調用,

如執行命令行

package main
import "os/exec"
import "fmt"
func main() {
    cmd := exec.Command("ping", "127.0.0.1")
    out, err := cmd.Output()
    if err!=nil {
        println("Command Error!", err.Error())
        return
    }
    fmt.Println(string(out))
}

作者:Chenzongshu
鏈接:https://www.jianshu.com/p/fe16d3762043
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/htQ4_HMxka8OBCBcBmgpqw