15分鐘學會Go語言

Yujiaao發表於2022-06-07

Go 是出於完成工作的需要而建立的。這不是程式語言理論的最新趨勢,但它是解決現實世界問題的一種方法。

它從具有靜態型別的命令式語言中汲取概念。它編譯速度快,執行速度快,它增加了易於理解的併發性,因為現在多核 CPU 很常見,並且它成功地用於大型程式碼庫(Google 有大約 1 億行Go程式碼)。

第一分鐘: 約定

下載安裝

GoLand

專案目錄結構

  • pkg:編譯後生成檔案
  • src :專案的原始碼
  • bin:編譯後可執行的檔案

第二分鐘:語法

// 單行註釋
/* 多
行註釋 */

 /* 構建標籤是以 // +build 開頭的行註釋
  ,可以通過 go build -tags="foo bar" 命令執行。
  構建標記放置在靠近或檔案頂部的 package 子句之前,
  後跟空行或其他行註釋。*/ 
// +build prod, dev, test

// package 子句啟動每個原始檔。
// Main 是一個特殊的名稱,它宣告一個可執行檔案而不是一個庫。
package main

// 匯入宣告宣告此檔案中引用的庫包。
import  ( 
    "fmt"        // Go 標準庫中的一個包。
    "io/ioutil"  // 實現一些 I/O 實用函式。
    m  "math"    // 具有本地別名為 m 的數學庫。
    "net/http"   //是的,一個網路伺服器!
    “os”         //作業系統功能,如:使用檔案系統
    “strconv”    //字串轉換。
)

// 一個函式定義。main是特別的。它是
// 可執行程式的入口點。愛也好恨也好,Go 使用大括號。
func  main ()  { 
    // Println 輸出一行到標準輸出。
    // 它來自包 fmt. 
    fmt.Println("Hello world!")

    // 在這個包中呼叫另一個函式。
    beyondHello()
}

// 函式有括號中的引數。
// 如果沒有引數,仍然需要空括號。
func beyondHello() {
    var x int  // 變數宣告。變數必須在使用前宣告。
    x = 3      // 變數賦值。
    // “短”宣告使用 := 來推斷型別、宣告和分配。
    y := 4 
    sum, prod := learnMultiple(x, y)         // 函式返回兩個值。
    fmt.Println("sum:", sum, "prod:", prod)  // 簡單輸出。
    learnTypes()                             // < 15 分鐘,瞭解更多!
}

/* <- 多行註釋
函式可以有引數和(多個!)返回值。
這裡 `x`、`y` 是引數,`sum`、`prod` 是簽名(返回的內容)。
注意 `x` 和 `sum` 接收型別 `int`。
*/ 
func learnMultiple(x, y int) (sum, prod int) {
    return  x + y ,  x * y  // 返回兩個值。
}

