Go 編程指南

每個 Go 程序都是由包構成的。

程序從 main 包開始運行。

本程序通過導入路徑 "fmt""math/rand" 來使用這兩個包。

按照約定,包名與導入路徑的最后一個元素一致。例如,"math/rand" 包中的源碼均以 package rand 語句開始。

注意: 此程序的運行環境是固定的,因此 rand.Intn 總是會返回相同的數字。 (要得到不同的數字,需為生成器提供不同的種子數,參見 rand.Seed。 練習場中的時間為常量,因此你需要用其它的值作為種子數。)

package main

import (
    "fmt"
    "math/rand"
)

func main() {
    fmt.Println("My favorite number is", rand.Intn(10))
}

導入

此代碼用圓括號組合了導入,這是“分組”形式的導入語句。

當然你也可以編寫多個導入語句,例如:

import "fmt"
import "math"

不過使用分組導入語句是更好的形式。

package main

import (
    "fmt"
    "math"
)

func main() {
    fmt.Printf("Now you have %g problems.\n", math.Sqrt(7))
}

導出名

在 Go 中,如果一個名字以大寫字母開頭,那么它就是已導出的。例如,Pizza 就是個已導出名,Pi 也同樣,它導出自 math 包。

pizzapi 并未以大寫字母開頭,所以它們是未導出的。

在導入一個包時,你只能引用其中已導出的名字。任何“未導出”的名字在該包外均無法訪問。

執行代碼,觀察錯誤輸出。

然后將 math.pi 改名為 math.Pi 再試著執行一次。

package main

import (
    "fmt"
    "math"
)

func main() {
    fmt.Println(math.pi)
}

函數

函數可以沒有參數或接受多個參數。

在本例中,add 接受兩個 int 類型的參數。

注意類型在變量名 之后

(參考 這篇關于 Go 語法聲明的文章了解這種類型聲明形式出現的原因。)

package main

import "fmt"

func add(x int, y int) int {
    return x + y
}

func main() {
    fmt.Println(add(42, 13))
}

函數(續)

當連續兩個或多個函數的已命名形參類型相同時,除最后一個類型以外,其它都可以省略。

在本例中,

x int, y int

被縮寫為

x, y int
package main

import "fmt"

func add(x, y int) int {
    return x + y
}

func main() {
    fmt.Println(add(42, 13))
}

多值返回

函數可以返回任意數量的返回值。

swap 函數返回了兩個字符串。

package main

import "fmt"

func swap(x, y string) (string, string) {
    return y, x
}

func main() {
    a, b := swap("hello", "world")
    fmt.Println(a, b)
}

命名返回值

Go 的返回值可被命名,它們會被視作定義在函數頂部的變量。

返回值的名稱應當具有一定的意義,它可以作為文檔使用。

沒有參數的 return 語句返回已命名的返回值。也就是 直接 返回。

直接返回語句應當僅用在下面這樣的短函數中。在長的函數中它們會影響代碼的可讀性。

package main

import "fmt"

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

func main() {
    fmt.Println(split(17))
}

變量

var 語句用于聲明一個變量列表,跟函數的參數列表一樣,類型在最后。

就像在這個例子中看到的一樣,var 語句可以出現在包或函數級別。

package main

import "fmt"

var c, python, java bool

func main() {
    var i int
    fmt.Println(i, c, python, java)
}

變量的初始化

變量聲明可以包含初始值,每個變量對應一個。

如果初始化值已存在,則可以省略類型;變量會從初始值中獲得類型。

package main

import "fmt"

var i, j int = 1, 2

func main() {
    var c, python, java = true, false, "no!"
    fmt.Println(i, j, c, python, java)
}

短變量聲明

在函數中,簡潔賦值語句 := 可在類型明確的地方代替 var 聲明。

函數外的每個語句都必須以關鍵字開始(var, func 等等),因此 := 結構不能在函數外使用。

package main

import "fmt"

func main() {
    var i, j int = 1, 2
    k := 3
    c, python, java := true, false, "no!"

    fmt.Println(i, j, k, c, python, java)
}

基本類型

Go 的基本類型有

bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 的別名

rune // int32 的別名
    // 表示一個 Unicode 碼點

float32 float64

complex64 complex128

本例展示了幾種類型的變量。 同導入語句一樣,變量聲明也可以“分組”成一個語法塊。

int, uintuintptr 在 32 位系統上通常為 32 位寬,在 64 位系統上則為 64 位寬。 當你需要一個整數值時應使用 int 類型,除非你有特殊的理由使用固定大小或無符號的整數類型。

package main

import (
    "fmt"
    "math/cmplx"
)

var (
    ToBe   bool       = false
    MaxInt uint64     = 1<<64 - 1
    z      complex128 = cmplx.Sqrt(-5 + 12i)
)

func main() {
    fmt.Printf("Type: %T Value: %v\n", ToBe, ToBe)
    fmt.Printf("Type: %T Value: %v\n", MaxInt, MaxInt)
    fmt.Printf("Type: %T Value: %v\n", z, z)
}

零值

沒有明確初始值的變量聲明會被賦予它們的 零值

零值是:

  • 數值類型為 0
  • 布爾類型為 false
  • 字符串為 ""(空字符串)。
package main

import "fmt"

func main() {
    var i int
    var f float64
    var b bool
    var s string
    fmt.Printf("%v %v %v %q\n", i, f, b, s)
}

類型轉換

表達式 T(v) 將值 v 轉換為類型 T

一些關于數值的轉換:

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

或者,更加簡單的形式:

i := 42
f := float64(i)
u := uint(f)

與 C 不同的是,Go 在不同類型的項之間賦值時需要顯式轉換。試著移除例子中 float64uint 的轉換看看會發生什么。

package main

import (
    "fmt"
    "math"
)

func main() {
    var x, y int = 3, 4
    var f float64 = math.Sqrt(float64(x*x + y*y))
    var z uint = uint(f)
    fmt.Println(x, y, z)
}

類型推導

在聲明一個變量而不指定其類型時(即使用不帶類型的 := 語法或 var = 表達式語法),變量的類型由右值推導得出。

當右值聲明了類型時,新變量的類型與其相同:

var i int
j := i // j 也是一個 int

不過當右邊包含未指明類型的數值常量時,新變量的類型就可能是 int, float64complex128 了,這取決于常量的精度:

i := 42           // int
f := 3.142        // float64
g := 0.867 + 0.5i // complex128

嘗試修改示例代碼中 v 的初始值,并觀察它是如何影響類型的。

package main

import "fmt"

func main() {
    v := 42 // 修改這里!
    fmt.Printf("v is of type %T\n", v)
}

常量

常量的聲明與變量類似,只不過是使用 const 關鍵字。

常量可以是字符、字符串、布爾值或數值。

常量不能用 := 語法聲明。

package main

import "fmt"

const Pi = 3.14

