編程語言 go

Google最近發佈新型的編程語言,Go。它被設計爲將現代編程語言的先進性帶入到目前仍由C語言佔統治地位的系統層面。然而,這一語言仍在試驗階段並在不斷演變。

Go語言的設計者計劃設計一門簡單、高效、安全和 併發的語言。這門語言簡單到甚至不需要有一個符號表來進行詞法分析。它可以快速地編譯;整個工程的編譯時間在秒以下的情況是常事。它具備垃圾回收功能,因此從內存的角度是安全的。它進行靜態類型檢查,並且不允許強制類型轉換,因而對於類型而言是安全的。同時語言還內建了強大的併發實現機制。

閱讀Go




Go的語法傳承了與C一樣的風格。程序由函數組成,而函數體是一系列的語句序列。一段代碼塊用花括號括起來。這門語言保留有限的關鍵字。表達式使用同樣的中綴運算符。語法上並無太多出奇之處。

Go語言的作者在設計這一語言時堅持一個單一的指導原則:簡單明瞭至上。一些新的語法構件提供了簡明地表達一些約定俗成的概念的方式,相較之下用C表達顯得冗長。而其他方面則是針對幾十年的使用所呈現出來的一些不合理的語言選擇作出了改進。

變量聲明




變量是如下聲明的:

  1. var sum int // 簡單聲明
  2. var total int = 42 // 聲明並初始化
複製代碼

最值得注意的是,這些聲明裏的類型跟在變量名的後面。乍一看有點怪,但這更清晰明瞭。比如,以下面這個C片段來說:

[code=c]int* a, b;[/code]
它並明瞭,但這裏實際的意思是a是一個指針,但b不是。如果要將兩者都聲明爲指針,必須要重複星號。然後在Go語言裏,通過如下方式可以將兩者都 聲明爲指針:

  1. var a, b *int
複製代碼

如果一個變量初始化了,編譯器通常能推斷它的類型,所以程序員不必顯式的敲出來:
  1. var label = "name"
複製代碼

然而,在這種情況下var幾乎顯得是多餘了。因此,Go的作者引入了一個新的運 算符來 聲明和初始化一個新的變量:

  1. name := "Samuel"
複製代碼


條件語句




Go語言當中的條件句與C當中所熟知的if-else構造一樣,但條件不需要被打包在括號內。這樣可以減少閱讀代碼時的視覺上的混亂。

括號並不是唯一被移去的視覺干擾。在條件之間可以包括一個簡單的語句,所以如下的代碼:

  1. result := someFunc();
  2. if result > 0 {
  3.         /* Do something */
  4. } else {
  5.         /* Handle error */
  6. }
複製代碼

可以被精簡成:

  1. if result := someFunc(); result > 0 {
  2.         /* Do something */
  3. } else {
  4.         /* Handle error */
  5. }
複製代碼

然而,在後面這個例子當中,result只在條件塊內部有效—— 而前者 中,它在整個包含它的上下文中都是可存取的。

分支語句




分支語句同樣是似曾相識,但也有增強。像條件語句一樣,它允許一個簡單的語句位於分支的表達式之前。然而,他們相對於在C語言中的分支而言走得更 遠。

首先,爲了讓分支跳轉更簡明,作了兩個修改。情況可以是逗號分隔的列表,而fall-throuth也不再是默認的行爲。

因此,如下的C代碼:

  1. int result;
  2. switch (byte) {
  3. case 'a':
  4. case 'b':
  5.    {
  6.      result = 1
  7.      break
  8.    }

  9. default:
  10.    result = 0
  11. }
複製代碼

在Go裏就變成了這樣:

  1. var result int
  2. switch byte {
  3. case 'a', 'b':
  4.   result = 1
  5. default:
  6.   result = 0
  7. }
複製代碼

第二點,Go的分支跳轉可以匹配比整數和字符更多的內容,任何有效的表達式都可以作爲跳轉語句值。只要它與分支條件的類型是一樣的。

因此如下的C代碼:

  1. int result = calculate();
  2. if (result < 0) {
  3.   /* negative */
  4. } else if (result > 0) {
  5.   /* positive */
  6. } else {
  7.   /* zero */
  8. }
複製代碼

在Go裏可以這樣表達:

  1. switch result := calculate(); true {
  2. case result < 0:
  3.   /* negative */
  4. case result > 0:
  5.   /* positive */
  6. default:
  7.   /* zero */
  8. }
複製代碼