// 一些內建型別和文字。
func  learnTypes ()  { 
    // 簡短的宣告通常會給你你想要的。
    str  :=  "學習Go!"  // 字串型別。

    s2  :=  `“原始”字串文字
可以包含換行符。`  // 相同的字串型別。

    // 非 ASCII 文字。Go 原始碼是 UTF-8。
    g  :=  'Σ'  // rune (符文)型別,int32 的別名,包含一個 unicode 程式碼點。

    f  :=  3.14195  // float64,一個 IEEE-754 64 位浮點數。
    c  :=  3  +  4i   // complex128,內部用兩個 float64 表示。

    // 帶有初始化器的 var 語法。
    var u uint = 7  // 無符號,但與 int 一樣取決於實現的大小。
    var pi float32 = 22. / 7

    // 帶有簡短宣告的轉換語法。
    n := byte('\n')  // byte 是 uint8 的別名。

    // 陣列的大小在編譯時是固定的。
    var a4 [4]int               // 4 個 int 的陣列,初始化為全 0。
    a5 := [...]int{3, 1, 5, 10, 100} // 一個固定大小為 5 的陣列初始化
    // 元素,值為 3、1、5、10 和 100。

    // 陣列具有值語義。
    a4_cpy := a4             // a4_cpy 是 a4 的副本,兩個獨立的例項。
    a4_cpy[0] = 25          // 只有 a4_cpy 改變了,a4 保持不變。
    fmt.Println(a4_cpy[0] == a4[0])  // false

    // 切片具有動態大小。陣列和切片各有優勢
    // 但切片的用例更為常見。
    s3 := []int{4, 5, 9}     // 與 a5 比較。這裡沒有省略號。
    s4 := make([]int, 4)     // 分配 4 個 int 的切片,初始化為全 0。
    var d2 [][]float64       // 僅宣告,此處不分配任何內容。
    bs := []byte("a slice")  // 型別轉換語法。

    // 切片(以及地圖和通道)具有引用語義。
    s3_cpy  :=  s3             // 兩個變數都指向同一個例項。
    s3_cpy[0] = 0             // 這意味著兩者都更新了。
    fmt.Println(s3_cpy[0] == s3[0])  // 真

    // 因為它們是動態的,切片可以按需追加。
    // 要將元素附加到切片,使用內建的 append() 函式。
    // 第一個引數是我們要附加的切片。通常,
    // 陣列變數會就地更新,如下例所示。
    s := []int{1, 2, 3}     // 結果是一個長度為 3 的切片。
    s = append(s, 4, 5, 6)   // 新增了 3 個元素。切片現在的長度為 6. 
    fmt.Println(s) // 更新的切片現在是 [1 2 3 4 5 6]

    // 要附加另一個切片,而不是原子元素列表,我們可以
    // 傳遞對切片的引用或像這樣的切片字面量,帶有一個
    // 尾隨省略號,意思是獲取一個切片並解包其元素,
    // 附加它們切片 s。
    s = append(s, []int{7, 8, 9}...)   // 第二個引數是一個切片文字。
    fmt.Println(s)   // 更新後的切片現在是 [1 2 3 4 5 6 7 8 9]

    p, q := learnMemory()  // 宣告 p, q 為指向 int 的型別指標。
    fmt.Println(*p, *q)    // * 跟隨一個指標。這會列印兩個整數。

    // Map 是一種動態可增長的關聯陣列型別,就像一些其他語言的
    // 雜湊或字典型別。
    m := map[string]int{"three": 3, "four": 4}
    m["one"] = 1

    // 未使用的變數是 Go 中的錯誤。
    // 下劃線讓你“使用”一個變數但丟棄它的值。
   _, _, _, _, _, _, _, _, _, _ = str, s2, g, f, u, pi, n, a5, s4, bs 
    // 通常你用它來忽略其中一個函式的返回值
    // 例如,在一個又髒又快的指令碼中,您可能會忽略
    // 從 os.Create 返回的錯誤值,並期望檔案
    // 將始終被建立。
    file, _ := os.Create("output.txt")
    fmt.Fprint(file, "This is how you write to a file, by the way")
    file.Close()

    // 當然,輸出算作使用變數。
    fmt.Println(s, c, a4, s3, d2, m)

    learnFlowControl()  // 回到流程中。
}

// 與許多其他語言不同,go 中的函式
// 有可能具有命名的返回值。
// 為函式宣告行中返回的型別分配一個名稱
// 允許我們輕鬆地從函式中的多個點返回,以及
// 僅使用 return 關鍵字,而無需進一步。
func learnNamedReturns(x, y int) (z int) {
    z = x * y
    return  // z 在這裡是隱含的,因為我們之前命名了它。
}

// Go 是完全垃圾回收的。它有指標但沒有指標算術。
// 你可以用 nil 空指標搞出錯,但不能通過遞增修改指標。
// 與 C/Cpp 不同,獲取和返回區域性變數的地址也是安全的。
func learnMemory() (p, q *int) {
    // 命名返回值 p 和 q 具有指向 int 的型別指標。
    p = new(int)  // 內建函式 new 分配記憶體。
    // 分配的 int slice 初始化為 0,p 不再為 nil。
    s := make([]int, 20)  // 分配 20 個整數作為單個記憶體塊。
    s[3] = 7              // 分配其中之一。
    r := -2               // 宣告另一個區域性變數。
    return &s[3], &r      // & 獲取物件的地址。
}