func main() {
    const World = "世界"
    fmt.Println("Hello", World)
    fmt.Println("Happy", Pi, "Day")

    const Truth = true
    fmt.Println("Go rules?", Truth)
}

數值常量

數值常量是高精度的

一個未指定類型的常量由上下文來決定其類型。

再嘗試一下輸出 needInt(Big) 吧。

int 類型最大可以存儲一個 64 位的整數,有時會更小。)

int 可以存放最大64位的整數,根據平臺不同有時會更少。)

package main

import "fmt"

const (
    // 將 1 左移 100 位來創建一個非常大的數字
    // 即這個數的二進制是 1 后面跟著 100 個 0
    Big = 1 << 100
    // 再往右移 99 位,即 Small = 1 << 1,或者說 Small = 2
    Small = Big >> 99
)

func needInt(x int) int { return x*10 + 1 }
func needFloat(x float64) float64 {
    return x * 0.1
}

func main() {
    fmt.Println(needInt(Small))
    fmt.Println(needFloat(Small))
    fmt.Println(needFloat(Big))
}

for

Go 只有一種循環結構:for 循環。

基本的 for 循環由三部分組成,它們用分號隔開:

  • 初始化語句:在第一次迭代前執行
  • 條件表達式:在每次迭代前求值
  • 后置語句:在每次迭代的結尾執行

初始化語句通常為一句短變量聲明,該變量聲明僅在 for 語句的作用域中可見。

一旦條件表達式的布爾值為 false,循環迭代就會終止。

注意:和 C、Java、JavaScript 之類的語言不同,Go 的 for 語句后面的三個構成部分外沒有小括號, 大括號 { } 則是必須的。

package main

import "fmt"

func main() {
    sum := 0
    for i := 0; i < 10; i++ {
        sum += i
    }
    fmt.Println(sum)
}

初始化語句和后置語句是可選的。

package main

import "fmt"

func main() {
    sum := 1
    for ; sum < 1000; {
        sum += sum
    }
    fmt.Println(sum)
}

for 是 Go 中的 “while”

此時你可以去掉分號,因為 C 的 while 在 Go 中叫做 for

package main

import "fmt"

func main() {
    sum := 1
    for sum < 1000 {
        sum += sum
    }
    fmt.Println(sum)
}

無限循環

如果省略循環條件,該循環就不會結束,因此無限循環可以寫得很緊湊。

package main

func main() {
    for {
    }
}

if

Go 的 if 語句與 for 循環類似,表達式外無需小括號 ( ) ,而大括號 { } 則是必須的。

package main

import (
    "fmt"
    "math"
)

func sqrt(x float64) string {
    if x < 0 {
        return sqrt(-x) + "i"
    }
    return fmt.Sprint(math.Sqrt(x))
}

func main() {
    fmt.Println(sqrt(2), sqrt(-4))
}

if 的簡短語句

for 一樣, if 語句可以在條件表達式前執行一個簡單的語句。

該語句聲明的變量作用域僅在 if 之內。

(在最后的 return 語句處使用 v 看看。)

package main

import (
    "fmt"
    "math"
)

func pow(x, n, lim float64) float64 {
    if v := math.Pow(x, n); v < lim {
        return v
    }
    return lim
}

func main() {
    fmt.Println(
        pow(3, 2, 10),
        pow(3, 3, 20),
    )
}

if 和 else

if 的簡短語句中聲明的變量同樣可以在任何對應的 else 塊中使用。

(在 mainfmt.Println 調用開始前,兩次對 pow 的調用均已執行并返回其各自的結果。)

package main

import (
    "fmt"
    "math"
)

func pow(x, n, lim float64) float64 {
    if v := math.Pow(x, n); v < lim {
        return v
    } else {
        fmt.Printf("%g >= %g\n", v, lim)
    }
    // 這里開始就不能使用 v 了
    return lim
}

func main() {
    fmt.Println(
        pow(3, 2, 10),
        pow(3, 3, 20),
    )
}

練習:循環與函數

為了練習函數與循環,我們來實現一個平方根函數:用牛頓法實現平方根函數。

計算機通常使用循環來計算 x 的平方根。從某個猜測的值 z 開始,我們可以根據 z2 與 x 的近似度來調整 z,產生一個更好的猜測:

z -= (z*z - x) / (2*z)

重復調整的過程,猜測的結果會越來越精確,得到的答案也會盡可能接近實際的平方根。

在提供的 func Sqrt 中實現它。無論輸入是什么,對 z 的一個恰當的猜測為 1。 要開始,請重復計算 10 次并隨之打印每次的 z 值。觀察對于不同的值 x(1、2、3 ...), 你得到的答案是如何逼近結果的,猜測提升的速度有多快。

提示:用類型轉換或浮點數語法來聲明并初始化一個浮點數值:

z := 1.0
z := float64(1)

然后,修改循環條件,使得當值停止改變(或改變非常小)的時候退出循環。觀察迭代次數大于還是小于 10。 嘗試改變 z 的初始猜測,如 x 或 x/2。你的函數結果與標準庫中的 math.Sqrt 接近嗎?

注: 如果你對該算法的細節感興趣,上面的 z2 ? x 是 z2 到它所要到達的值(即 x)的距離, 除以的 2z 為 z2 的導數,我們通過 z2 的變化速度來改變 z 的調整量。 這種通用方法叫做牛頓法。 它對很多函數,特別是平方根而言非常有效。)

package main

import (
    "fmt"
)

func Sqrt(x float64) float64 {
}

func main() {
    fmt.Println(Sqrt(2))
}

switch

switch 是編寫一連串 if - else 語句的簡便方法。它運行第一個值等于條件表達式的 case 語句。

Go 的 switch 語句類似于 C、C++、Java、JavaScript 和 PHP 中的,不過 Go 只運行選定的 case,而非之后所有的 case。 實際上,Go 自動提供了在這些語言中每個 case 后面所需的 break 語句。 除非以 fallthrough 語句結束,否則分支會自動終止。 Go 的另一點重要的不同在于 switch 的 case 無需為常量,且取值不必為整數。

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Print("Go runs on ")
    switch os := runtime.GOOS; os {
    case "darwin":
        fmt.Println("OS X.")
    case "linux":
        fmt.Println("Linux.")
    default:
        // freebsd, openbsd,
        // plan9, windows...
        fmt.Printf("%s.\n", os)
    }
}

switch 的求值順序

switch 的 case 語句從上到下順次執行,直到匹配成功時停止。

(例如,

switch i {
case 0:
case f():
}

i==0f 不會被調用。)

注意: Go 練習場中的時間總是從 2009-11-10 23:00:00 UTC 開始,該值的意義留給讀者去發現。

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("When's Saturday?")
    today := time.Now().Weekday()
    switch time.Saturday {
    case today + 0:
        fmt.Println("Today.")
    case today + 1:
        fmt.Println("Tomorrow.")
    case today + 2:
        fmt.Println("In two days.")
    default:
        fmt.Println("Too far away.")
    }
}

沒有條件的 switch

