Go語言入門


在學習Go語言程式設計之前,我們需要安裝和組態好Go語言的開發環境。可以選擇線上的編譯器:http://tour.golang.org/welcome/1 來直接執行程式碼。也可以在您自己的計算機上安裝開發編譯環境。

Go本地環境設定

如果您願意在本地環境安裝和組態Go程式設計語言,則需要在計算機上提供以下兩個軟體:

  • 文字編輯器
  • Go編譯器

文字編輯器

這是用於編寫您的程式程式碼。常見的幾個編輯器包括Windows記事本,OS編輯命令,BriefEpsilonEMACSvim(或vi)。

文字編輯器的名稱和版本可能因不同的作業系統而異。例如,記事本只能在Windows上使用,vim(或vi)可以在Windows以及Linux或UNIX上使用。

使用編輯器建立的檔案稱為原始檔,原始檔中包含程式的原始碼。Go程式的原始檔通常使用擴充套件名「.go」來命名。

在開始程式設計之前,確保您安裝好並熟練使用一個文字編輯器,並且有足夠的經驗來編寫計算機程式程式碼,將程式碼儲存在檔案中,編譯並最終執行它。

Go編譯器

在原始檔中編寫的原始碼是人類可讀的源程式。 它需要「編譯」變成機器語言,以便CPU可以根據給出的指令實際執行程式。

這個Go程式設計語言編譯器用於將原始碼編譯成可執行程式。這裡假設您知道或了解程式設計語言編譯器的基本知識。

Go發行版本是FreeBSD(版本8及更高版本),Linux,Mac OS X(Snow Leopard及更高版本)和具有32位(386)和64位(amd64)x86處理器架構的Windows作業系統的二進位制安裝版本 。

以下部分將演示如何在各種作業系統上安裝Go語言環境的二進位制分發包。

下載Go存檔檔案

從連結【Go下載】中下載最新版本的Go可安裝的歸檔檔案。在寫本教學的時候,選擇的是go1.7.4.windows-amd64.msi並將下載到桌面上。

註:寫本教學的時,使用的電腦是:Windows 10 64bit 系統

如果作業系統不一樣,可選擇對應版本下載安裝。

作業系統 存檔名稱
Windows go1.7.windows-amd64.msi
Linux go1.7.linux-amd64.tar.gz
Mac go1.7.4.darwin-amd64.pkg
FreeBSD go1.7.freebsd-amd64.tar.gz

在UNIX/Linux/Mac OS X和FreeBSD上安裝

將下載歸檔檔案解壓縮到/usr/local目錄中,在/usr/local/go目錄建立一個Go樹。 例如:

tar -C /usr/local -xzf go1.7.4.linux-amd64.tar.gz

/usr/local/go/bin新增到PATH環境變數。

作業系統 輸出
Linux export PATH=$PATH:/usr/local/go/bin
Mac export PATH=$PATH:/usr/local/go/bin
FreeBSD export PATH=$PATH:/usr/local/go/bin

在Windows上安裝

使用MSI檔案並按照提示安裝Go工具。 預設情況下,安裝程式使用C:\Go目錄。安裝程式應該在視窗的PATH環境變數中設定C:\Go\bin目錄。重新啟動後,開啟的命令提示驗證更改是否生效。

驗證安裝結果

F:\worksp\golang中建立一個test.go的go檔案。編寫並儲存以下程式碼到 test.go 檔案中。

package main

import "fmt"

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

現在執行test.go檢視結果並驗證輸出結果如下:

F:\worksp\golang>go run test.go
Hello, World!

包的使用

每個 Go 程式都是由包組成的。
程式執行的入口是包 main
這個程式使用並匯入了包 「fmt「 和 「math/rand「 。
按照慣例,包名與匯入路徑的最後一個目錄一致。例如,」math/rand「 包由 package rand 語句開始。

注意:這個程式的執行環境是確定性的,因此 rand.Intn 每次都會返回相同的數位。

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.", math.Sqrt(7))
}

匯出名稱

Go 中,首字母大寫的名稱是被匯出的。

在匯入包之後,只能存取包所匯出的名字,任何未匯出的名字是不能被包外的程式碼存取的。

FooFOO 都是被匯出的名稱。名稱 foo 是不會被匯出的。

執行程式碼,注意編譯器報的錯誤。

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

package main

import (
    "fmt"
    "math"
)

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

函式

函式可以沒有引數或接受多個引數。
在這個例子中, add 接受兩個 int 型別的引數。
注意型別在變數名之後 。

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

函式多值返回

函式可以返回任意數量的返回值。
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 定義。
函式外的每個語句都必須以關鍵字開始( varfunc 、等等), :=結構不能使用在函式外。

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

這個例子演示了具有不同型別的變數。 同時與匯入語句一樣,變數的定義「打包」在一個語法塊中。

