The Little Go Book 閱讀筆記

- notes golang

因為過年閒閒沒事,開始來看一點 Go Lang 的書,從官網連結中找到一本『小』書 The Little Go Book,它是 2014 年發佈的,僅介紹基礎的部分,整份 PDF 只有 84 頁,用來入門的負擔應該不會太大。

另外,可以觀察到這本書的一個特色,作者應該很喜歡七龍珠。一些變數或自訂結構的名稱如:

所以,才會在第 1 個例子有戰鬥力的數值。

The Basics (1)

  1. 變數宣告
  2. 型態推論
  3. 函式

第 1 章是介紹基礎知識,除了如何開始跑出第 1 隻 hello world 程式,並開始使用 import 來使用 standard library 的功能,還有語法的一些基本常識之外沒有太多難以理解的部分。

變數

只是變數宣告或函式宣告的順序跟以外習慣的 Java 或 Python 不太相同。這點可以透過 language specification 中的語法規則來說明:

VarDecl     = "var" ( VarSpec | "(" { VarSpec ";" } ")" ) .
VarSpec     = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .

能看得出在關鍵字 var 後要先接著 IdentifierList (簡單說就是一堆變數名稱),最後才是接到 Type,如果有 assignment 的情況,才會再有 = 出現。書上的第 1 個宣告變數並開始使用它的範例為:

var power int
power = 9000

其實也能寫成同一行,因為 golang 並不允許我們有未使用的變數或其它東西 (例如:import 了卻沒有使用),它們會是編譯期的錯誤。在這一章,書會也介紹了比較短的宣告型式,並且我們可以省略 Type 的部分:

power := 9000

同樣地,我們能回到官網找出對應的語法規格:

ShortVarDecl = IdentifierList ":=" ExpressionList .

由於不再需要寫 Type 了,就是透過 golang 自行做型態推論,並且會自動檢查 := 左方的變數 有新的名字出現,像我們無法連續寫 2 次的:

power := 9000
power := 5566

在寫第 1 組的 power := 前,power 不存在,它是新出現的變數可以宣告。第 2 組的 := 左式,僅有 power 它出現過了,沒有其他新的變數了,編譯會失敗。要讓它合乎語法,我們得加入 的變數,把上面的例子改寫一下:

power := 9000
power, action := 5566, 8787

第 2 組宣告出現了新變數,在語法上就合法了。對已出現過的 power 來說,就是重新賦值的動作而已 (但只能指定一相型態的 int)

函式

在第 1 章尾,開始介紹函式如何使用。內容相當簡單,我們截取一下官關的語法規則補充:

FunctionType   = "func" Signature .
Signature      = Parameters [ Result ] .
Result         = Parameters | Type .
Parameters     = "(" [ ParameterList [ "," ] ] ")" .
ParameterList  = ParameterDecl { "," ParameterDecl } .
ParameterDecl  = [ IdentifierList ] [ "..." ] Type .

由上面的規則,可能衍生出下列的例子:

func()
func(x int) int
func(a, _ int, z float32) bool
func(a, b int, z float32) (bool)
func(prefix string, values ...int)
func(a, b int, z float64, opt ...interface{}) (success bool)
func(int, int, float64) (float64, *[]int)
func(n int) func(p *T)

比對後可以知道,如同變數宣告一樣,函式的型態也是放在最後,或著可以沒有宣告回傳型態 (因為回傳的 Result 被 [ ] 包圍,表示它是 optional 的):

FunctionType   = "func" Signature .
Signature      = Parameters [ Result ] .

在書上有提到一個接函式結果時,可以使用 _ 作為不需要的值的慣例:

_, exists := power("goku")
if exists == false {
// handle this error case
}

power 回傳的值為 (戰鬥力, 是否有此人物),它問了 悟空 的 power 結果,在這段程式它只在意悟空存不存在,所以使用 _ 來接目前情境下不重要的戰鬥力。另外做了一個小實驗 _ 不會被視為 new variables,所以使用 := 時不能單獨存在。