// 使用別名 數學庫(參見上面的匯入)
     
func expensiveComputation() float64 {
    return m.Exp(10)
}

func learnFlowControl() {
    // If 語句需要大括號,不需要括號。
    if true {
        fmt.Println("told ya")
    }
    // 格式由命令列命令“go fmt”標準化。
    if  false  { 
        // 噘嘴 - Pout 
    }  else  { 
        // 幸災樂禍 - Gloat
    } 
    // 使用 switch 優先於鏈式 if 語句。
    x := 42.0
    switch x {
    case 0:
    case 1, 2: // 一個case可以有多個匹配
    case 42: 
        // case不會“失敗”。
        /*
        但是有一個 `fallthrough` 關鍵字,請參閱:
          https ://github.com/golang/go/wiki/Switch#fall-through 
        */ 
    case  43 : 
        // Unreached。
    default : 
        // 預設情況是可選的。
    }

    // 型別開關允許開啟某物的型別而不是值
    var data interface{}
    data = ""
    switch c := data.(type) {
    case string:
        fmt.Println(c, "is a string")
    case int64:
        fmt.Printf("%d is an int64\n", c)
    default:
        // 所有其他情況
    }

    // 就像 if, for 也不使用括號。
    // 在 for 和 if 中宣告的變數在其範圍內是本地的。
    for x := 0; x < 3; x++ {  // ++ 是一個語句。
        fmt.Println("iteration", x)
    } 
    // x == 42 這裡。

    // For 是 Go 中唯一的迴圈語句,但它有其他形式。
    for  {  // 無限迴圈
        break     // 開個玩笑
        continue // 未到達
    }

    // 您可以使用 range 來迭代陣列、切片、字串、對映或通道。
    // range 返回一個(通道)或兩個值(陣列、切片、字串和對映)。
    for key, value := range map[string]int{"one": 1, "two": 2, "three": 3} {
        // 對於 map 中的每一對,列印 key 和 value 
        fmt.Printf("key=%s, value=%d\n", key, value)
    }
    // 如果只需要值,使用下劃線作為 _ 的鍵
    for _, name := range []string{"Bob", "Bill", "Joe"} {
        fmt.Printf("Hello, %s\n", name)
    }

    // 與 for 一樣,if 語句中的 := 表示先宣告和賦值
    // y,然後測試 y > x。
    if y := expensiveComputation(); y > x {
        x = y
    }
    // 函式字面量(literals)是閉包(closures)。
    xBig := func() bool {
        return x > 10000 // 引用在 switch 語句上面宣告的 x。
    } 
    x  =  99999 
    fmt.Println("xBig:", xBig())   // true 
    x  =  1.3e3                    // 這使得 x == 1300 
    fmt.Println("xBig:", xBig())   // 現在為假。

    // 更重要的是函式字面量可以被定義時立即呼叫,
    // 作為函式的引數,只要:
    // a) 函式字面量被立即呼叫 (), 
    // b) 結果型別匹配預期的引數型別. 
    fmt.Println("Add + double two numbers: ",
        func(a, b int) int {
            return (a + b) * 2
        }(10, 2)) // 使用 args 10 和 2 呼叫
    // => Add + double兩個數字:24

    // 當你需要它時,你會愛上它。
    goto love
love:

    learnFunctionFactory() // func 返回 func is fun(3)(3) 
    learnDefer()       // 快速繞道一個重要的關鍵字。
    learnInterfaces()  // 好東西來了!
}

func learnFunctionFactory() {
    // 接下來兩個是等價的,第二個更實用
    fmt.Println(sentenceFactory("summer")("A beautiful", "day!"))

    d := sentenceFactory("summer")
    fmt.Println(d("A beautiful", "day!"))
    fmt.Println(d("A lazy", "afternoon!"))
}