intuintuintptr 型別在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() {
    const f = "%T(%v)\n"
    fmt.Printf(f, ToBe, ToBe)
    fmt.Printf(f, MaxInt, MaxInt)
    fmt.Printf(f, 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 的在不同型別之間的專案賦值時需要顯式轉換。 試著移除例子中 float64int 的轉換看看會發生什麼。

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

但是當右邊包含了未指名型別的數位常數時,新的變數就可能是 intfloat64complex128 。 這取決於常數的精度:

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

嘗試修改演示程式碼中 v 的初始值,並觀察這是如何影響其型別的。

package main

import "fmt"

func main() {
    v := 42 // change me!
    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位的整數,根據平台不同有時會更少。)

package main

import "fmt"

const (
    Big   = 1 << 100
    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 等其他語言,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

就像 for 迴圈一樣,Go 的 if 語句也不要求用( ) 將條件括起來,同時,{ }還是必須有的。

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 塊中使用。
(提示:兩個 pow 呼叫都在 main 呼叫 fmt.Println 前執行完畢了。)

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

switch語句

你可能已經知道 switch 語句會長什麼樣了。
除非以 fallthrough 語句結束,否則分支會自動終止。

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

switch 的執行順序

switch 的條件從上到下的執行,當匹配成功的時候停止。

(例如,

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

i==0 時不會呼叫 f 。)

注意:Go playground 中的時間總是從 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

沒有條件的 switchswitch 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 棧

延遲的函式呼叫被壓入一個棧中。當函式返回時, 會按照後進先出的順序呼叫被延遲的函式呼叫。

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         // point to i
    fmt.Println(*p) // read i through the pointer
    *p = 21         // set i through the pointer
    fmt.Println(i)  // see the new value of i

    p = &j         // point to j
    *p = *p / 37   // divide j through the pointer
    fmt.Println(j) // see the new value of j
}

結構體

一個結構體( struct )就是一個欄位的集合。(而 type 的含義跟其字面意思相符。)

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)
}

結構體指標

結構體欄位可以通過結構體指標來存取。
通過指標間接的存取是透明的。

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 是一個有 n 個型別為 T 的值的陣列。

表示式

var a [10]int

定義變數 a 是一個有十個整數的陣列。

陣列的長度是其型別的一部分,因此陣列不能改變大小。這看起來是一個制約,但是請不要擔心; Go 提供了更加便利的方式來使用陣列。

切片(slice)

一個 slice 會指向一個序列的值,並且包含了長度資訊。

[]T 是一個元素型別為 T 的 切片(slice)。

len(s)返回 slice s的長度。

package main

import "fmt"

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

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

切片(slice)的切片

切片(slice)可以包含任意的型別,包括另一個 slice

package main

import (
    "fmt"
    "strings"
)

func main() {
    // Create a tic-tac-toe board.
    game := [][]string{
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
    }

    // The players take turns.
    game[0][0] = "X"
    game[2][2] = "O"
    game[2][0] = "X"
    game[1][0] = "O"
    game[0][2] = "X"

    printBoard(game)
}

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

對 slice 切片

slice 可以重新切片,建立一個新的 slice 值指向相同的陣列。

表示式

s[lo:hi]

表示從 lohi-1slice 元素,含前端,不包含後端。因此

s[lo:lo]

是空的,而

s[lo:lo+1]

有一個元素。

package main

import "fmt"

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

    // 省略下標代表從 0 開始
    fmt.Println("s[:3] ==", s[:3])

    // 省略上標代表到 len(s) 結束
    fmt.Println("s[4:] ==", s[4:])
}

構造 slice

slice 由函式 make 建立。這會分配一個全是零值的陣列並且返回一個 slice 指向這個陣列:

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)
}

nil slice

slice 的零值是 nil

一個 nilslice 的長度和容量是 0

package main

import "fmt"

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

向 slice 新增元素

slice 的末尾新增元素是一種常見的操作,因此 Go 提供了一個內建函式 append 。 內建函式的文件對 append 有詳細介紹。

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

append 的第一個引數 s 是一個元素型別為 Tslice ,其餘型別為 T 的值將會附加到該 slice 的末尾。

append 的結果是一個包含原 slice 所有元素加上新新增的元素的 slice

如果 s 的底層陣列太小,而不能容納所有值時,會分配一個更大的陣列。 返回的 slice 會指向這個新分配的陣列。

package main

import "fmt"

func main() {
    var a []int
    printSlice("a", a)

    // append works on nil slices.
    a = append(a, 0)
    printSlice("a", a)

    // the slice grows as needed.
    a = append(a, 1)
    printSlice("a", a)

    // we can add more than one element at a time.
    a = append(a, 2, 3, 4)
    printSlice("a", a)
}

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

範圍(range)

for 迴圈的 range 格式可以對 slice 或者 map 進行疊代迴圈。

當使用 for 迴圈遍歷一個 slice 時,每次疊代 range 將返回兩個值。 第一個是當前下標(序號),第二個是該下標所對應元素的一個拷貝。

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)
    }
}

可以通過賦值給 _ 來忽略序號和值。

如果只需要索引值,去掉 「 , value 」 的部分即可。

package main

import "fmt"

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