這些都是公共的約定俗成,比如如果分支值省略了,就是默認爲真,所以上面的代碼可以這樣寫:

  1. switch result := calculate(); {
  2. case result < 0:
  3.   /* negative */
  4. case result > 0:
  5.   /* positive */
  6. default:
  7.   /* zero */
  8. }
複製代碼


循環




Go只有一個關鍵字用於引入循環。但它提供了除do-while外C語言當中所有可用的循環方式。

條件
  1. for a > b { /* ... */ }
複製代碼

初始,條件和步進
[code=java]for i := 0; i < 10; i++ { /* ... */ }[/code]

範圍




range語句右邊的表達式必須是array,slice,string或者map, 或是指向array的指針,也可以是channel。

  1. for i := range "hello" { /* ... */ }
複製代碼

無限循環
for { /* ever */ }

函數




聲明函數的語法與C不同。就像變量聲明一樣,類型是在它們所描述的術語之後聲明的。在C語言中:

  1. int add(int a, b) { return a + b }
複製代碼

在Go裏面是這樣描述的:

  1. func add(a, b int) int { return a + b }
複製代碼


多返回值




在C語言當中常見的做法是保留一個返回值來表示錯誤(比如,read()返回 0),或 者保留返回值來通知狀態,並將傳遞存儲結果的內存地址的指針。這容易產生了不安全的編程實踐,因此在像Go語言這樣有良好管理的語言中是不可行的。

認識到這一問題的影響已超出了函數結果與錯誤通訊的簡單需求的範疇,Go的作者們在語言中內建了函數返回多個值的能力。

作爲例子,這個函數將返回整數除法的兩個部分:

  1. func divide(a, b int) (int, int) {
  2.   quotient := a / b
  3.   remainder := a % b
  4.   return quotient, remainder
  5. }
複製代碼

有了多個返回值,有良好的代碼文檔會更好——而Go允許你給返回值命名,就像參數一樣。你可以對這些返回的變量賦值,就像其它的變量一樣。所以我們 可以重寫divide:

  1. func divide(a, b int) (quotient, remainder int) {
  2.   quotient = a / b
  3.   remainder = a % b
  4.   return
  5. }
複製代碼

多返回值的出現促進了"comma-ok"的模式。有可能失敗的函數可以返回第二個布爾結果來表示成功。作爲替代,也可以返回一個錯誤對象,因此像 下面這樣的代碼也就不見怪了:

  1. if result, ok := moreMagic(); ok {
  2.   /* Do something with result */
  3. }
複製代碼


匿名函數




有了垃圾收集器意味着爲許多不同的特性敞開了大門——其中就包括匿名函數。Go爲聲明匿名函數提供了簡單的語法。像許多動態語言一樣,這些函數在它 們被定義的範圍內創建了詞法閉包。

考慮如下的程序:

  1. func makeAdder(x int) (func(int) int) {
  2.   return func(y int) int { return x + y }
  3. }
複製代碼

  1. func main() {
  2.   add5 := makeAdder(5)
  3.   add36 := makeAdder(36)
  4.   fmt.Println("The answer:", add5(add36(1))) //=> The answer: 42
  5. }
複製代碼


基本類型




像C語言一樣,Go提供了一系列的基本類型,常見的布爾,整數和浮點數類型都具備。它有一個Unicode的字符串類型和數組類型。同時該語言還引 入了兩 種新的類型:slice 和map。
Go語言的基本類型包括:
  1. bool
  2. string
  3. int  int8  int16  int32  int64
  4. uint uint8 uint16 uint32 uint64 uintptr
  5. byte // alias for uint8
  6. rune // alias for int32
  7.      // represents a Unicode code point
  8. float32 float64
  9. complex64 complex128
複製代碼


數組和切片




Go語言當中的數組不是像C語言那樣動態的。它們的大小是類型的一部分,在編譯時就決定了。數組的索引還是使用的熟悉的C語法(如 a[ i ]),並且與C一樣,索引是由0開始的。編譯器提供了內建的功能在編譯時求得一個數組的長度 (如 len(a))。如果試圖超過數組界限寫入,會產生一個運行時錯誤。

