那些在Google的大牛們開發出了一種稱為Go的牛叉的語言。乍一看,Ruby和Go有點像遠房表親。其實不然,他們那些互為補充的功能卻讓他們成為一對完美組合。
Ruby程序員花時間了解一下Go還是非常有好處的,因為Go語言中一些創新之舉還是很有不錯的。
對於我來說,Go彌補了C++和Ruby之間空缺的聯系。特別是當需要實現高響應的服務器的時候,我通常會選擇C++,但是這樣我就丟失了Ruby的精細之處。雖然我比較偏愛Ruby,可是即便是最近,當需要性能有明顯提高的時,Ruby還是應付不來。
Go彌補了這個空缺。它提供像Ruby和Python這樣動態語言的感覺的同時,也提供了編譯語言的性能。
Go同時有一些與眾不同的特性,本文會詳細介紹。好了,讓我們好好瞧瞧吧。
當寫一個服務器的時候,一種實現並發的方式是為每個客戶端開一個線程(你可能會覺得是在扯淡,好吧!沒關系,繼續讀下去),特別是有許多客戶端的時候,這種方式是非常糟糕的。較為好的解決方法是選擇非阻塞IO(大家肯定表示贊同吧)。可是,即便都是Unix系的操作系統(諸如Linux,Mac OS X等等),有效地處理非阻塞IO的機制也是各不相同。此外,除了這些紛繁混雜,還有個C語言。我絕不反對嵌入式設備使用C語言,因為那絕對是速度第一,開發時間第二的。但是,作為一門日常語言,C已經不能滿足我的需求了。
Go提供了令人驚訝的並發基元(primitives),良好的語法,優秀的函數庫和快速的編譯器。它解決我在使用C(某種程度上C++也是)遇到的問題。即使是基礎代碼變得很大的時候,使用Go語言依然很輕松。
在這篇文章中,我會依據文檔,快速的回顧一下Go語言的基礎特性。我們的重點在於突出那些讓Go語言與眾不同的創新之舉。
Go語言是很容易上手,在基本語法這方面沒玩什麼新花樣。下面是些基本代碼:
package main
func main() {
}
我們從main函數開始。好了,試著輸出個“Hello,world”吧!
package main
import "fmt"
func main() {
fmt.Println("Hello, world!")
}
Go語言中輸入輸出模塊被稱作“fmt”,不像Ruby,這個“fmt”是默認不被包含的。所以需要在文件開始處用“import”聲明引入。“fmt”模塊中的Println函數會將你傳入的字符串加上一個換行符一起輸出(類似ruby的puts函數)。注意Go語言中公共方法是以大寫字母開頭的。
下面看一下簡單的循環:
package main
import "fmt"
func main() {
//the basic for loop
for i:=1; i < 100; i++ {
fmt.Println(i)
}
}
對於for循環,Go語言和Ruby完全不同。Go語言的for循環或多或少有點像C語言。你需要先定義個變量,然後檢查狀態,最後說明在迭代一次結束後需要做什麼事(這個例子是i遞增)。Go語言中的基本循環語法只有這一種。幸運的是,這個for循環非常靈活。比如說,下面這個死循環:
for {
}
我希望你能查看一些有個for的文檔[http://golang.org/doc/effective_go.html#for].
請注意在我們的上面的for循環中,給變量i賦值的時候,我們沒有用“=”,而是使用了“:=”。這兒有個說明差異的例子:
package main
import "fmt"
func main() {
//defines the variable a
a := 5
fmt.Println(a)
//sets a different value to a
a = 10
fmt.Println(a)
//another way to define a variable
var b int
b = 15
fmt.Println(b)
}
在main函數的開始,在聲明變量a的同時進行了初始化,所以使用“:="。接下來的是簡單的賦值,所以使用“=”。之所以這樣,是因為實際上Go語言是靜態類型語言,不像Ruby這樣的動態類型。因此編譯器必須得知道這個變量在哪聲明和在哪賦值的。最後一部分代碼比較清楚,就是簡單地使用var關鍵字聲明變量,然後進行賦值。
最後,作為和Ruby中數組的一個相似點,在Go語言中的數組也有分片。下面的代碼中有個[]type的類型,這個type意思是著你希望分片返回的類型。但是這樣的做法有點變扭 :
package main
func main {
///this creates a slice of integers with length 15
mySlice := make([]int, 15)
}
我們需要make()函數來獲得一個分片。
如果這樣繼續下去的話,文章就可能成為Go語言語法的的簡明教程。而我更希望將時間花費在一些有意思的新特性上,而不是這樣的一個語法介紹。基本語法可以參照Go語言的文檔,那會介紹得更好。
下面讓我們看看goroutines吧。
寫並發的代碼已經很困難了,寫並發訪問網絡的代碼就更加困難了。問題在於傳統的線程不能很好得伸縮,而且線程一旦運行起來,就會很難去控制。Go語言項目組著手解決這個問題,於是乎goroutine就誕生了。
本質上, goroutines是個輕量級的並發機制,通過使用一種稱為channels的構建來進行線程間交互。它們都非常易於使用:
package main
import "fmt"
func wait() {
//wait around with a forever loop
for {
}
}
func main() {
go wait()
fmt.Println("We didn't wait because it was called as a goroutine!")
}
在上面的代碼中,wait方法是一個死循環,但是我們通過go wait()的方式來調用,而非直接的通過wait()來調用。這是告訴Go我們希望以一個goroutine的方式來調用,同時異步運行。既然這個循環是在後台運行的,那樣運行這個程序就不會因為死循環而阻塞。
這麼說,Go從語言本身支持並發。也就是,Go語言中有並發基元(primitives)。這樣意義何在呢?僅僅因為不是由某個庫或者模塊來實現並發,這好像不是什麼了不起的舉措啊。但是,實際上goroutine從根本上與線程不同。goroutine更加輕量化。還記得在服務器中,我們不該為每個客戶端創建一個線程吧?但是,使用goroutine,情況就不同了:
package main
import (
"fmt"
"net"
)
//notice that in the arguments, the name of
//the variable comes first, then comes the
//type of the variable, just like in "var"
//declarations
func manageClient(conn net.Conn) {
conn.Write([]byte("Hi!"))
conn.Close()
//do something with the client
}
func main() {
//we are creating a server her that listens
//on port 1337. Notice that, similar to Ruby,
//a method can have two return values (although
//in Ruby, this would be an array instead)
listener, err := net.Listen("tcp", ":1337")
for {
//accept a connection
connection, _ := listener.Accept()
go manageClient(connection)
}
}
噢,等會!這些代碼似有那麼一小點復雜啊,雖然想法是很簡單。好吧,讓我們一步一步慢慢來
首先,我們來看一下main函數。在main函數一開始調用了net.Listen方法,該方法會返回兩個值,一個是服務器連接,另一個是錯誤消息。然後,進入到服務的主循環部分,在這兒程序調用server.Accept方法,然後等待請求。該方法調用後,程序會被掛起,直到有有一個客戶端的連接出現。一旦有個連接出現,我們將connection對象傳值到manageClient方法中,由於通過goroutine的方式調用manageClient,所以主程序會繼續等待處理下一個客戶端連接請求。
最後,關於這個manageClient方法要注意一下。首先,注意一下參數表,是變量名在先,類型在後。這樣的格式多少是由Go語言創造者決定的。你可能甚至可能一周後都沒有注意到。
在方法體中,向客戶端寫入“Hi!”信息,然後關閉套接字。
好了,就這麼幾行代碼,我們輕松完成了一個基礎服務器。你可以將它改成一個HTTP代理(如果加上緩存,那就更棒了)。Goroutines支持我們這麼做。事實上goroutine不單單是一個輕量級的線程,因為還有許多與眾不同的機制在背後在起著作用,所以才可以通過如此簡練的代碼的來實現goroutine功能。
雖然,單純只有Goroutines已經很有作用了,但是如果在channels概念的支持下,那麼Goroutines將更具威力。Channels是一種goroutine之間或者goroutine和主進程之間的通信機制。讓我們來看個簡單的實例。
package main
import (
"fmt"
)
var eventChannel chan int = make(chan int)
func sayHello() {
fmt.Println("Hello, world!")
//pass a message through the eventChannel
//it doesn't matter *what* we actually send across
eventChannel < - 1
}
func main() {
//run a goroutine that says hello
go sayHello()
//read the eventChannel
//this call blocks so it waits until sayHello()
//is done
<- eventChannel
}
程序中有個調用了sayHellothat方法的goroutine,該方法輸出 “Hello, world”消息。但是,注意那個eventChannel的聲明。本質上,我們聲明了一個整型的channel。我們可以通過這個channel來發送數據,而其他部分可以從這個channel中讀取數據。這就使得channel成為了一種通信方式。在 sayHello方法中,eventChannel < - 1將整數1加入到eventChannel中,然後在主函數中,我們可以從 eventChannel將數據讀出。
這兒有一點很重要:默認情況下,如果channel中沒有數據的情況下,從channel中讀數據會被阻塞的,一直阻塞到可以從channel中讀到數據。
來的稍微復雜的:
package main
import (
"fmt"
)
var logChannel chan string = make(chan string)
func loggingLoop() {
for {
//wait for a message to arrive
msg := < - logChannel
//log the msg
fmt.Println(msg)
}
}
func main() {
go loggingLoop()
//do some stuff here
logChannel <- "messaged to be logged"
//do other stuff here
}
這裡,我們完成了一個main的事件輪詢,它會一直處於監聽事件狀態,也就是loggingLoop函數。它從loggChanne中接收到一個消息後,就會輸到屏幕。這是一個非常普片的設計,特別在事件輪詢中獲得一些狀態。
就這樣,短短幾行代碼,我們就完成了一個main函數和goroutines之間的通信。由於共享內存的通信方式,存在著諸如互斥鎖,競態條件等問題,早已成為了開發者的噩夢。但是在Go中,channels的概念解決了多數傳統問題。此外,Go的channels是語言的固有部分,而非附加在某個庫中的。
與Ruby相比,Go的goroutines實際上是運行在後台,並且由語言本身實現的(MRI Ruby整個運行在一個單獨的線程中,所以它不能提供一個真實的並行)。此外,雖然Ruby自帶線程實現,但是那實在不好使用。事實上,Agent庫嘗試將一些goroutines精妙的地方引入Ruby中去。
這篇文章我們已經講了不少東西了,首先介紹了一些非常基礎的語法,然後直接介紹了Go語言的並發機制。
請繼續關注後續的第2部分,那裡我們會接觸一些復雜語法,和其他一些Go語言帶給我們的牛叉特性。