沒有條件的 switch 同 switch true 一樣。

這種形式能將一長串 if-then-else 寫得更加清晰。

package main

import (
    "fmt"
    "time"
)

func main() {
    t := time.Now()
    switch {
    case t.Hour() < 12:
        fmt.Println("Good morning!")
    case t.Hour() < 17:
        fmt.Println("Good afternoon.")
    default:
        fmt.Println("Good evening.")
    }
}

defer

defer 語句會將函數推遲到外層函數返回之后執行。

推遲調用的函數其參數會立即求值,但直到外層函數返回前該函數都不會被調用。

package main

import "fmt"

func main() {
    defer fmt.Println("world")

    fmt.Println("hello")
}

defer 棧

推遲的函數調用會被壓入一個棧中。當外層函數返回時,被推遲的函數會按照后進先出的順序調用。

更多關于 defer 語句的信息,請閱讀此博文

package main

import "fmt"

func main() {
    fmt.Println("counting")

    for i := 0; i < 10; i++ {
        defer fmt.Println(i)
    }

    fmt.Println("done")
}

指針

Go 擁有指針。指針保存了值的內存地址。

類型 *T 是指向 T 類型值的指針。其零值為 nil

var p *int

& 操作符會生成一個指向其操作數的指針。

i := 42
p = &i

* 操作符表示指針指向的底層值。

fmt.Println(*p) // 通過指針 p 讀取 i
*p = 21         // 通過指針 p 設置 i

這也就是通常所說的“間接引用”或“重定向”。

與 C 不同,Go 沒有指針運算。

package main

import "fmt"

func main() {
    i, j := 42, 2701

    p := &i         // 指向 i
    fmt.Println(*p) // 通過指針讀取 i 的值
    *p = 21         // 通過指針設置 i 的值
    fmt.Println(i)  // 查看 i 的值

    p = &j         // 指向 j
    *p = *p / 37   // 通過指針對 j 進行除法運算
    fmt.Println(j) // 查看 j 的值
}

結構體

一個結構體(struct)就是一組字段(field)。

package main

import "fmt"

type Vertex struct {
    X int
    Y int
}

func main() {
    fmt.Println(Vertex{1, 2})
}

結構體字段

結構體字段使用點號來訪問。

package main

import "fmt"

type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex{1, 2}
    v.X = 4
    fmt.Println(v.X)
}

結構體指針

結構體字段可以通過結構體指針來訪問。

如果我們有一個指向結構體的指針 p,那么可以通過 (*p).X 來訪問其字段 X。不過這么寫太啰嗦了,所以語言也允許我們使用隱式間接引用,直接寫 p.X 就可以。

package main

import "fmt"

type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex{1, 2}
    p := &v
    p.X = 1e9
    fmt.Println(v)
}

結構體文法

結構體文法通過直接列出字段的值來新分配一個結構體。

使用 Name: 語法可以僅列出部分字段。(字段名的順序無關。)

特殊的前綴 & 返回一個指向結構體的指針。

package main

import "fmt"

type Vertex struct {
    X, Y int
}

var (
    v1 = Vertex{1, 2}  // 創建一個 Vertex 類型的結構體
    v2 = Vertex{X: 1}  // Y:0 被隱式地賦予
    v3 = Vertex{}      // X:0 Y:0
    p  = &Vertex{1, 2} // 創建一個 *Vertex 類型的結構體(指針)
)

func main() {
    fmt.Println(v1, p, v2, v3)
}

數組

類型 [n]T 表示擁有 nT 類型的值的數組。

表達式

var a [10]int

會將變量 a 聲明為擁有 10 個整數的數組。

數組的長度是其類型的一部分,因此數組不能改變大小。這看起來是個限制,不過沒關系,Go 提供了更加便利的方式來使用數組。

package main

import "fmt"

func main() {
    var a [2]string
    a[0] = "Hello"
    a[1] = "World"
    fmt.Println(a[0], a[1])
    fmt.Println(a)

    primes := [6]int{2, 3, 5, 7, 11, 13}
    fmt.Println(primes)
}

切片

每個數組的大小都是固定的。而切片則為數組元素提供動態大小的、靈活的視角。在實踐中,切片比數組更常用。

類型 []T 表示一個元素類型為 T 的切片。

切片通過兩個下標來界定,即一個上界和一個下界,二者以冒號分隔:

a[low : high]

它會選擇一個半開區間,包括第一個元素,但排除最后一個元素。

以下表達式創建了一個切片,它包含 a 中下標從 1 到 3 的元素:

a[1:4]
package main

import "fmt"

func main() {
    primes := [6]int{2, 3, 5, 7, 11, 13}

    var s []int = primes[1:4]
    fmt.Println(s)
}

切片就像數組的引用

切片并不存儲任何數據,它只是描述了底層數組中的一段。

更改切片的元素會修改其底層數組中對應的元素。

與它共享底層數組的切片都會觀測到這些修改。

package main

import "fmt"

func main() {
    names := [4]string{
        "John",
        "Paul",
        "George",
        "Ringo",
    }
    fmt.Println(names)

    a := names[0:2]
    b := names[1:3]
    fmt.Println(a, b)

    b[0] = "XXX"
    fmt.Println(a, b)
    fmt.Println(names)
}

切片文法

切片文法類似于沒有長度的數組文法。

這是一個數組文法:

[3]bool{true, true, false}

下面這樣則會創建一個和上面相同的數組,然后構建一個引用了它的切片:

[]bool{true, true, false}
package main

import "fmt"

func main() {
    q := []int{2, 3, 5, 7, 11, 13}
    fmt.Println(q)

    r := []bool{true, false, true, true, false, true}
    fmt.Println(r)

    s := []struct {
        i int
        b bool
    }{
        {2, true},
        {3, false},
        {5, true},
        {7, true},
        {11, false},
        {13, true},
    }
    fmt.Println(s)
}

切片的默認行為

在進行切片時,你可以利用它的默認行為來忽略上下界。

切片下界的默認值為 0,上界則是該切片的長度。

對于數組

var a [10]int

來說,以下切片是等價的:

a[0:10]
a[:10]
a[0:]
a[:]
package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}

    s = s[1:4]
    fmt.Println(s)

    s = s[:2]
    fmt.Println(s)

    s = s[1:]
    fmt.Println(s)
}

切片的長度與容量

切片擁有 長度容量

切片的長度就是它所包含的元素個數。

切片的容量是從它的第一個元素開始數,到其底層數組元素末尾的個數。

切片 s 的長度和容量可通過表達式 len(s)cap(s) 來獲取。

你可以通過重新切片來擴展一個切片,給它提供足夠的容量。試著修改示例程序中的切片操作,向外擴展它的容量,看看會發生什么。

package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s)

    // 截取切片使其長度為 0
    s = s[:0]
    printSlice(s)

    // 拓展其長度
    s = s[:4]
    printSlice(s)

    // 舍棄前兩個值
    s = s[2:]
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

