第一章 - 基本概念

Go 是需要編譯、靜態型別、具有類似於 C 的語法與擁有 Garbage Collection 等特性的語言。 這是什麼意思?

編譯

編譯是將高階程式碼轉換為低階程式碼的過程。例如:在 Go 來說就會是組合語言,或是其他的中介語言(比如說 Java 和 C#)

你可能覺得編譯語言讓你感到不愉快,因為編譯速度可能相對慢。如果你需要等待數分鐘甚至數小時來編譯你的程式碼,那要快速迭代是相當困難的。編譯速度是 Go 語言在設計上的主要考量。這對於過去使用直譯式語言並且得利於快速開發週期的人來說,是相當好的消息。

編譯語言的執行速度較快,而且執行時不需要額外的相依套件(至少,這對於 C、C++ 和 Go 這類編譯成組合語言的程式語言來說是這樣的)。

靜態型別

靜態型別意味著變數必須是特定類型(int、string、bool、[]byte 等)。 這可以透過在宣告變數時指定類型來實現,或者在許多情況下,讓編譯器來幫助你推斷類型 (我們稍後將看看範例)。關於靜態型別有很多可以提的,但我相信透過閱讀程式碼你會有更好的理解。如果你習慣於動態類型的語言,你可能會發現這很麻煩。沒有錯,但這是有好處的。使用靜態型別系統,編譯器除了能夠檢查語法錯誤外,並能進一步進行優化。

類似於 C 的語法

如果一個程式語言的語法類似於 C,並且你曾經使用過其他類似語法的語言, 例如:C、C++、Java、Javascript 和 C#,那你會發現 Go 至少表面上看起來很類似。比如說,這代表了 && 是 boolean 的 AND、== 是用來比較是否相等、{} 是一個宣告的範圍,以及陣列的 index 從 0 開始。

類似於 C 的語法也代表了,分號指的是一行的結束,以及用括號來包住條件。但在 Go 語言中省去了這兩個部分, 儘管大括號還是用在控制範圍。舉例來說,一個 if 條件式會長的像:

if name == "Leto" {
  print("the spice must flow")
}

一個更複雜一點的例子中,括號依舊是有用的:

if (name == "Goku" && power > 9000) || (name == "gohan" && power < 4000)  {
  print("super Saiyan")
}

除此之外,Go 比 C# 或 Java 更接近 C - 不僅在語法上,更在於其目的。當你去學習 Go 語言後,你會發現它的簡潔和簡單。

Garbage Collected

一些變數在建立時,有一個容易定義的生命週期。例如,函式的區域變數在函式結束時消失。在其他情況下,這不是那麼顯著 - 至少對於編譯器來說。例如,由函式返回或由其他變數和物件引用的變數生命週期可能難以確定。沒有 garbage collection 機制,開發人員需要在不需要變數的時候釋放與這些變數相關的記憶體。怎麼做?在 C,你可以使用 free(str);。具有 garbage collection(例如:Ruby、Python、Java、JavaScript、C#、Go)機制的語言能夠追蹤變數,並在不需要使用時釋放它們。垃圾收集增加了負擔,但它也解決了一些會造成毀滅性的 bug 的問題。

執行 Go 程式碼

讓我們開始我們的旅程吧!建立一個簡單的程式,學習如何編譯和執行它。打開你喜歡的編輯器,並撰寫以下程式碼:

package main

func main() {
  println("it's over 9000!")
}

將檔案另存為 main.go。現在,你可以儲存在任何你想要的地方。我們不需要把這個小範例放到工作目錄中。

接下來,打開 shell/command prompt,並將目錄切換到檔案的位置。對我來說,那是在 cd ~/code

最後,執行程式碼:

go run main.go

如果一切正確,你會看到 it's over 9000!

但等等,編譯的步驟呢? go run 是一個同時進行編譯和執行程式碼的指令。它使用一個臨時目錄來建置程式、執行它、最後砍掉自己。你可以透過以下指令查看臨時檔案的位置:

go run --work main.go

想要直接編譯程式碼,使用 go build

go build main.go

這個指令會產生一個執行檔 main。在 Linux/OSX 系統中,你需要用 ./main 來執行它。

在你開發的過程中,你可能會用 go rungo build 等指令,但在你部署程式時, 你會就直接編譯好執行檔,並且將執行檔進行部署。

Main

希望我們上面執行的程式碼是好理解的。我們建立了一個函式,並且透過內建的 println 函式印出字串。當我們執行 go run 的時候,Go 會知道因為我們只有單一的一個檔案,而知道我們要執行的就是那隻程式嗎?不,在 Go 語言裡,程式的進入點是在 main package 裡面的 main 函式。

我們會在後面的章節討論 packege 的概念。現在,我們只要了解 Go 的基礎就好。 在這裡,我們一律將我們的程式碼寫在 main package 中。

如果你想要試試看,也可以變更 package 的名稱,接著一樣執行程式,看看會跑出什麼錯誤訊息。也可以把 package 改回 main,但是變更函式的名稱,又會跑出什麼錯誤訊息。嘗試執行一樣的變更,但不是執行,而是去編譯他,編譯時,你會發現這沒有進入點的編譯。當你在編譯一個套件時,這是相當正常的。

Import

Go 有相當多內建的函式,例如:println 可以不需要引用任何套件就可以使用。即使我們使用第三方套件,不使用內建的函式幾乎是不可能的。在 Go 中,使用 import 關鍵字可以用來引用在程式碼中使用的相關套件。

讓我們修改一下原本的程式碼:

package main

import (
  "fmt"
  "os"
)

func main() {
  if len(os.Args) != 2 {
    os.Exit(1)
  }
  fmt.Println("It's over", os.Args[1])
}

透過以下指令來執行:

go run main.go 9000

我們現在使用兩個 Go 的標準套件:fmtos。同時也會使用另一個內建的函式 lenlen 會傳回 string 的長度,或是一個 dictionary 的數量,亦或是我們這裡看到的 array 的長度。如果你不知道為什麼我們的程式是存取第二個參數,那是因為第一個參數 (index 是 0) 代表的是目前執行程式碼的路徑。

你或許注意到了我們在函數的前面多了前綴名稱 fmt.Println。這和其他的語言有所不同,我們會在後面談論到 package。現在,了解如何使用 import 和 package 就夠了。

Go 對於如何使用 import 來引入套件的管理上是嚴格的,如果你 import 了套件, 卻沒有使用它的話,是無法編譯成功的。嘗試執行下面的程式碼:

package main

import (
  "fmt"
  "os"
)

func main() {
}

你會得到兩個錯誤訊息,告訴你 fmtos 這兩個套件被引用但卻無法使用。覺得困擾嗎?肯定的,但隨著不斷學習,你會習慣的(儘管你仍然會感到困擾)。Go 會這麼嚴格的原因是,引用未使用的套件會使得編譯速度變慢,儘管,大多數人可能不會有這種程度的困擾。

另一個值得一提的是,Go 的標準函式庫有相當良好的文件。你可以到 https://golang.org/pkg/fmt/#Println 閱讀關於 fmt 套件中 Println 函式的文件,也可以點擊觀看原始碼。

如果你的電腦無法連上網路,你可以在本機端輸入:

godoc -http=:6060

然後在瀏覽器上到 http://localhost:6060 來檢視文件。

變數與宣告

當我們寫下 x = 4,這對於變數的宣告來說,也許是一個開始,也可以是結束。但不幸的,在 Go 中,事情相對複雜了些。我們會從簡單的範例開始,在下一章中使用 structure 的時候,來擴展我們的例子。你可能會想說,哇,是什麼事情會這麼複雜?讓我們先來看些例子。 在 Go 中,宣告變數最明確也是最冗長的方式是:

package main

import (
  "fmt"
)

func main() {
  var power int
  power = 9000
  fmt.Printf("It's over %d\n", power)
}

這裡,我們宣告了一個 int 類型的 power 變數,預設情況下,Go 會為這個變數分配一個零值。整數的話是 0、布林值為 false、字串是 "" 等。下一步,我們將 9000 指派給 power 這個變數。我們可以合併這兩行:

var power int = 9000

這仍然需要打很多字。在 Go 中,有一個方便的簡短宣告運算子::=,這個運算子可以進行型別的推論:

power := 9000

這很方便,同時在函式上也能這樣使用:

func main() {
  power := getPower()
}

func getPower() int {
  return 9001
}

重要的是,要記住 := 同時用於宣告變數以及為它賦值。為什麼? 因為變數不能被宣告兩次(在同一範圍內)。如果你嘗試執行以下操作,會收到錯誤。

func main() {
  power := 9000
  fmt.Printf("It's over %d\n", power)

  // 編譯錯誤:
  // 在 := 的左方沒有新的變數指派
  power := 9001
  fmt.Printf("It's also over %d\n", power)
}

編譯器會抱怨在 := 運算子的左側沒有新的變數。這代表,當我們首次宣告一個變數時,我們使用 := 運算子,但是在後續賦值中,我們使用賦值運算子 =。這相當合理,但會有點微妙,你需要多練習來讓你的肌肉可以順利的在這兩者之間進行轉換。

如果你仔細閱讀錯誤訊息,你會注意到變數是複數。這是因為 Go 允許你指派多個變數(使用 =:= 皆可)。

func main() {
  name, power := "Goku", 9000
  fmt.Printf("%s's power is over %d\n", name, power)
}

一旦這個變數是新的,:= 運算子就可以被使用。看看以下的範例:

func main() {
  power := 1000
  fmt.Printf("default power is %d\n", power)

  name, power := "Goku", 9000
  fmt.Printf("%s's power is over %d\n", name, power)
}

雖然 power 這個變數被 := 指派兩次,編譯器卻不會產生錯誤。編譯器會看到有一個新的變數 name,因此你可以正常使用 :=。但要注意的是,你不能變更 power 這個變數的型態,因為他被隱性的指派為整數,所以只能被整數賦值。

現在,你要知道的最後一件事情是,就跟 import 一樣,Go 不會讓你有宣告但未使用的變數。看看這個例子:

func main() {
  name, power := "Goku", 1000
  fmt.Printf("default power is %d\n", power)
}

將無法編譯成功。因為 name 變數宣告了但沒有被使用。就像 import 了未使用的套件一樣,這可能會讓某些人覺得沮喪,但整體來看,我認為這對於程式碼的整潔度和可讀性是有所幫助的。

關於變數的宣告和指派還有更多可以學習的部分。現在,你要記住,使用 var NAME TYPE 的方式來宣告一個變數會被賦予該變數的零值,使用 NAME := VALUE 是宣告變數並賦值,而 NAME = VALUE 是指派一個值給之前宣告過的變數。

函式宣告

在學習完變數宣告後,現在正是一個好時機讓你知道,Go 的函式是可以有多個回傳值的。讓我們來看看三個函式,一個沒有回傳值、一個回傳一個值,最後一個回傳兩個值:

func log(message string) {
}

func add(a int, b int) int {
}

func power(name string) (int, bool) {
}

我們可以這樣使用多回傳值的函式:

value, exists := power("goku")
if exists == false {
  // 處理錯誤情況
}

有時候,你只需要其中一個回傳值。在這種情況下,你可以將不需要處理的變數,用 _ 來取代:

_, exists := power("goku")
if exists == false {
  // 處理錯誤情況
}

這種使用方式不僅僅是一種常規,_ 空白識別符號是特別用在返回值不需要被實際指派的時候使用。你可以不管返回的類型,重複的使用 _ 識別符號。

最後,如果在函式所用到的參數共用同一種類型的話,我們可以用較短的語法宣告(如下方的 a、b 變數共用整數型態的宣告):

func add(a, b int) int {

}

函式多重返回值和使用 _ 來丟棄你不需要的回傳值是很常用到的功能。命名返回值和少量的 verbose 參數宣告不是那麼常見。不過你遲早會使用它們,所以了解他們還是很重要的。

在你繼續學習之前

我們學習了一些小部分的概念,你可能會覺得有點零散。我們會慢慢學習一些更大的程式,屆時這些部分將會聚集成完整的程式碼。

如果你之前是學習動態程式語言,你或許會覺得複雜的型別和宣告是一種退步。但我並不認同這樣的想法,對於某些系統,也許動態語言會更有生產力。如果你之前是學習靜態程式語言,你也許在學習 Go 的過程是覺得熟悉的,推論的型別和多回傳值是相當好的特性(儘管並非 Go 所獨有)。希望隨著我們學習更多,會對於 Go 乾淨簡潔的語法會更加欣賞。