Structures (2)

書上的第 2 部分為結構,並開宗明義表示 Go 並非 OOP 語言,它沒有物件的階層,所以也不會有承繼與多形。它會是一種以 structure 作為資料載體,並將 method 關聯到此結構的實作型式。

type Saiyan struct {
	Name string
	Power int
}

結構的第 1 個範例是宣告了『賽亞人』,它有名子,有戰力數值。對寫過 C/C++ 的朋友,對這結構應該不算陌生,雖然語言有點變化,但意義上大致是一樣的。接下來有趣的例子是經驗的將 struct 傳進 function 後,並無法改變原始值的問題:

func Super(s Saiyan) {
	s.Power += 10000
}

func main() {
	goku := Saiyan{
		Name:  "Goku",
		Power: 9000,
	}

	// 悟空要變超級賽亞人囉~~~
	Super(goku)
	fmt.Println(goku)
}

輸出結果為

{Goku 9000}

少了 1 萬點的戰力數值怎麼好意思叫超級賽亞人呢?於是要透過在 C/C++ 已經用到很熟的 &*,透過傳址與指標來搞定它:

func Super(s *Saiyan) {
	s.Power += 10000
}

func main() {
	goku := &Saiyan{
		Name:  "Goku",
		Power: 9000,
	}

	// 真的變超級賽亞人囉!!!共 19000 的戰力數值
	Super(goku)
	fmt.Println(goku)
}

接著,我們還差一點把 function 變得好看一點,實際上我們期望的會是:

goku.Super()

而不是

Super(goku)

這件事需要稍為改寫一下 Super 函式的宣告,有沒有很像 Python class 在定義 method 時,第 1 個參數要填 self 的感覺:

func (s *Saiyan) Super() {
	s.Power += 10000
}

func main() {
	goku := &Saiyan{
		Name:  "Goku",
		Power: 9000,
	}

	goku.Super()
	fmt.Println(goku)
}

語法規格上,被提出去的 (s *Saiyan) 稱作 Receiver:

MethodDecl = "func" Receiver MethodName Signature [ FunctionBody ] .
Receiver   = Parameters .

它其實就是 Parameters,換句話說,是有變數名稱與 Type 的 list。一旦 function 有了 Receiver,我們就改稱它叫 Method 囉!覺得這詞語慣例其實也挺 OOP 的就是,雖然它沒不支援繼承。

在這裡需注意一下,書上雖然用 struct 來示範,但作為 Receiver 的型態,並沒有限制在 struct,請見golang patterns 的範例

建構子

建構子的概念來自 OOP,即使 golang 不支援我們定義 class,但仍有自訂型態可以用,像 structure 就是一種。所以,慣例上會替 structure 實作 factory method 來建立它:

func NewSaiyan(name string, power int) *Saiyan {
	return &Saiyan{name, power}
}

其實就是傳回一個建立好的 Saiyan 結構的位置罷了 (目前的範例不夠大,若建構流程比較多事要做的話,準備 factory method 是合理的選項)。在關於建構子的話題,書上也順便介紹了 new 內建函式,它其實是簡化語法用的:

s := &Saiyan{}
s := new(Saiyan)

可以直接看 new 的定義,僅僅配置好空間,並回傳一個指標:

// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type

由於使用 new 函式建立物件,仍要各別指定欄位的內容,跟直接寫在結構內初始參數沒太大差異。本書的作者認為,後者比較被多人接受:

goku := new(Saiyan)
goku.name = "goku"
goku.power = 9001
//vs
goku := &Saiyan {
name: "goku",
power: 9000,
}

聚合

基於 golang 不支援 class 語法,一些相應用使用方式都會以 structure 為主,例如過去常在 OOP 內把物件嵌入某個物件之內,並透過委派 (delegation) 來間接使用聚合物件的功能。在 golang 裡,要成為聚合 (composition) 只要 不宣告欄位名稱 即可:

type Person struct {
	Name string
}