nil 切片

切片的零值是 nil

nil 切片的長度和容量為 0 且沒有底層數組。

package main

import "fmt"

func main() {
    var s []int
    fmt.Println(s, len(s), cap(s))
    if s == nil {
        fmt.Println("nil!")
    }
}

用 make 創建切片

切片可以用內建函數 make 來創建,這也是你創建動態數組的方式。

make 函數會分配一個元素為零值的數組并返回一個引用了它的切片:

a := make([]int, 5)  // len(a)=5

要指定它的容量,需向 make 傳入第三個參數:

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:]      // len(b)=4, cap(b)=4
package main

import "fmt"

func main() {
    a := make([]int, 5)
    printSlice("a", a)

    b := make([]int, 0, 5)
    printSlice("b", b)

    c := b[:2]
    printSlice("c", c)

    d := c[2:5]
    printSlice("d", d)
}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
        s, len(x), cap(x), x)
}

切片的切片

切片可包含任何類型,甚至包括其它的切片。

package main

import (
    "fmt"
    "strings"
)

func main() {
    // 創建一個井字板(經典游戲)
    board := [][]string{
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
    }

    // 兩個玩家輪流打上 X 和 O
    board[0][0] = "X"
    board[2][2] = "O"
    board[1][2] = "X"
    board[1][0] = "O"
    board[0][2] = "X"

    for i := 0; i < len(board); i++ {
        fmt.Printf("%s\n", strings.Join(board[i], " "))
    }
}

向切片追加元素

為切片追加新的元素是種常用的操作,為此 Go 提供了內建的 append 函數。內建函數的文檔對此函數有詳細的介紹。

func append(s []T, vs ...T) []T

append 的第一個參數 s 是一個元素類型為 T 的切片,其余類型為 T 的值將會追加到該切片的末尾。

append 的結果是一個包含原切片所有元素加上新添加元素的切片。

s 的底層數組太小,不足以容納所有給定的值時,它就會分配一個更大的數組。返回的切片會指向這個新分配的數組。

(要了解關于切片的更多內容,請閱讀文章 Go 切片:用法和本質。)

package main

import "fmt"

func main() {
    var s []int
    printSlice(s)

    // 添加一個空切片
    s = append(s, 0)
    printSlice(s)

    // 這個切片會按需增長
    s = append(s, 1)
    printSlice(s)

    // 可以一次性添加多個元素
    s = append(s, 2, 3, 4)
    printSlice(s)
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

Range

for 循環的 range 形式可遍歷切片或映射。

當使用 for 循環遍歷切片時,每次迭代都會返回兩個值。第一個值為當前元素的下標,第二個值為該下標所對應元素的一份副本。

package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
    for i, v := range pow {
        fmt.Printf("2**%d = %d\n", i, v)
    }
}

可以將下標或值賦予 _ 來忽略它。

for i, _ := range pow
for _, value := range pow

若你只需要索引,忽略第二個變量即可。

for i := range pow
package main

import "fmt"

func main() {
    pow := make([]int, 10)
    for i := range pow {
        pow[i] = 1 << uint(i) // == 2**i
    }
    for _, value := range pow {
        fmt.Printf("%d\n", value)
    }
}

練習:切片

實現 Pic。它應當返回一個長度為 dy 的切片,其中每個元素是一個長度為 dx,元素類型為 uint8 的切片。當你運行此程序時,它會將每個整數解釋為灰度值(好吧,其實是藍度值)并顯示它所對應的圖像。

圖像的選擇由你來定。幾個有趣的函數包括 (x+y)/2, x*y, x^y, x*log(y)x%(y+1)

(提示:需要使用循環來分配 [][]uint8 中的每個 []uint8;請使用 uint8(intValue) 在類型之間轉換;你可能會用到 math 包中的函數。)

package main

import "golang.org/x/tour/pic"

func Pic(dx, dy int) [][]uint8 {
}

func main() {
    pic.Show(Pic)
}

映射

映射將鍵映射到值。

映射的零值為 nilnil 映射既沒有鍵,也不能添加鍵。

make 函數會返回給定類型的映射,并將其初始化備用。

package main

import "fmt"

type Vertex struct {
    Lat, Long float64
}

var m map[string]Vertex

func main() {
    m = make(map[string]Vertex)
    m["Bell Labs"] = Vertex{
        40.68433, -74.39967,
    }
    fmt.Println(m["Bell Labs"])
}

映射的文法

映射的文法與結構體相似,不過必須有鍵名。

package main

import "fmt"

type Vertex struct {
    Lat, Long float64
}

var m = map[string]Vertex{
    "Bell Labs": Vertex{
        40.68433, -74.39967,
    },
    "Google": Vertex{
        37.42202, -122.08408,
    },
}

func main() {
    fmt.Println(m)
}

若頂級類型只是一個類型名,你可以在文法的元素中省略它。

package main

import "fmt"

type Vertex struct {
    Lat, Long float64
}

var m = map[string]Vertex{
    "Bell Labs": {40.68433, -74.39967},
    "Google":    {37.42202, -122.08408},
}

func main() {
    fmt.Println(m)
}

修改映射

在映射 m 中插入或修改元素:

m[key] = elem

獲取元素:

elem = m[key]

刪除元素:

delete(m, key)

通過雙賦值檢測某個鍵是否存在:

elem, ok = m[key]

keym 中,oktrue ;否則,okfalse

key 不在映射中,那么 elem 是該映射元素類型的零值。

同樣的,當從映射中讀取某個不存在的鍵時,結果是映射的元素類型的零值。

:若 elemok 還未聲明,你可以使用短變量聲明:

elem, ok := m[key]
package main

import "fmt"

func main() {
    m := make(map[string]int)

    m["Answer"] = 42
    fmt.Println("The value:", m["Answer"])

    m["Answer"] = 48
    fmt.Println("The value:", m["Answer"])

    delete(m, "Answer")
    fmt.Println("The value:", m["Answer"])

    v, ok := m["Answer"]
    fmt.Println("The value:", v, "Present?", ok)
}

練習:映射

實現 WordCount。它應當返回一個映射,其中包含字符串 s 中每個“單詞”的個數。函數 wc.Test 會對此函數執行一系列測試用例,并輸出成功還是失敗。

你會發現 strings.Fields 很有幫助。

package main

import (
    "golang.org/x/tour/wc"
)

func WordCount(s string) map[string]int {
    return map[string]int{"x": 1}
}

func main() {
    wc.Test(WordCount)
}

函數值

函數也是值。它們可以像其它值一樣傳遞。

函數值可以用作函數的參數或返回值。

package main

import (
    "fmt"
    "math"
)

func compute(fn func(float64, float64) float64) float64 {
    return fn(3, 4)
}

func main() {
    hypot := func(x, y float64) float64 {
        return math.Sqrt(x*x + y*y)
    }
    fmt.Println(hypot(5, 12))

    fmt.Println(compute(hypot))
    fmt.Println(compute(math.Pow))
}