// 裝飾器在其他語言中很常見。同樣可以在 Go 
// 中使用接受引數的函式文字來完成。
func sentenceFactory(mystring string) func(before, after string) string {
    return func(before, after string) string {
        return fmt.Sprintf("%s %s %s", before, mystring, after) // 新字串
    }
}

func learnDefer() (ok bool) {
    // defer 語句將函式呼叫推送到列表中。儲存的
    // 呼叫列表在周圍函式返回後執行
    defer fmt.Println("延遲語句以相反 (LIFO) 順序執行.")
    defer fmt.Println("\n此行首先列印,因為")

    // Defer 通常用於關閉檔案,因此關閉檔案的函式
    // 與開啟檔案的函式保持接近。
    return true
}

// 將 Stringer 定義為具有一種方法 String 的介面型別。
type Stringer interface {
    String() string
}

// 將 pair 定義為具有兩個欄位的結構,int 名為 x 和 y。
type pair struct {
    x, y int
}


// 在型別對上定義一個方法。Pair 現在實現了 Stringer,因為 Pair 已經定義了介面中的所有方法。
func (p pair) String() string {  // p 被稱為“接收者” 
    // Sprintf 是 fmt 包中的另一個公共函式。
    // 點語法引用 p 的欄位。
    return fmt.Sprintf("(%d, %d)", p.x, p.y)
}

func learnInterfaces() {
    // 大括號語法是“結構字面量”。它評估為一個初始化的
    // 結構。:= 語法宣告並初始化 p 到這個結構。
    p := pair{3, 4}
    fmt.Println(p.String())   // 呼叫 p 的 String 方法,型別為 pair。
    var  i  Stringer           // 宣告 i 的介面型別為 Stringer。
    i  =  p                    // 有效,因為 pair 實現了 Stringer 
    // 呼叫 i 的 String 方法,Stringer 型別。輸出同上。
    fmt.Println(i.String())

    // fmt 包中的函式呼叫 String 方法來請求物件
    // 獲取其自身的可列印表示。
    fmt.Println(p)   // 輸出同上。Println 呼叫 String 方法。
    fmt.Println(i)  // 輸出同上。

    learnVariadicParams("great", "learning", "here!") 
}

// 函式可以有可變引數。
func learnVariadicParams(myStrings ...interface{}) {
    // 迭代可變引數的每個值。
    // 這裡的下劃線忽略了陣列的索引引數。
    for _, param := range myStrings {
        fmt.Println("param:", param)
    }

    // 將可變引數值作為可變引數傳遞。
    fmt.Println("params:", fmt.Sprintln(myStrings...))

    learnErrorHandling()
}

func  learnErrorHandling ()  { 
    // ", ok" 模式用來判斷某事是否有效。
    m := map[int]string{3: "three", 4: "four"}
    if x, ok := m[1]; !ok {   // ok 將是假的,因為 1 不在map中。
         fmt.Println("no one there")
    } else {
        fmt.Print(x)  // x 將是值,如果它在map中。
    } 
    // 錯誤值不僅傳達“ok”,還傳達更多關於問題的資訊。
    if _, err := strconv.Atoi("non-int"); err != nil {  // _ 丟棄值
        // 列印 'strconv.ParseInt: parsing "non-int": invalid syntax' 
        fmt.Println(err)
    } 
    // 我們稍後再討論介面。併發,
    learnConcurrency()
}

// c 是一個通道,一個併發安全的通訊物件。
func inc(i int, c chan int) {
    c <- i + 1  // <- 是當頻道出現在左側時的“傳送”運算子。
}