Go還提供了切片(slices),作爲數組的變形。一個切片(slice)表示一個數組內的連續分段,支持程序員指定底層存儲的明確部分。構建一 個切片 的語法與訪問一個數組元素類似:

  1. /* Construct a slice on ary that starts at s and is len elements long */
  2. s1 := ary[s:len]

  3. /* Omit the length to create a slice to the end of ary */
  4. s2 := ary[ s:]

  5. /* Slices behave just like arrays */
  6. s[0] == ary[ s ] //=> true

  7. // Changing the value in a slice changes it in the array
  8. ary[ s] = 1
  9. s[0] = 42
  10. ary[ s ] == 42 //=> true
複製代碼

該切片所引用的數組分段可以通過將新的切片賦值給同一變量來更改:

  1. /* Move the start of the slice forward by one, but do not move the end */
  2. s2 = s2[1:]

  3. /* Slices can only move forward */
  4. s2 = s2[-1:] // this is a compile error
複製代碼

切片的長度可以更改,只要不超出切片的容量。切片s的容量是數組 從s[0]到數組尾端的大小,並由內建的cap()函數返回。一個切片的長度永遠不能超出它的容量。

這裏有一個展示長度和容量交互的例子:

  1. a := [...]int{1,2,3,4,5} // The ... means "whatever length the initializer has"
  2. len(a) //=> 5

  3. /* Slice from the middle */
  4. s := a[2:4] //=> [3 4]
  5. len(s), cap(s) //=> 2, 3

  6. /* Grow the slice */
  7. s = s[0:3] //=> [3 4 5]
  8. len(s), cap(s) //=> 3, 3

  9. /* Cannot grow it past its capacity */
  10. s = s[0:4] // this is a compile error
複製代碼

通常,一個切片就是一個程序所需要的全部了,在這種情況下,程序員根本用不着一個數組,Go有兩種方式直接創建切片而不用引用底層存儲:

  1. /* literal */
  2. s1 := []int{1,2,3,4,5}

  3. /* empty (all zero values) */
  4. s2 := make([]int, 10) // cap(s2) == len(s2) == 10
複製代碼


Map類型




幾乎每個現在流行的動態語言都有的數據類型,但在C中不具備的,就是dictionary。Go提供了一個基本的dictionary類型叫做 map。下 面的例子展示瞭如何創建和使用Go map:

  1. m := make(map[string] int) // A mapping of strings to ints

  2. /* Store some values */
  3. m["foo"] = 42
  4. m["bar"] = 30

  5. /* Read, and exit program with a runtime error if key is not present. */
  6. x := m["foo"]

  7. /* Read, with comma-ok check; ok will be false if key was not present. */
  8. x, ok := m["bar"]

  9. /* Check for presence of key, _ means "I don't care about this value." */
  10. _, ok := m["baz"] // ok == false

  11. /* Assign zero as a valid value */
  12. m["foo"] = 0;
  13. _, ok := m["foo"] // ok == true

  14. /* Delete a key */
  15. m["bar"] = 0, false
  16. _, ok := m["bar"] // ok == false
複製代碼


面向對象




Go語言支持類似於C語言中使用的面向對象風格。數據被組織成structs,然後定義操作這些structs的函數。類似於Python,Go語言提供了定義函數並調用它們的方式,因此語法並不會笨拙。

Struct類型




定義一個新的struct類型很簡單:

  1. type Point struct {
  2.   x, y float64
  3. }
複製代碼

現在這一類型的值可以通過內建的函數new來分配,這將返回一個指針,指向一塊 內存單元,其所佔內存槽初始化爲零。

[code=java]var p *Point = new(Point)
p.x = 3
p.y = 4[/code]
這顯得很冗長,而Go語言的一個目標是儘可能的簡明扼要。所以提供了一個同時分配和初始化struct的語法:

[code=java]var p1 Point = Point{3,4}  // Value
var p2 *Point = &Point{3,4} // Pointer[/code]

方法




一旦聲明瞭類型,就可以將該類型顯式的作爲第一個參數來聲明函數:

[code=java]func (self Point) Length() float {
  return math.Sqrt(self.x*self.x + self.y*self.y);
}[/code]
這些函數之後可作爲struct的方法而被調用:

  1. p := Point{3,4}
  2. d := p.Length() //=> 5
複製代碼

方法實際上既可以聲明爲值也可以聲明爲指針類型。Go將會適當的處理引用或解引用對象,所以既可以對類型T,也可以對類型*T聲明方式,併合理地使用它們。

讓我們爲Point擴展一個變換器:

  1. /* Note the receiver is *Point */
  2. func (self *Point) Scale(factor float64) {
  3.   self.x = self.x * factor
  4.   self.y = self.y * factor
  5. }
複製代碼