函數的閉包

Go 函數可以是一個閉包。閉包是一個函數值,它引用了其函數體之外的變量。該函數可以訪問并賦予其引用的變量的值,換句話說,該函數被這些變量“綁定”在一起。

例如,函數 adder 返回一個閉包。每個閉包都被綁定在其各自的 sum 變量上。

package main

import "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),
        )
    }
}

練習:斐波納契閉包

讓我們用函數做些好玩的事情。

實現一個 fibonacci 函數,它返回一個函數(閉包),該閉包返回一個斐波納契數列 (0, 1, 1, 2, 3, 5, ...)

package main

import "fmt"

// 返回一個“返回int的函數”
func fibonacci() func() int {
}

func main() {
    f := fibonacci()
    for i := 0; i < 10; i++ {
        fmt.Println(f())
    }
}

方法

Go 沒有類。不過你可以為結構體類型定義方法。

方法就是一類帶特殊的 接收者 參數的函數。

方法接收者在它自己的參數列表內,位于 func 關鍵字和方法名之間。

在此例中,Abs 方法擁有一個名為 v,類型為 Vertex 的接收者。

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
}

方法即函數

記住:方法只是個帶接收者參數的函數。

現在這個 Abs 的寫法就是個正常的函數,功能并沒有什么變化。

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func Abs(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(Abs(v))
}

你也可以為非結構體類型聲明方法。

在此例中,我們看到了一個帶 Abs 方法的數值類型 MyFloat

你只能為在同一包內定義的類型的接收者聲明方法,而不能為其它包內定義的類型(包括 int 之類的內建類型)的接收者聲明方法。

(譯注:就是接收者的類型定義和方法聲明必須在同一包內;不能為內建類型聲明方法。)

package main

import (
    "fmt"
    "math"
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f < 0 {
        return float64(-f)
    }
    return float64(f)
}

func main() {
    f := MyFloat(-math.Sqrt2)
    fmt.Println(f.Abs())
}

指針接收者

你可以為指針接收者聲明方法。

這意味著對于某類型 T,接收者的類型可以用 *T 的文法。(此外,T 不能是像 *int 這樣的指針。)

例如,這里為 *Vertex 定義了 Scale 方法。

指針接收者的方法可以修改接收者指向的值(就像 Scale 在這做的)。由于方法經常需要修改它的接收者,指針接收者比值接收者更常用。

試著移除第 16 行 Scale 函數聲明中的 *,觀察此程序的行為如何變化。

若使用值接收者,那么 Scale 方法會對原始 Vertex 值的副本進行操作。(對于函數的其它參數也是如此。)Scale 方法必須用指針接受者來更改 main 函數中聲明的 Vertex 的值。

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    v.Scale(10)
    fmt.Println(v.Abs())
}

指針與函數

現在我們要把 AbsScale 方法重寫為函數。

同樣,我們先試著移除掉第 16 的 *。你能看出為什么程序的行為改變了嗎?要怎樣做才能讓該示例順利通過編譯?

(若你不確定,繼續往下看。)

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func Abs(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func Scale(v *Vertex, f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    Scale(&v, 10)
    fmt.Println(Abs(v))
}

方法與指針重定向

比較前兩個程序,你大概會注意到帶指針參數的函數必須接受一個指針:

var v Vertex
ScaleFunc(v, 5)  // 編譯錯誤!
ScaleFunc(&v, 5) // OK

而以指針為接收者的方法被調用時,接收者既能為值又能為指針:

var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK

對于語句 v.Scale(5),即便 v 是個值而非指針,帶指針接收者的方法也能被直接調用。 也就是說,由于 Scale 方法有一個指針接收者,為方便起見,Go 會將語句 v.Scale(5) 解釋為 (&v).Scale(5)

package main

import "fmt"

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func ScaleFunc(v *Vertex, f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    v.Scale(2)
    ScaleFunc(&v, 10)

    p := &Vertex{4, 3}
    p.Scale(3)
    ScaleFunc(p, 8)

    fmt.Println(v, p)
}

同樣的事情也發生在相反的方向。

接受一個值作為參數的函數必須接受一個指定類型的值:

var v Vertex
fmt.Println(AbsFunc(v))  // OK
fmt.Println(AbsFunc(&v)) // 編譯錯誤!

而以值為接收者的方法被調用時,接收者既能為值又能為指針:

var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK

這種情況下,方法調用 p.Abs() 會被解釋為 (*p).Abs()

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func AbsFunc(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
    fmt.Println(AbsFunc(v))

    p := &Vertex{4, 3}
    fmt.Println(p.Abs())
    fmt.Println(AbsFunc(*p))
}

選擇值或指針作為接收者

使用指針接收者的原因有二:

首先,方法能夠修改其接收者指向的值。

其次,這樣可以避免在每次調用方法時復制該值。若值的類型為大型結構體時,這樣做會更加高效。

在本例中,ScaleAbs 接收者的類型為 *Vertex,即便 Abs 并不需要修改其接收者。

通常來說,所有給定類型的方法都應該有值或指針接收者,但并不應該二者混用。(我們會在接下來幾頁中明白為什么。)

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := &Vertex{3, 4}
    fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
    v.Scale(5)
    fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}

接口

接口類型 是由一組方法簽名定義的集合。

接口類型的變量可以保存任何實現了這些方法的值。

注意: 示例代碼的 22 行存在一個錯誤。由于 Abs 方法只為 *Vertex (指針類型)定義,因此 Vertex(值類型)并未實現 Abser

package main

import (
    "fmt"
    "math"
)

type Abser interface {
    Abs() float64
}

func main() {
    var a Abser
    f := MyFloat(-math.Sqrt2)
    v := Vertex{3, 4}

    a = f  // a MyFloat 實現了 Abser
    a = &v // a *Vertex 實現了 Abser

    // 下面一行,v 是一個 Vertex(而不是 *Vertex)
    // 所以沒有實現 Abser。
    a = v

    fmt.Println(a.Abs())
}

type MyFloat float64

func (f MyFloat) Abs() float64 {
    if f < 0 {
        return float64(-f)
    }
    return float64(f)
}

type Vertex struct {
    X, Y float64
}