// 我們將使用 inc 來同時增加一些數字。
func learnConcurrency() {
    // 之前使用相同的 make 函式來製作切片。Make 分配和
    // 初始化切片、對映和通道。
    c := make(chan int) 
    // 啟動三個併發的 goroutine。
    // 如果機器有能力並且//正確配置,數字將同時遞增,可能是並行遞增。
    這三個都傳送到同一個頻道。
    go inc(0, c)  // go 是一個啟動新 goroutine 的語句。
    go inc(10, c)
    go inc(-805, c) 
    // 從通道中讀取三個結果並列印出來。
    // 不知道結果將以什麼順序到達!
    fmt.Println(<-c, <-c, <-c)  // 右邊的通道,<- 是“接收”操作符。

    cs := make(chan string)       // 另一個通道,這個通道處理字串。
    ccs := make(chan chan string)  // 字串通道的通道。
    go func() { c <- 84 }()        // 啟動一個新的 goroutine 只是為了傳送一個值。
    go func() { cs <- "wordy" }()  // 同樣,這次是 cs。
    // Select 的語法類似於 switch 語句,但每種情況都涉及
    // 一個通道操作。它從案例中隨機選擇一個案例
    // 準備好進行通訊。
    select  { 
    case i := <-c:   // 接收到的值可以分配給變數
        fmt.Printf("it's a %T", i) 
    case <-cs:  // 或者接收到的值可以被丟棄。
        fmt.Println("it's a string")
    case <-ccs:   // 空通道,未準備好進行通訊。
        fmt.Println("didn't happen.")
    }
    // 此時,從 c 或 cs 中獲取了一個值。上面啟動的兩個
    // goroutine 之一已經完成,另一個將保持阻塞狀態。

    learnWebProgramming()   // Go 做到了。你也想做。
}

// http 包中的單個函式啟動 Web 伺服器。
func learnWebProgramming() {

    // ListenAndServe 的第一個引數是要監聽的 TCP 地址。
    // 第二個引數是一個介面,具體是http.Handler。
    go func() {
        err := http.ListenAndServe(":8080", pair{})
        fmt.Println(err) // 不要忽略錯誤
    }()

    requestServer()
}
    

// 通過實現它的唯一方法 ServeHTTP,使配對成為一個 http.Handler。
func (p pair) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // 使用 http.ResponseWriter 方法提供資料。
    w.Write([]byte("You learned Go in Y minutes!"))
}

func requestServer() {
    resp, err := http.Get("http://localhost:8080")
    fmt.Println(err)
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    fmt.Printf("\nWebserver said: `%s`", string(body))
}

延伸閱讀

Go 的所有東西源於官方的 Go 網站。在那裡,您可以按照教程進行操作,進行互動式遊戲並閱讀大量內容。除了遊覽之外,這些文件還包含有關如何編寫乾淨有效的 Go 程式碼、包和命令文件以及釋出歷史的資訊。

強烈推薦Go 語言規範本身。它易於閱讀且非常短(就像現在的語言定義一樣。)

您可以在Go Playground上玩弄程式碼。嘗試更改它並從瀏覽器執行它!請注意,您可以使用https://play.golang.org 作為 REPL 在瀏覽器中測試事物和程式碼,甚至無需安裝 Go。

Go 學生的閱讀清單上是標準庫的原始碼。全面記錄,它展示了最好的可讀性和可理解的 Go、Go 風格和 Go 習慣用法。或者您可以單擊文件中的函式名稱並顯示原始碼!

另一個學習 Go 的好資源是Go by example。

YouTube 上有很多關於 Go 的優秀會議演講和視訊教程,這裡有三個非常好的播放列表,分別為初學者、中級和高階 Gophers 量身定製:

  • Golang University 101介紹了基本的 Go 概念並向您展示如何使用用於建立和管理 Go 程式碼的 Go 工具
  • Golang University 201 更上一層樓,解釋了測試、Web 服務和 API 等重要技術
  • Golang University 301深入探討了更高階的主題,例如 Go 排程程式、地圖和通道的實現、和優化技術

Go Mobile 增加了對移動平臺(Android 和 iOS)的支援。您可以編寫全 Go 原生移動應用程式或編寫包含來自 Go 包的繫結的庫,這些繫結可以通過 Java (Android) 和 Objective-C (iOS) 呼叫。檢視Go Mobile 頁面瞭解更多資訊。

相關文章