然後我們可以像這樣調用:

  1. p.Scale(2);
  2. d = p.Length() //=> 10
複製代碼

很重要的一點是理解傳遞給MoveToXY的self和其它的參數一樣,並且是值傳遞,而不是引用傳遞。如果它被聲明爲Point,那麼在方法內修改的struct就不再跟調用方的一樣——值在它們傳遞給方法的時候被 拷貝,並在調用結束後被丟棄。

接口




像Ruby這樣的動態語言所強調面向對象編程的風格認爲對象的行爲比哪種對象是動態類型(duck typing)更爲重要。Go所帶來的一個最強大的特性之一就是提供了可以在編程時運用動態類型的思想而把行爲定義的合法性檢查的工作推到編譯時。這一行爲的名字被稱作接口。

定義一個接口很簡單:

  1. type Writer interface {
  2.   Write(p []byte) (n int, err os.Error)
  3. }
複製代碼

這裏定義了一個接口和一個寫字節緩衝的方法。任何實現了這一方法的對象也實現了這一接口。不需要像Java一樣進行聲明,編譯器能推斷出來。這既給予了動態類型的表達能力又保留了靜態類型檢查的安全。

Go當中接口的運作方式支持開發者在編寫程序的時候發現程序的類型。如果幾個對象間存在公共行爲,而開發者想要抽象這種行爲,那麼它就可以創建一個接口並使用它。

考慮如下的代碼:

  1. // Somewhere in some code:
  2. type Widget struct {}
  3. func (Widget) Frob() { /* do something */ }

  4. // Somewhere else in the code:
  5. type Sprocket struct {}
  6. func (Sprocket) Frob() { /* do something else */ }

  7. /* New code, and we want to take both Widgets and Sprockets and Frob them */
  8. type Frobber interface {
  9.   Frob()
  10. }

  11. func frobtastic(f Frobber) { f.Frob() }
複製代碼

需要特別指出的很重要的一點就是所有的對象都實現了這個空接口:

interface {}

繼承




Go語言不支持繼承,至少與大多數語言的繼承不一樣。並不存在類型的層次結構。相較於繼承,Go鼓勵使用組合和委派,併爲此提供了相應的語法甜點使 其更容易接受。

有了這樣的定義:

  1. type Engine interface {
  2.   Start()
  3.   Stop()
  4. }

  5. type Car struct {
  6.   Engine
  7. }
複製代碼

於是我可以像下面這樣編寫:

  1. func GoToWorkIn(c Car) {
  2.   /* get in car */

  3.   c.Start();

  4.   /* drive to work */

  5.   c.Stop();

  6.   /* get out of car */
  7. }
複製代碼

當我聲明Car這個struct的時候,我定義了一個匿名成員。 這是一 個只能被其類型識別的成員。匿名成員與其它的成員一樣,並有着和類型一樣的名字。因此我還可以寫成c.Engine.Start()。 如果Car並沒有其自身方法可以滿足調用的話,編譯器自動的會將在Car上的調用委派給它的Engine上面的方法。

由匿名成員提供的分離方法的規則是保守的。如果爲一個類型定義了一個方法,就使用它。如果不是,就使用爲匿名成員定義的方法。如果有兩個匿名成員都提供一個方法,編譯器將會報錯,但只在該方法被調用的情況下。

這種組合是通過委派來實現的,而不是繼承。一旦匿名成員的方法被調用,控制流整個都被委派給了該方法。所以你無法做到和下面的例子一樣來模擬類型層次:

  1. type Base struct {}
  2. func (Base) Magic() { fmt.Print("base magic") }
  3. func (self Base) MoreMagic() {
  4.   self.Magic()
  5.   self.Magic()
  6. }

  7. type Foo struct {
  8.   Base
  9. }
  10. func (Foo) Magic() { fmt.Print("foo magic") }
複製代碼

當你創建一個Foo對象時,它將會影響Base的兩個方法。然而,當你調用MoreMagic時, 你將得不到期望的結果:
f := new(Foo)
f.Magic() //=> foo magic
f.MoreMagic() //=> base magic base magic

併發




Go的作者選擇了消息傳遞模型來作爲推薦的併發編程方法。該語言同樣支持共享內存,然而作者自有道理:

不要通過共享內存來通信,相反,通過通信來共享內存。
該語言提供了兩個基本的構件來支持這一範型:goroutines和channels。

Go例程