func (v *Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

接口與隱式實現

類型通過實現一個接口的所有方法來實現該接口。既然無需專門顯式聲明,也就沒有“implements”關鍵字。

隱式接口從接口的實現中解耦了定義,這樣接口的實現可以出現在任何包中,無需提前準備。

因此,也就無需在每一個實現上增加新的接口名稱,這樣同時也鼓勵了明確的接口定義。

package main

import "fmt"

type I interface {
    M()
}

type T struct {
    S string
}

// 此方法表示類型 T 實現了接口 I,但我們無需顯式聲明此事。
func (t T) M() {
    fmt.Println(t.S)
}

func main() {
    var i I = T{"hello"}
    i.M()
}

接口值

接口也是值。它們可以像其它值一樣傳遞。

接口值可以用作函數的參數或返回值。

在內部,接口值可以看做包含值和具體類型的元組:

(value, type)

接口值保存了一個具體底層類型的具體值。

接口值調用方法時會執行其底層類型的同名方法。

package main

import (
    "fmt"
    "math"
)

type I interface {
    M()
}

type T struct {
    S string
}

func (t *T) M() {
    fmt.Println(t.S)
}

type F float64

func (f F) M() {
    fmt.Println(f)
}

func main() {
    var i I

    i = &T{"Hello"}
    describe(i)
    i.M()

    i = F(math.Pi)
    describe(i)
    i.M()
}

func describe(i I) {
    fmt.Printf("(%v, %T)\n", i, i)
}

底層值為 nil 的接口值

即便接口內的具體值為 nil,方法仍然會被 nil 接收者調用。

在一些語言中,這會觸發一個空指針異常,但在 Go 中通常會寫一些方法來優雅地處理它(如本例中的 M 方法)。

注意: 保存了 nil 具體值的接口其自身并不為 nil。

package main

import "fmt"

type I interface {
    M()
}

type T struct {
    S string
}

func (t *T) M() {
    if t == nil {
        fmt.Println("<nil>")
        return
    }
    fmt.Println(t.S)
}

func main() {
    var i I

    var t *T
    i = t
    describe(i)
    i.M()

    i = &T{"hello"}
    describe(i)
    i.M()
}

func describe(i I) {
    fmt.Printf("(%v, %T)\n", i, i)
}

nil 接口值

nil 接口值既不保存值也不保存具體類型。

為 nil 接口調用方法會產生運行時錯誤,因為接口的元組內并未包含能夠指明該調用哪個 具體 方法的類型。

package main

import "fmt"

type I interface {
    M()
}

func main() {
    var i I
    describe(i)
    i.M()
}

func describe(i I) {
    fmt.Printf("(%v, %T)\n", i, i)
}

空接口

指定了零個方法的接口值被稱為 空接口:

interface{}

空接口可保存任何類型的值。(因為每個類型都至少實現了零個方法。)

空接口被用來處理未知類型的值。例如,fmt.Print 可接受類型為 interface{} 的任意數量的參數。

package main

import "fmt"

func main() {
    var i interface{}
    describe(i)

    i = 42
    describe(i)

    i = "hello"
    describe(i)
}

func describe(i interface{}) {
    fmt.Printf("(%v, %T)\n", i, i)
}

類型斷言

類型斷言 提供了訪問接口值底層具體值的方式。

t := i.(T)

該語句斷言接口值 i 保存了具體類型 T,并將其底層類型為 T 的值賦予變量 t

i 并未保存 T 類型的值,該語句就會觸發一個恐慌。

為了 判斷 一個接口值是否保存了一個特定的類型,類型斷言可返回兩個值:其底層值以及一個報告斷言是否成功的布爾值。

t, ok := i.(T)

i 保存了一個 T,那么 t 將會是其底層值,而 oktrue

否則,ok 將為 falset 將為 T 類型的零值,程序并不會產生恐慌。

請注意這種語法和讀取一個映射時的相同之處。

package main

import "fmt"

func main() {
    var i interface{} = "hello"

    s := i.(string)
    fmt.Println(s)

    s, ok := i.(string)
    fmt.Println(s, ok)

    f, ok := i.(float64)
    fmt.Println(f, ok)

    f = i.(float64) // 報錯(panic)
    fmt.Println(f)
}

類型選擇

類型選擇 是一種按順序從幾個類型斷言中選擇分支的結構。

類型選擇與一般的 switch 語句相似,不過類型選擇中的 case 為類型(而非值), 它們針對給定接口值所存儲的值的類型進行比較。

switch v := i.(type) {
case T:
    // v 的類型為 T
case S:
    // v 的類型為 S
default:
    // 沒有匹配,v 與 i 的類型相同
}

類型選擇中的聲明與類型斷言 i.(T) 的語法相同,只是具體類型 T 被替換成了關鍵字 type

此選擇語句判斷接口值 i 保存的值類型是 T 還是 S。在 TS 的情況下,變量 v 會分別按 TS 類型保存 i 擁有的值。在默認(即沒有匹配)的情況下,變量 vi 的接口類型和值相同。

package main

import "fmt"

func do(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Twice %v is %v\n", v, v*2)
    case string:
        fmt.Printf("%q is %v bytes long\n", v, len(v))
    default:
        fmt.Printf("I don't know about type %T!\n", v)
    }
}

func main() {
    do(21)
    do("hello")
    do(true)
}

Stringer

fmt 包中定義的 Stringer 是最普遍的接口之一。

type Stringer interface {
    String() string
}

Stringer 是一個可以用字符串描述自己的類型。fmt 包(還有很多包)都通過此接口來打印值。

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
    a := Person{"Arthur Dent", 42}
    z := Person{"Zaphod Beeblebrox", 9001}
    fmt.Println(a, z)
}

練習:Stringer

通過讓 IPAddr 類型實現 fmt.Stringer 來打印點號分隔的地址。

例如,IPAddr{1, 2, 3, 4} 應當打印為 "1.2.3.4"

package main

import "fmt"

type IPAddr [4]byte

// TODO: 給 IPAddr 添加一個 "String() string" 方法

func main() {
    hosts := map[string]IPAddr{
        "loopback":  {127, 0, 0, 1},
        "googleDNS": {8, 8, 8, 8},
    }
    for name, ip := range hosts {
        fmt.Printf("%v: %v\n", name, ip)
    }
}

錯誤

Go 程序使用 error 值來表示錯誤狀態。

fmt.Stringer 類似,error 類型是一個內建接口:

type error interface {
    Error() string
}

(與 fmt.Stringer 類似,fmt 包在打印值時也會滿足 error。)

通常函數會返回一個 error 值,調用的它的代碼應當判斷這個錯誤是否等于 nil 來進行錯誤處理。

i, err := strconv.Atoi("42")
if err != nil {
    fmt.Printf("couldn't convert number: %v\n", err)
    return
}
fmt.Println("Converted integer:", i)

error 為 nil 時表示成功;非 nil 的 error 表示失敗。

package main

import (
    "fmt"
    "time"
)

type MyError struct {
    When time.Time
    What string
}

func (e *MyError) Error() string {
    return fmt.Sprintf("at %v, %s",
        e.When, e.What)
}

func run() error {
    return &MyError{
        time.Now(),
        "it didn't work",
    }
}

func main() {
    if err := run(); err != nil {
        fmt.Println(err)
    }
}

練習:錯誤

之前的練習中復制 Sqrt 函數,修改它使其返回 error 值。

Sqrt 接受到一個負數時,應當返回一個非 nil 的錯誤值。復數同樣也不被支持。

創建一個新的類型

type ErrNegativeSqrt float64

并為其實現

func (e ErrNegativeSqrt) Error() string