type Saiyan struct {
	*Person
	Power  int
}

改寫賽亞人,把名字的部分拆出去到 Person 結構之中,為了讓 Person 在 Saiyan 內是個聚合的型式,我們宣告它的名稱。接下來我們能針對 Person 提供 method

func (p *Person) Introduce() {
	fmt.Printf("Hi, I'm %s\n", p.Name)
}

神奇的事發生了,我們能直接對 Saiyan 使用這個 method:

goku.Introduce()
// 會印出 Hi, I'm Goku

雖然 Receiver 並非針對 Saiyan,但在 Saiyan 內隱含著有 Person,於是它就會被呼叫到了。

Maps, Arrays and Slices (3)

書上的第 3 部分為常見的容器介紹,這部分大致上就熟悉語法為主。其中比較特別的大概就 slice 的部分了,在其它靜態語言比較少在初學書籍就介紹這一塊,也許要先猜測這東西在 golang 中被廣泛地使用。若是學過 Python 的朋友,應該蠻習慣在 list 變數旁用 slice operation,概念上相似但 golang 中的用法不太一樣就是了。

Array

陣列是多數語言都提供這個型態:一個固定長度的容器,完成配置就不能變更大小,像我們可以用 var 來宣告它,並指定第 1 個位置 87 分不能再高了:

var scores [4]int
scores[0] = 87

我們也可以用 := 同時完成宣告與配置,並利用 for 配合 range

scores := [4]int{55, 66, 87, 87}

for index, value := range scores {
	fmt.Println(index, value)
}

這裡值得注意的地方是 range 是 golang 的關鍵字,不像 Python 是提供 range generator 來進行 iteration。另外,若是用 GoLand 開發的話,它的 Postfix Completion 有支援 for range 的樣版,名稱為 forr

若單純想知道陣列的長度,有 len 函式可以使用:

len(scores)

附帶一提由語法規則可以看出 [] 標註是在型態的前方:

ArrayType   = "[" ArrayLength "]" ElementType .
ArrayLength = Expression .
ElementType = Type .

由陣列的語法來看,它似乎是只有 1 維,對話規格書的解說:

The elements can be addressed by integer indices 0 through len(a)-1. Array types are always one-dimensional but may be composed to form multi-dimensional types.

但英文文件的重點常在 but 轉折語之後:

may be composed to form multi-dimensional types.

這句的玄機就是,看到 [] 起頭的宣告時,第 1 組會被當作陣列長度,之後的會變成 ElementType,所以,這樣寫是合法的:

var test [3][3][3]int
test[0][0][0] = 9
test[0][0][1] = 7
test[0][0][2] = 8
fmt.Println(test)

Slice

直接上 code !!! 在 golang 中用 make 函式來定義 slice (慣例的寫法):

scores := make([]int, 0, 10)
fmt.Println(scores)

make 函式配合 slice 使用,後面接著 長度容量

make([]T, length, capacity)

語法規格有提到下面 2 者是等價的:

produces the same slice as allocating an array and slicing it, so these two expressions are equivalent:

make([]int, 50, 100)
new([100]int)[0:50]

另外 make 其實有 overloading,會因為第 1 個參數不同而有不同的效果,它分別對於 slice、map 與 channel 有不同的意義。

Map

Map 的語法規格如下:

MapType     = "map" "[" KeyType "]" ElementType .
KeyType     = Type .

宣告 map 時,就使用 map 關鍵字,配合 [] 在中間放 key 的型態,尾端放 value 的型態即可,書中簡單的例子如下:

func main() {
	lookup := make(map[string]int)
	lookup["goku"] = 9001

	power, exists := lookup["vegeta"]
	fmt.Println(power, exists)
}

它查了一個不存在 map 的人物 vegeta (經 google 查詢,似乎是達爾),獲得:

0 false

學會使用 map 後,書上提供了與 struct 併用的例子:

goku := &Saiyan{
	Name:    "Goku",
	Friends: make(map[string]*Saiyan),
}
goku.Friends["krillin"] = &Saiyan{
	Name:    "Krillin",
	Friends: nil,
}