Goroutine是輕量級的並行程序執行路徑,與線程,coroutine或者進程類似。然而,它們彼此相當不同,因此Go作者決定給它一個新的名字並放棄其它術語可能隱含的意義。

創建一個goroutine來運行名爲DoThis的函數十分簡單:

  1. go DoThis() // but do not wait for it to complete
複製代碼

匿名的函數可以這樣使用:

  1. go func() {
  2.   for { /* do something forever */ }
  3. }() // Note that the function must be invoked
複製代碼

這些goroutine將會通過Go運行時而映射到適當的操作系統原語(比如,POSIX線程)。

通道類型




有了goroutine,代碼的並行執行就容易了。然而,它們之間仍然需要通訊機制。Channel提供一個FIFO通信隊列剛好能達到這一目的。

以下是使用channel的語法:

  1. /* Creating a channel uses make(), not new - it was also used for map creation */
  2. ch := make(chan int)

  3. /* Sending a value blocks until the value is read */
  4. ch <- 4

  5. /* Reading a value blocks until a value is available */
  6. i := <-ch
複製代碼

舉例來說,如果我們想要進行長時間運行的數值計算,我們可以這樣做:

  1. ch := make(chan int)
  2. go func() {
  3.   result := 0
  4.   for i := 0; i < 100000000; i++ {
  5.     result = result + i
  6.   }
  7.   ch <- result
  8. }()

  9. /* Do something for a while */

  10. sum := <-ch // This will block if the calculation is not done yet
  11. fmt.Println("The sum is:", sum)
複製代碼


channel的阻塞行爲並非永遠是最佳的。該語言提供了兩種對其進行定製的方式:

程序員可以指定緩衝大小——想緩衝的channel發送消息不會阻塞,除非緩衝已滿,同樣從緩衝的channel讀取也不會阻塞,除非緩衝是空的。
該語言同時還提供了不會被阻塞的發送和接收的能力,而操作成功是仍然要報告。
  1. /* Create a channel with buffer size 5 */
  2. ch := make(chan int, 5)

  3. /* Send without blocking, ok will be true if value was buffered */
  4. ok := ch <- 42

  5. /* Read without blocking, ok will be true if a value was read */
  6. val, ok := <-ch
複製代碼





Go提供了一種簡單的機制來組織代碼:包。每個文件開頭都會聲明它屬於哪一個包,每個文件也可以引入它所用到的包。任何首字母大寫的名字是由包導出 的,並可以被其它的包所使用。

以下是一個完整的源文件:

  1. package geometry

  2. import "math"

  3. /* Point is capitalized, so it is visible outside the package. */

  4. type Point struct {

  5.   /* the fields are not capitalized, so they are not visible
  6.      outside of the package */

  7.   x, y float64
  8. }

  9. /* These functions are visible outside of the package */

  10. func (self Point) Length() float64 {
  11.   /* This uses a function in the math package */
  12.   return math.Sqrt(self.x*self.x + self.y*self.y)
  13. }

  14. func (self *Point) Scale(factor float64) {
  15.   self.setX(self.x * factor)
  16.   self.setY(self.y * factor)
  17. }

  18. /* These functions are not visible outside of the package, but can be
  19.    used inside the package */

  20. func (self *Point) setX(x float64) { self.x = x }
  21. func (self *Point) setY(y float64) { self.y = y }
複製代碼


缺失




Go語言的作者試圖將代碼的清晰明確作爲設計該語言作出所有決定的指導思想。第二個目標是生產一個編譯速度很快的語言。有了這兩個標準作爲方向,來自其它語言的許多特性就不那麼適合了。許多程序員會發現他們最愛的語言特性在Go當中不存在,確實,有很多人也許會覺得Go語言由於缺乏其它語言所共有的 一些特性,還不太可用。

這當中兩個缺失的特性就是異常和泛型,兩者在其它語言當中都是非常有用的。而它們目前都不是Go的一分子。但因爲該語言仍處於試驗階段,它們有可能最終會加入到語言裏。然而,如果將Go與其它語言作比較的話,

我們應當記住Go是打算在系統編程層面作爲C語言的替代

。明白這一點的話,那麼缺失的這許多特性倒也不是很大的問題了。

最後,因爲這一語言纔剛剛發佈,因此它沒有什麼類庫或工具可以用,也沒有Go語 言的集成編程環境。Go語言標準庫有些有用的代碼,但這與更爲成熟的語言比 起來仍還是很少的。
發佈了28 篇原創文章 · 獲贊 11 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章