方法使其擁有 error 值,通過 ErrNegativeSqrt(-2).Error() 調用該方法應返回 "cannot Sqrt negative number: -2"

注意:Error 方法內調用 fmt.Sprint(e) 會讓程序陷入死循環。可以通過先轉換 e 來避免這個問題:fmt.Sprint(float64(e))。這是為什么呢?

修改 Sqrt 函數,使其接受一個負數時,返回 ErrNegativeSqrt 值。

package main

import (
    "fmt"
)

func Sqrt(x float64) (float64, error) {
    return 0, nil
}

func main() {
    fmt.Println(Sqrt(2))
    fmt.Println(Sqrt(-2))
}

Reader

io 包指定了 io.Reader 接口,它表示從數據流的末尾進行讀取。

Go 標準庫包含了該接口的許多實現,包括文件、網絡連接、壓縮和加密等等。

io.Reader 接口有一個 Read 方法:

func (T) Read(b []byte) (n int, err error)

Read 用數據填充給定的字節切片并返回填充的字節數和錯誤值。在遇到數據流的結尾時,它會返回一個 io.EOF 錯誤。

示例代碼創建了一個 strings.Reader 并以每次 8 字節的速度讀取它的輸出。

package main

import (
    "fmt"
    "io"
    "strings"
)

func main() {
    r := strings.NewReader("Hello, Reader!")

    b := make([]byte, 8)
    for {
        n, err := r.Read(b)
        fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
        fmt.Printf("b[:n] = %q\n", b[:n])
        if err == io.EOF {
            break
        }
    }
}

練習:Reader

實現一個 Reader 類型,它產生一個 ASCII 字符 'A' 的無限流。

package main

import "golang.org/x/tour/reader"

type MyReader struct{}

// TODO: 給 MyReader 添加一個 Read([]byte) (int, error) 方法

func main() {
    reader.Validate(MyReader{})
}

練習:rot13Reader

有種常見的模式是一個 io.Reader 包裝另一個 io.Reader,然后通過某種方式修改其數據流。

例如,gzip.NewReader 函數接受一個 io.Reader(已壓縮的數據流)并返回一個同樣實現了 io.Reader*gzip.Reader(解壓后的數據流)。

編寫一個實現了 io.Reader 并從另一個 io.Reader 中讀取數據的 rot13Reader,通過應用 rot13 代換密碼對數據流進行修改。

rot13Reader 類型已經提供。實現 Read 方法以滿足 io.Reader

package main

import (
    "io"
    "os"
    "strings"
)

type rot13Reader struct {
    r io.Reader
}

func main() {
    s := strings.NewReader("Lbh penpxrq gur pbqr!")
    r := rot13Reader{s}
    io.Copy(os.Stdout, &r)
}

圖像

image 包定義了 Image 接口:

package image

type Image interface {
    ColorModel() color.Model
    Bounds() Rectangle
    At(x, y int) color.Color
}

注意: Bounds 方法的返回值 Rectangle 實際上是一個 image.Rectangle,它在 image 包中聲明。

(請參閱文檔了解全部信息。)

color.Colorcolor.Model 類型也是接口,但是通常因為直接使用預定義的實現 image.RGBAimage.RGBAModel 而被忽視了。這些接口和類型由 image/color 包定義。

package main

import (
    "fmt"
    "image"
)

func main() {
    m := image.NewRGBA(image.Rect(0, 0, 100, 100))
    fmt.Println(m.Bounds())
    fmt.Println(m.At(0, 0).RGBA())
}

練習:圖像

還記得之前編寫的圖片生成器 嗎?我們再來編寫另外一個,不過這次它將會返回一個 image.Image 的實現而非一個數據切片。

定義你自己的 Image 類型,實現必要的方法并調用 pic.ShowImage

Bounds 應當返回一個 image.Rectangle ,例如 image.Rect(0, 0, w, h)

ColorModel 應當返回 color.RGBAModel

At 應當返回一個顏色。上一個圖片生成器的值 v 對應于此次的 color.RGBA{v, v, 255, 255}

package main

import "golang.org/x/tour/pic"

type Image struct{}

func main() {
    m := Image{}
    pic.ShowImage(m)
}

Go 程

Go 程(goroutine)是由 Go 運行時管理的輕量級線程。

go f(x, y, z)

會啟動一個新的 Go 程并執行

f(x, y, z)

f, x, yz 的求值發生在當前的 Go 程中,而 f 的執行發生在新的 Go 程中。

Go 程在相同的地址空間中運行,因此在訪問共享的內存時必須進行同步。sync 包提供了這種能力,不過在 Go 中并不經常用到,因為還有其它的辦法(見下一頁)。

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

信道

信道是帶有類型的管道,你可以通過它用信道操作符 <- 來發送或者接收值。

ch <- v    // 將 v 發送至信道 ch。
v := <-ch  // 從 ch 接收值并賦予 v。

(“箭頭”就是數據流的方向。)

和映射與切片一樣,信道在使用前必須創建:

ch := make(chan int)

默認情況下,發送和接收操作在另一端準備好之前都會阻塞。這使得 Go 程可以在沒有顯式的鎖或競態變量的情況下進行同步。

以下示例對切片中的數進行求和,將任務分配給兩個 Go 程。一旦兩個 Go 程完成了它們的計算,它就能算出最終的結果。

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // 將和送入 c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // 從 c 中接收

    fmt.Println(x, y, x+y)
}

帶緩沖的信道

信道可以是 帶緩沖的。將緩沖長度作為第二個參數提供給 make 來初始化一個帶緩沖的信道:

ch := make(chan int, 100)

僅當信道的緩沖區填滿后,向其發送數據時才會阻塞。當緩沖區為空時,接受方會阻塞。

修改示例填滿緩沖區,然后看看會發生什么。

package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

range 和 close

發送者可通過 close 關閉一個信道來表示沒有需要發送的值了。接收者可以通過為接收表達式分配第二個參數來測試信道是否被關閉:若沒有值可以接收且信道已被關閉,那么在執行完

v, ok := <-ch

之后 ok 會被設置為 false

循環 for i := range c 會不斷從信道接收值,直到它被關閉。

注意: 只有發送者才能關閉信道,而接收者不能。向一個已經關閉的信道發送數據會引發程序恐慌(panic)。

還要注意: 信道與文件不同,通常情況下無需關閉它們。只有在必須告訴接收者不再有需要發送的值時才有必要關閉,例如終止一個 range 循環。

package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    for i := range c {
        fmt.Println(i)
    }
}

select 語句

select 語句使一個 Go 程可以等待多個通信操作。

select 會阻塞到某個分支可以繼續執行為止,這時就會執行該分支。當多個分支都準備好時會隨機選擇一個執行。

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

默認選擇

select 中的其它分支都沒有準備好時,default 分支就會執行。

為了在嘗試發送或者接收時不發生阻塞,可使用 default 分支:

select {
case i := <-c:
    // 使用 i
default:
    // 從 c 中接收會阻塞時執行
}
package main