其它常用的功能:

Code Organization and Interfaces (4)

package

在第 4 章裡,主要是講述 packageimport 的使用方法,另外也介紹 interface。由於這部分的概念在許多語言都有,其實不算太新鮮的事,若不去問 golang 如何做的,單純需要能理解語法怎麼用的程度,區分上不會太大。

由於 golang 不像 Java 有 class 或 也不像 Python 有 module,能作為變數或 method 的載體的容器,觀察看起 檔名 對於 golang 是沒有意義的,它會將所有的變數或函式儲放在 package name 的空間內,所以 package name 會成為一個 reference,能用來存取相關的變數或函式。

package 的語法規格 可以知道:

Go programs are constructed by linking together packages. A package in turn is constructed from one or more source files that …

相同 package name 的 source code 會被連結在一起最後生出 Go 應用程式:

SourceFile       = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .

我們可以看出,在一個原始檔內,需要由 package 子句開頭的規則。需注意的是,不像 Java 需要寫完整的 package 名稱,golang 宣告 package 時只需要該相對錄路的名稱即可。

此外,書本上有介紹到 Cyclical Imports 的問題需要注意,還有 Visibility 的問題,這也終於解開了我對於為何 Golang 都是大寫開頭的命名慣例的疑問:

Go uses a simple rule to define what types and functions are visible outside of a package. If the name of the type or function starts with an uppercase letter, it’s visible. If it starts with a lowercase letter, it isn’t.

大寫開頭的變數或函式才能被 export 出來,讓其它人使用。

import

書上對於 import 的介紹不算多,但在看語法規格時發現了個實用的用法,可以替 importpackage name 改變名稱:

ImportDecl       = "import" ( ImportSpec | "(" { ImportSpec ";" } ")" ) .
ImportSpec       = [ "." | PackageName ] ImportPath .
ImportPath       = string_lit .

在比較單純的情況,我們的 ImportSpec 只有單純寫 ImportPath 而已,但有些情況我們會希望替它改名稱,它給了我們些範例:

Import declaration          Local name of Sin

import   "lib/math"         math.Sin
import m "lib/math"         m.Sin
import . "lib/math"         Sin

import _

在 import 的語法規格,還有介紹到一個特殊的 package name:

import _ "lib/math"

它的功能描述如下:

An import declaration declares a dependency relation between the importing and imported package. It is illegal for a package to import itself, directly or indirectly, or to directly import a package without referring to any of its exported identifiers. To import a package solely for its side-effects (initialization), use the blank identifier as explicit package name

大意是說,因為 golang 不能讓我們 import 了又不使用它,但有些情況我們需要 import 它,舉個 Java 程式設計師懂得例子:

Class.forName("com.mysql.jdbc.Driver");

我們透過 ClassLoader 來註冊 JDBC Driver,而 golang 沒有 class 的設計,但它有 Package initialization,所以對 golang 的資料庫相關的函式庫,透過這個機制來初始化 Driver,只需要 import 它就行。以 lib/pq 的 conn.go 為例:

func init() {
	sql.Register("postgres", &Driver{})
}

當程式執行到 import 時,它就會完成 postgres driver 的註冊:

import (
  "database/sql"
  _ "github.com/lib/pq"
)

如同 _ blank identify 的意義,它用來承接需要接下的變數,接下後丟棄。我們單純為了運用 import 後自動呼叫 init() 的副作用罷了。這例子取自於 Why we import SQL drivers as the blank identifier ( _ ) in Go

interface

這章介紹的 interface 用法很平常,但在第 5 章的 Empty Interface and Conversions 看到偽 overloading 的希望。

後續的章節

在書中最後二章,第 5 章是講一些各種小東西的用法,例如 defer,第 6 章是講 concurrency。看了一下內容,差不多是該換點學習材料了。跟 concurrency 有關的部分,在 golang 官網的 wiki 有豐富的整理呦。