文章目錄
- 變數聲明
- 條件陳述式
- 分支語句
- 迴圈
- 函數
- 基本類型
- Struct類型
- 方法
- 介面
- 繼承
- Go常式
- 通道類型
Go語言是什嗎?
Google最近發布新型的程式設計語言,Go。它被設計為將現代程式設計語言的先進 性帶入到目前仍由C語言占統治地位的系統層面。然而,這一語言仍在實驗階段並在不斷演變。
Go語言的設計者計劃設計一門簡單、高效、安全和 並發的語言。這門語言簡單到甚至不需要有一個符號表來進行詞法分析。它可以快速地編譯;整個工程的編譯時間在秒以下的情況是常事。它具備記憶體回收功能,因 此從記憶體的角度是安全的。它進行靜態類型檢查,並且不允許強制類型轉換,因而對於類型而言是安全的。同時語言還內建了強大的並發實現機制。
閱讀Go
Go的文法傳承了與C一樣的風格。程式由函數組成,而函數體是一系列的語句序列。一段代碼塊用花括弧括起來。這門語言保留有限的關鍵字。運算式使用 同樣的中綴運算子。文法上並無 太多出奇之處。
Go語言的作者在設計這一語言時堅持一個單一的指導原則:簡單明了至上。一些新的文法構件提供了簡明地表達一些約定俗成的概 念的方式,相較之下用C表達顯得冗長。而其他方面則是針對幾十年的使用所呈現出來的一些不合理的語言選擇作出了改進。
變數聲明
變數是如下聲明的:
var sum int // 簡單聲明var total int = 42 // 聲明並初始化
最值得注意的是,這些聲明裡的類型跟在變數名的後面。乍一看有點怪,但這更清晰明了。比如,以下面這個C片段來說:
int* a, b;
它並明了,但這裡實際的意思是a是一個指標,但b不是。如果要將兩者都聲明為指標,必須要重複星號。然後在Go語言裡,通過如下方式可以將兩者都 聲明為指標:
var a, b *int
如果一個變數初始化了,編譯器通常能推斷它的類型,所以程式員不必顯式的敲出來:
var label = "name"
然而,在這種情況下var幾乎顯得是多餘了。因此,Go的作者引入了一個新的運算子來 聲明和初始化一個新的變數:
name := "Samuel"
條件陳述式
Go語言當中的條件句與C當中所熟知的if-else構造一樣,但條件不需要被打包在括弧內。這樣可以減少閱讀代碼時的視覺上的混亂。
括弧並不是唯一被移去的視覺幹擾。在條件之間可以包括一個簡單的語句,所以如下的代碼:
result := someFunc();
if result > 0 {
/* Do something */
} else {
/* Handle error */
}
可以被精簡成:
if result := someFunc(); result > 0 {
/* Do something */
} else {
/* Handle error */
}
然而,在後面這個例子當中,result只在條件塊內部有效——而前者 中,它在整個包含它的上下文中都是可存取的。
分支語句
分支語句同樣是似曾相識,但也有增強。像條件陳述式一樣,它允許一個簡單的語句位於分支的運算式之前。然而,他們相對於在C語言中的分支而言走得更遠。
首先,為了讓分支跳轉更簡明,作了兩個修改。情況可以是逗號分隔的列表,而fall-throuth也不再是預設的行為。
因此,如下的C代碼:
int result;
switch (byte) {
case 'a':
case 'b':
{
result = 1
break
}
default:
result = 0
}
在Go裡就變成了這樣:
var result int
switch byte {
case 'a', 'b':
result = 1
default:
result = 0
}
第二點,Go的分支跳轉可以匹配比整數和字元更多的內容,任何有效運算式都可以作為跳躍陳述式值。只要它與分支條件的類型是一樣的。
因此如下的C代碼:
int result = calculate();
if (result < 0) {
/* negative */
} else if (result > 0) {
/* positive */
} else {
/* zero */
}
在Go裡可以這樣表達:
switch result := calculate(); true {
case result < 0:
/* negative */
case result > 0:
/* positive */
default:
/* zero */
}
這些都是公用的約定俗成,比如如果分支值省略了,就是預設為真,所以上面的代碼可以這樣寫:
switch result := calculate(); {
case result < 0:
/* negative */
case result > 0:
/* positive */
default:
/* zero */
}
迴圈
Go只有一個關鍵字用於引入迴圈。但它提供了除do-while外C語言當中所有可用的迴圈方式。
條件
for a > b { /* ... */ }
初始,條件和步進
for i := 0; i < 10; i++ { /* ... */ }
範圍
range語句右邊的運算式必須是array,slice,string或者map, 或是指向array的指標,也可以是channel。
for i := range "hello" { /* ... */ }
無限迴圈
for { /* ever */ }
函數
聲明函數的文法與C不同。就像變數聲明一樣,類型是在它們所描述的術語之後聲明的。在C語言中:
int add(int a, b) { return a + b }
在Go裡面是這樣描述的:
func add(a, b int) int { return a + b }
多傳回值
在C語言當中常見的做法是保留一個傳回值來表示錯誤(比如,read()返回0),或 者保留傳回值來通知狀態,並將傳遞儲存結果的記憶體位址的指標。這容易產生了不安全的編程實踐,因此在像Go語言這樣有良好管理的語言中是不可行的。
認識到這一問題的影響已超出了函數結果與錯誤通訊的簡單需求的範疇,Go的作者們在語言中內建了函數返回多個值的能力。
作為例子,這個函數將返回整數除法的兩個部分:
func divide(a, b int) (int, int) {
quotient := a / b
remainder := a % b
return quotient, remainder
}
有了多個傳回值,有良好的代碼文檔會更好——而Go允許你給傳回值命名,就像參數一樣。你可以對這些返回的變數賦值,就像其它的變數一樣。所以我們可以重寫divide:
func divide(a, b int) (quotient, remainder int) {
quotient = a / b
remainder = a % b
return
}
多傳回值的出現促進了"comma-ok"的模式。有可能失敗函式可以返回第二個布爾結果來表示成功。作為替代,也可以返回一個錯誤對象,因此像下面這樣的代碼也就不見怪了:
if result, ok := moreMagic(); ok {
/* Do something with result */
}
匿名函數
有了垃圾收集器意味著為許多不同的特性敞開了大門——其中就包括匿名函數。Go為聲明匿名函數提供了簡單的文法。像許多動態語言一樣,這些函數在它們被定義的範圍內建立了詞法閉包。
考慮如下的程式:
func makeAdder(x int) (func(int) int) {
return func(y int) int { return x + y }
}
func main() {
add5 := makeAdder(5)
add36 := makeAdder(36)
fmt.Println("The answer:", add5(add36(1))) //=> The answer: 42
}
基本類型
像C語言一樣,Go提供了一系列的基本類型,常見的布爾,整數和浮點數類型都具備。它有一個Unicode的字串類型和數群組類型。同時該語言還引入了兩 種新的類型:slice 和map。
數組和切片
Go語言當中的數組不是像C語言那樣動態。它們的大小是類型的一部分,在編譯時間就決定了。數組的索引還是使用的熟悉的C文法(如 a[i]),並且與C一樣,索引是由0開始的。編譯器提供了內建的功能在編譯時間求得一個數組的長度 (如 len(a))。如果試圖超過數組界限寫入,會產生一個執行階段錯誤。
Go還提供了切片(slices),作為數組的變形。一個切片(slice)表示一個數組內的連續分段,支援程式員指定底層儲存的明確部分。構建一個切片 的文法與訪問一個數組元素類似:
/* Construct a slice on ary that starts at s and is len elements long */
s1 := ary[s:len]
/* Omit the length to create a slice to the end of ary */
s2 := ary[s:]
/* Slices behave just like arrays */
s[0] == ary[s] //=> true
// Changing the value in a slice changes it in the array
ary[s] = 1
s[0] = 42
ary[s] == 42 //=> true
該切片所引用的數組分段可以通過將新的切片賦值給同一變數來更改:
/* Move the start of the slice forward by one, but do not move the end */
s2 = s2[1:]
/* Slices can only move forward */
s2 = s2[-1:] // this is a compile error
切片的長度可以更改,只要不超出切片的容量。切片s的容量是數組從s[0]到數組尾端的大小,並由內建的cap()函數返回。一個切片的長度永遠不能超出它的容量。
這裡有一個展示長度和容量互動的例子:
a := [...]int{1,2,3,4,5} // The ... means "whatever length the initializer has"
len(a) //=> 5
/* Slice from the middle */
s := a[2:4] //=> [3 4]
len(s), cap(s) //=> 2, 3
/* Grow the slice */
s = s[0:3] //=> [3 4 5]
len(s), cap(s) //=> 3, 3
/* Cannot grow it past its capacity */
s = s[0:4] // this is a compile error
通常,一個切片就是一個程式所需要的全部了,在這種情況下,程式員根本用不著一個數組,Go有兩種方式直接建立切片而不用引用底層儲存:
/* literal */
s1 := []int{1,2,3,4,5}
/* empty (all zero values) */
s2 := make([]int, 10) // cap(s2) == len(s2) == 10
Map類型
幾乎每個現在流行的動態語言都有的資料類型,但在C中不具備的,就是dictionary。Go提供了一個基本的dictionary類型叫做map。下 面的例子展示了如何建立和使用Go map:
m := make(map[string] int) // A mapping of strings to ints
/* Store some values */
m["foo"] = 42
m["bar"] = 30
/* Read, and exit program with a runtime error if key is not present. */
x := m["foo"]
/* Read, with comma-ok check; ok will be false if key was not present. */
x, ok := m["bar"]
/* Check for presence of key, _ means "I don't care about this value." */
_, ok := m["baz"] // ok == false
/* Assign zero as a valid value */
m["foo"] = 0;
_, ok := m["foo"] // ok == true
/* Delete a key */
m["bar"] = 0, false
_, ok := m["bar"] // ok == false
物件導向
Go語言支援類似於C語言中使用的物件導向風格。資料被組織成structs,然後定義操作這些structs的函數。類似於Python,Go語言提供 了定義函數並調用它們的方式,因此文法並不會笨拙。
Struct類型
定義一個新的struct類型很簡單:
type Point struct {
x, y float64
}
現在這一類型的值可以通過內建的函數new來分配,這將返回一個指標,指向一塊記憶體單元,其所佔記憶體槽初始化為零。
var p *Point = new(Point)
p.x = 3
p.y = 4
這顯得很冗長,而Go語言的一個目標是儘可能的簡明扼要。所以提供了一個同時分配和初始化struct的文法:
var p1 Point = Point{3,4} // Value
var p2 *Point = &Point{3,4} // Pointer
方法
一旦聲明了類型,就可以將該類型顯式的作為第一個參數來聲明函數:
func (self Point) Length() float {
return math.Sqrt(self.x*self.x + self.y*self.y);
}
這些函數之後可作為struct的方法而被調用:
p := Point{3,4}
d := p.Length() //=> 5
方法實際上既可以聲明為值也可以聲明為指標類型。Go將會適當的處理引用或解引用對象,所以既可以對類型New">T,也可以對類型*T聲明方式,併合理地使用它們。
讓我們為Point擴充一個變換器:
/* Note the receiver is *Point */
func (self *Point) Scale(factor float64) {
self.x = self.x * factor
self.y = self.y * factor
}
然後我們可以像這樣調用:
p.Scale(2);
d = p.Length() //=> 10
很重要的一點是理解傳遞給MoveToXY的New">self和其它的參數一樣,並且是值傳遞,而不是引用傳遞。如果它被聲明為Point,那麼在方法內修改的struct就不再跟調用方的一樣——值在它們傳遞給方法的時候被 拷貝,並在調用結束後被丟棄。
介面
像Ruby這樣的動態語言所強調物件導向編程的風格認為對象的行為比哪種對象是動態類型(duck typing)更為重要。Go所 帶來的一個最強大的特性之一就是提供了可以在編程時運用動態類型的思想而把行為定義的合法性檢查的工作推到編譯時間。這一行為的名字被稱作介面。
定義一個介面很簡單:
type Writer interface {
Write(p []byte) (n int, err os.Error)
}
這裡定義了一個介面和一個寫位元組緩衝的方法。任何實現了這一方法的對象也實現了這一介面。不需要像Java一樣進行聲明,編譯器能推斷出來。這既給予了動態類型的表達能力又保留了靜態類型檢查的安全。
Go當中介面的運作方式支援開發人員在編寫程式的時候發現程式的類型。如果幾個對象間存在公用行為,而開發人員想要抽象這種行為,那麼它就可以建立一個介面並使用它。
考慮如下的代碼:
// Somewhere in some code:
type Widget struct {}
func (Widget) Frob() { /* do something */ }
// Somewhere else in the code:
type Sprocket struct {}
func (Sprocket) Frob() { /* do something else */ }
/* New code, and we want to take both Widgets and Sprockets and Frob them */
type Frobber interface {
Frob()
}
func frobtastic(f Frobber) { f.Frob() }
需要特別指出的很重要的一點就是所有的對象都實現了這個空介面:
interface {}
繼承
Go語言不支援繼承,至少與大多數語言的繼承不一樣。並不存在類型的階層。相較於繼承,Go鼓勵使用組合和委派,並為此提供了相應的文法甜點使其更容易接受。
有了這樣的定義:
type Engine interface {
Start()
Stop()
}
type Car struct {
Engine
}
於是我可以像下面這樣編寫:
func GoToWorkIn(c Car) {
/* get in car */
c.Start();
/* drive to work */
c.Stop();
/* get out of car */
}
當我聲明Car這個struct的時候,我定義了一個匿名成員。這是一 個只能被其類型識別的成員。匿名成員與其它的成員一樣,並有著和類型一樣的名字。因此我還可以寫成c.Engine.Start()。 如果Car並沒有其自身方法可以滿足調用的話,編譯器自動的會將在Car上的調用委派給它的Engine上面的方法。
由匿名成員提供的分離方法的規則是保守的。如果為一個類型定義了一個方法,就使用它。如果不是,就使用為匿名成員定義的方法。如果有兩個匿名成員都提供一 個方法,編譯器將會報錯,但只在該方法被調用的情況下。
這種組合是通過委派來實現的,而不是繼承。一旦匿名成員的方法被調用,控制流程整個都被委派給了該方法。所以你無法做到和下面的例子一樣來類比類型層次:
type Base struct {}
func (Base) Magic() { fmt.Print("base magic") }
func (self Base) MoreMagic() {
self.Magic()
self.Magic()
}
type Foo struct {
Base
}
func (Foo) Magic() { fmt.Print("foo magic") }
當你建立一個Foo對象時,它將會影響New">Base的兩個方法。然而,當你調用MoreMagic時, 你將得不到期望的結果:
f := new(Foo)
f.Magic() //=> foo magic
f.MoreMagic() //=> base magic base magic
並發
Go的作者選擇了訊息傳遞模型來作為推薦的並發編程方法。該語言同樣支援共用記憶體,然後作者自有道理:
不要通過共用記憶體來通訊,相反,通過通訊來共用記憶體。
該語言提供了兩個基本的構件來支援這一範型:goroutines和channels。
Go常式
Goroutine是輕量級的並行程式執行路徑,與線程,coroutine或者進程類似。然而,它們彼此相當不同,因此Go作者決定給它一個新的名字並 放棄其它術語可能隱含的意義。
建立一個goroutine來運行名為DoThis的函數十分簡單:
go DoThis() // but do not wait for it to complete
匿名的函數可以這樣使用:
go func() {
for { /* do something forever */ }
}() // Note that the function must be invoked
這些goroutine將會通過Go運行時而映射到適當的作業系統原語(比如,POSIX線程)。
通道類型
有了goroutine,代碼的並存執行就容易了。然而,它們之間仍然需要通訊機制。Channel提供一個FIFO通訊隊列剛好能達到這一目的。
以下是使用channel的文法:
/* Creating a channel uses make(), not new - it was also used for map creation */
ch := make(chan int)
/* Sending a value blocks until the value is read */
ch <- 4
/* Reading a value blocks until a value is available */
i := <-ch
舉例來說,如果我們想要進行長時間啟動並執行數值計算,我們可以這樣做:
ch := make(chan int)
go func() {
result := 0
for i := 0; i < 100000000; i++ {
result = result + i
}
ch <- result
}()
/* Do something for a while */
sum := <-ch // This will block if the calculation is not done yet
fmt.Println("The sum is:", sum)
channel的阻塞行為並非永遠是最佳的。該語言提供了兩種對其進行定製的方式:
- 程式員可以指定緩衝大小——想緩衝的channel發送訊息不會阻塞,除非緩衝已滿,同樣從緩衝的channel讀取也不會阻塞,除非緩衝是空的。
- 該語言同時還提供了不會被阻塞的發送和接收的能力,而操作成功是仍然要報告。
/* Create a channel with buffer size 5 */
ch := make(chan int, 5)
/* Send without blocking, ok will be true if value was buffered */
ok := ch <- 42
/* Read without blocking, ok will be true if a value was read */
val, ok := <-ch
包
Go提供了一種簡單的機制來組織代碼:包。每個檔案開頭都會聲明它屬於哪一個包,每個檔案也可以引入它所用到的包。任何首字母大寫的名字是由包匯出的,並可以被其它的包所使用。
以下是一個完整的源檔案:
package geometry
import "math"
/* Point is capitalized, so it is visible outside the package. */
type Point struct {
/* the fields are not capitalized, so they are not visible
outside of the package */
x, y float64
}
/* These functions are visible outside of the package */
func (self Point) Length() float64 {
/* This uses a function in the math package */
return math.Sqrt(self.x*self.x + self.y*self.y)
}
func (self *Point) Scale(factor float64) {
self.setX(self.x * factor)
self.setY(self.y * factor)
}
/* These functions are not visible outside of the package, but can be
used inside the package */
func (self *Point) setX(x float64) { self.x = x }
func (self *Point) setY(y float64) { self.y = y }
缺失
Go語言的作者試圖將代碼的清晰明確作為設計該語言作出所有決定的指導思想。第二個目標是生產一個編譯速度很快的語言。有了這兩個標準作為方向,來 自其它語言的許多特性就不那麼適合了。許多程式員會發現他們最愛的語言特性在Go當中不存在,確實,有很多人也許會覺得Go語言由於缺乏其它語言所共有的 一些特性,還不太可用。
這當中兩個缺失的特性就是異常和泛型,兩者在其它語言當中都是非常有用的。而它們目前都不是Go的一分子。但因為該 語言仍處於實驗階段,它們有可能最終會加入到語言裡。然而,如果將Go與其它語言作比較的話,我們應當記住Go是打算在系統編程層面作為C語言的替代。明 白這一點的話,那麼缺失的這許多特性倒也不是很大的問題了。
最後,因為這一語言才剛剛發布,因此它沒有什麼類庫或工具可以用,也沒有Go語 言的整合編程環境。Go語言標準庫有些有用的代碼,但這與更為成熟的語言比 起來仍還是很少的。
查看英文原文:Google Go: A Primer。