import (
    "fmt"
    "time"
)

func main() {
    tick := time.Tick(100 * time.Millisecond)
    boom := time.After(500 * time.Millisecond)
    for {
        select {
        case <-tick:
            fmt.Println("tick.")
        case <-boom:
            fmt.Println("BOOM!")
            return
        default:
            fmt.Println("    .")
            time.Sleep(50 * time.Millisecond)
        }
    }
}

練習:等價二叉查找樹

不同二叉樹的葉節點上可以保存相同的值序列。例如,以下兩個二叉樹都保存了序列 1,1,2,3,5,8,13

在大多數語言中,檢查兩個二叉樹是否保存了相同序列的函數都相當復雜。 我們將使用 Go 的并發和信道來編寫一個簡單的解法。

本例使用了 tree 包,它定義了類型:

type Tree struct {
    Left  *Tree
    Value int
    Right *Tree
}

1. 實現 Walk 函數。

2. 測試 Walk 函數。

函數 tree.New(k) 用于構造一個隨機結構的已排序二叉查找樹,它保存了值 k, 2k, 3k, ..., 10k

創建一個新的信道 ch 并且對其進行步進:

go Walk(tree.New(1), ch)

然后從信道中讀取并打印 10 個值。應當是數字 1, 2, 3, ..., 10

3.Walk 實現 Same 函數來檢測 t1t2 是否存儲了相同的值。

4. 測試 Same 函數。

Same(tree.New(1), tree.New(1)) 應當返回 true,而 Same(tree.New(1), tree.New(2)) 應當返回 false

Tree 的文檔可在這里找到。

package main

import "golang.org/x/tour/tree"

// Walk 步進 tree t 將所有的值從 tree 發送到 channel ch。
func Walk(t *tree.Tree, ch chan int)

// Same 檢測樹 t1 和 t2 是否含有相同的值。
func Same(t1, t2 *tree.Tree) bool

func main() {
}

sync.Mutex

我們已經看到信道非常適合在各個 Go 程間進行通信。

但是如果我們并不需要通信呢?比如說,若我們只是想保證每次只有一個 Go 程能夠訪問一個共享的變量,從而避免沖突?

這里涉及的概念叫做 互斥(mutualexclusion)* ,我們通常使用 互斥鎖(Mutex) 這一數據結構來提供這種機制。

Go 標準庫中提供了 sync.Mutex 互斥鎖類型及其兩個方法:

  • Lock
  • Unlock

我們可以通過在代碼前調用 Lock 方法,在代碼后調用 Unlock 方法來保證一段代碼的互斥執行。參見 Inc 方法。

我們也可以用 defer 語句來保證互斥鎖一定會被解鎖。參見 Value 方法。

package main

import (
    "fmt"
    "sync"
    "time"
)

// SafeCounter 的并發使用是安全的。
type SafeCounter struct {
    v   map[string]int
    mux sync.Mutex
}

// Inc 增加給定 key 的計數器的值。
func (c *SafeCounter) Inc(key string) {
    c.mux.Lock()
    // Lock 之后同一時刻只有一個 goroutine 能訪問 c.v
    c.v[key]++
    c.mux.Unlock()
}

// Value 返回給定 key 的計數器的當前值。
func (c *SafeCounter) Value(key string) int {
    c.mux.Lock()
    // Lock 之后同一時刻只有一個 goroutine 能訪問 c.v
    defer c.mux.Unlock()
    return c.v[key]
}

func main() {
    c := SafeCounter{v: make(map[string]int)}
    for i := 0; i < 1000; i++ {
        go c.Inc("somekey")
    }

    time.Sleep(time.Second)
    fmt.Println(c.Value("somekey"))
}

練習:Web 爬蟲

在這個練習中,我們將會使用 Go 的并發特性來并行化一個 Web 爬蟲。

修改 Crawl 函數來并行地抓取 URL,并且保證不重復。

提示:你可以用一個 map 來緩存已經獲取的 URL,但是要注意 map 本身并不是并發安全的!

package main

import (
    "fmt"
)

type Fetcher interface {
    // Fetch 返回 URL 的 body 內容,并且將在這個頁面上找到的 URL 放到一個 slice 中。
    Fetch(url string) (body string, urls []string, err error)
}

// Crawl 使用 fetcher 從某個 URL 開始遞歸的爬取頁面,直到達到最大深度。
func Crawl(url string, depth int, fetcher Fetcher) {
    // TODO: 并行的抓取 URL。
    // TODO: 不重復抓取頁面。
        // 下面并沒有實現上面兩種情況:
    if depth <= 0 {
        return
    }
    body, urls, err := fetcher.Fetch(url)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Printf("found: %s %q\n", url, body)
    for _, u := range urls {
        Crawl(u, depth-1, fetcher)
    }
    return
}

func main() {
    Crawl("https://golang.org/", 4, fetcher)
}

// fakeFetcher 是返回若干結果的 Fetcher。
type fakeFetcher map[string]*fakeResult

type fakeResult struct {
    body string
    urls []string
}

func (f fakeFetcher) Fetch(url string) (string, []string, error) {
    if res, ok := f[url]; ok {
        return res.body, res.urls, nil
    }
    return "", nil, fmt.Errorf("not found: %s", url)
}

// fetcher 是填充后的 fakeFetcher。
var fetcher = fakeFetcher{
    "https://golang.org/": &fakeResult{
        "The Go Programming Language",
        []string{
            "https://golang.org/pkg/",
            "https://golang.org/cmd/",
        },
    },
    "https://golang.org/pkg/": &fakeResult{
        "Packages",
        []string{
            "https://golang.org/",
            "https://golang.org/cmd/",
            "https://golang.org/pkg/fmt/",
            "https://golang.org/pkg/os/",
        },
    },
    "https://golang.org/pkg/fmt/": &fakeResult{
        "Package fmt",
        []string{
            "https://golang.org/",
            "https://golang.org/pkg/",
        },
    },
    "https://golang.org/pkg/os/": &fakeResult{
        "Package os",
        []string{
            "https://golang.org/",
            "https://golang.org/pkg/",
        },
    },
}

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • 1.安裝 https://studygolang.com/dl 2.使用vscode編輯器安裝go插件 3.go語...
    go含羞草閱讀 1,565評論 0 6
  • 官方網站:https://golang.org/標準庫文檔:https://golang.org/pkg/在線編碼...
    技術學習閱讀 2,329評論 2 39
  • Lua 5.1 參考手冊 by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 13,870評論 0 38
  • 第3章 基本概念 3.1 語法 3.2 關鍵字和保留字 3.3 變量 3.4 數據類型 5種簡單數據類型:Unde...
    RickCole閱讀 5,146評論 0 21
  • 第一個品飲期:剛壓制好兩三個月以后,這個時候的熟茶肯定是不好喝的,堆味重,性燥熱,高溫蒸壓之后的水味還在,但也有熟...
    付小然閱讀 202評論 0 0