Go for Ruby developers(譯)
如果你是一個 Ruby 開發者, 並且想要開始使用 Go, 這篇文章可以幫助你入門. 在這篇文章裡, 我們會以 Ruby 的視角一探 Go 的那些很棒的特性.
Ruby is a dynamic, interpreted, reflective, object-oriented, general-purpose programming language. It was designed and developed in the mid-1990s by Yukihiro “Matz” Matsumoto in Japan.According to the creator, Ruby was influenced by Perl, Smalltalk, Eiffel, Ada, and Lisp. It supports multiple programming paradigms, including functional, object-oriented, and imperative. It also has a dynamic type system and automatic memory management. Go (often referred to as Golang) is a programming language designed by Google engineers Robert Griesemer, Rob Pike, and Ken Thompson. Go is a statically typed, compiled language in the tradition of C, with the added benefits of memory safety, garbage collection, structural typing, and CSP-style concurrency. The compiler, tools, and source code are all free and open source.— Wikipedia
我們先從國際慣例hello world
開始:
Ruby
puts 'hello world'
Go
package main // similar to require in ruby import "fmt" func main(){ fmt.Println("Hello World!!!") }
Ruby 中我們只用了一行,而 Go 裡寫了一大段. 先別對 Go 失去信心, 我們一起看看這段 Go 程式碼都做了什麼.
在 Go 語言裡面, 關鍵字 package 定義了程式碼與引入庫的作用域. 在上面這個例子裡, 我們用 package main 告訴 Go 編譯器這段程式碼是可執行的. 所有可執行的 Go 程式都必須要有 package main .
關鍵字 import 與 Ruby 裡的 require 有些像, 用來引入外部庫. 這裡我們引入了 fmt 庫, 用來對輸入輸出做格式化.
main 函式是程式的執行入口. 所有在 Go 裡面執行的程式碼, 都會進入 main 函式中. 在 Go 中, 可執行的程式碼被包在一對大括號之間. 並且, 左大括號必須與塊條件放在一行, 比如函式, 分支或迴圈條件等.
命令列中執行程式碼
Ruby
把如下這段 Ruby 程式碼複製到 hello_world.rb 檔案中. ".rb" 是 Ruby 原始碼檔案的副檔名. 我們可以用 ruby <filename> 執行
> ruby hello_world.rbHello World!!!
Go
把如下這段 Go 程式碼複製到 hello_world.go 檔案中. ".go" 是 Go 原始碼檔案的副檔名. 我們有兩種方式執行下面這段程式碼.
> go build hello-world.go> ./hello-worldHello World!!!
或者
> go run hello-world.goHello World!!!
第一種方式, 先使用 build 命令編譯了原始碼檔案同時生成了一個可執行的二進位制檔案然後執行. 第二種方式, 我們直接使用 go run 命令執行原始碼. 這個命令實際上是在後臺編譯出了可執行檔案並執行.
程式碼註釋
Ruby 裡使用 # 註釋程式碼. Go 中, 單行程式碼註釋使用 // , 多行註釋使用 /*..... */ .
Ruby
puts "Commenting Code in Ruby" # this is single line comment in ruby =begin This is multiline Comment in ruby =end
Go
package main import "fmt" func main(){ fmt.Println("Commenting in Go") // this is single line comment in Go /* this is multiline Comment in Go */ }
變數
由於 Ruby 是動態型別語音, 所以並不需要定義變數型別. 但是 Go 作為一個靜態型別語言, 就必須在宣告變數的同時定義型別.
Ruby
#Ruby a = 1
Go
var a int = 1 // OR // this dynamically declares type for variable a as int. var a = 1 // this dynamically defines variable a and declares its type as int. a := 1
在這個例子中可以看到, 儘管 Go 是個靜態型別語言, 但由於型別推導自動定義了型別, 寫起來可以像動態語言一樣爽.
資料型別
這是 Go 的一些型別
var a bool = true var b int = 1 // In go the string should be declared using double quote. // Using single quote unlike in Ruby can be used only for single character for its byte representation var c string = "hello world" var d float32 = 1.222 var x complex128 = cmplx.Sqrt(-5 + 12i)
在 Ruby 裡面, 我們可以給一個變數賦值兩個不同型別的值, 但在 Go 裡就不行.
a = 1 a = "Hello"
a := 1 a = "Hello" // Gives error: cannot use "hello"(type string) as type int is assignment
Hash/Map(對映)
與 Ruby 一樣, Go 中我們也可以定義 hash 物件, 但 Go 裡面叫 map. map 的語法是 map[string] int, 中括號內用於指定 key 的型別, 而右邊這個用於指定 value 的型別.
#Ruby: hash = {name:'Nikita', lastname: 'Acharya'} # Access data assigned to name key name = hash[:name]
//Go hash := map[string]string{"name":"Nikita", "lastname": "Acharya"} // Access data assigned to name key name := hash["name"]
檢查一個 key 是否在 map 中
我們可以通過 多重賦值的方式檢查一個 key 是否存在. 在下面這個例子裡, 如果 name 物件確實有 last_name 這個 key , 那麼 ok 變數將是 true 且 lastname 變數會賦值為 last_name 的值, 反之 ok 會是 false.
//GO name := map[string]string{name: "Nikita"} lastname, ok := name["lastName"] if ok{ fmt.Printf("Last Name: %v", lastname) }else{ fmt.Println("Last Name is missing") }
Array(陣列)
跟 Ruby 一樣, 我們也有陣列. 但在 Go 裡面, 我們需要在宣告的時候定義陣列長度.
#Ruby array = [1,2,3]
array := [3]int{1,2,3} names := [2]string{"Nikita", "Aneeta"}
Slice(切片)
陣列有個限制是不能在執行時被修改. 也沒有提供巢狀陣列的能力. 對於這個問題, Go 有個資料型別叫 slices (切片). 切片有序的儲存元素, 並且可以隨時被修改. 切片的定義與陣列類似, 但不用宣告長度.
var b []int
好, 接下來我們通過一些酷炫的玩法來對比一下 Ruby
給陣列新增新元素
在 Ruby 中, 我們使用 + 給陣列新增新元素. 在 Go 中 , 我們使用 append 函式.
#Ruby numbers = [1, 2] a= numbers + [3, 4] #-> [1, 2, 3, 4]
// Go numbers := []int{1,2} numbers = append(numbers, 3, 4) //->[1 2 3 4]
擷取子陣列
#Ruby number2 = [1, 2, 3, 4] slice1 = number2[2..-1] # -> [3, 4] slice2 = number2[0..2] # -> [1, 2, 3] slice3 = number2[1..3] # -> [2, 3, 4]
//Go // initialize a slice with 4 len, and values number2 = []int{1,2,3,4} fmt.Println(numbers) // -> [1 2 3 4] // create sub slices slice1 := number2[2:] fmt.Println(slice1) // -> [3 4] slice2 := number2[:3] fmt.Println(slice2) // -> [1 2 3] slice3 := number2[1:4] fmt.Println(slice3) // -> [2 3 4]
複製陣列(切片)
在 Ruby 中, 我們可以通過把陣列賦值給另一個變數的方式直接複製陣列. 在 Go 中, 我們不能直接賦值. 首先, 初始化一個與目標陣列長度一致的陣列, 然後使用 copy 方法.
#Ruby array1 = [1, 2, 3, 4] array2 = array2 # -> [1, 2, 3, 4]
//Go // Make a copy of array1 array1 := []int{1,2,3,4} array2 := make(int[],4) copy(array2, array1)
注意: 複製陣列的元素數量取決於目標陣列定義的長度:
a := []int{1,2,3,4} b := make([]int, 2) copy(b, a) // copy a to b fmt.Println(b) //=> [1 2]
這裡只有兩個元素被複制了, 因為 b 陣列變數的長度只有 2.
條件判斷
if...else
#Ruby num = 9 if num < 0 puts "#{num} is negative" elsif num > 100 && num < 200 puts "#{num} has 1 digit" else puts "#{num} has multiple digits" end
// go num := 9 if num < 0 { fmt.Printf("%d is negative", num) } else if num < 10 { fmt.Printf("%d has 1 digit", num) } else { fmt.Printf("%d has multiple digits", num) }
如果一個變數的作用域只在 if 塊之內, 那我們可以在 if 條件中給變數賦值, 如下:
if name, ok := address["city"]; ok { // do something }
Switch Case
Go 中 switch case 的執行方式與 Ruby 很類似 (只執行符合條件的 case 子句, 而不會包括之後所有的 case). Go 中我們使用 switch...case 語法, 在 Ruby 中是 case...when.
#Ruby def printNumberString(i) case i when 1 puts "one" when 2 puts "two" else puts "none" end end printNumberString(1) printNumberString(2)
//Go func printNumberString(i int) { switch i { case 1: fmt.Println("one") case 2: fmt.Println("two") default: fmt.Println("none") } } func main(){ printNumberString(1) //=> one printNumberString(2) //=> two }
Type switches
用於匹配變數型別的 switch case 語法.
switch v := i.(type) { case int: fmt.Printf("Twice %v is %v\n", v, v*2) case string: fmt.Printf("%q is %v bytes long\n", v, len(v)) default: fmt.Printf("I don't know about type %T!\n", v) }
迴圈
在 Go 中, 我們只有 loop. 但是, 我們可以用 loop 實現 Ruby 中支援的各種不同的迴圈. 我們來看一下:
基本迴圈
//Go sum := 0 for i := 0; i < 10; i++ { sum += i } fmt.Println(sum)
While 迴圈
Go 中沒有 while 關鍵字. 但可以用下面這種方式實現 while.
#Ruby sum = 1 while sum < 1000 sum += sum end puts sum
//Go sum := 1 for sum < 1000 { sum += sum } fmt.Println(sum)
無限迴圈
#Ruby while true #do something end
//Go for { // do something }
Each 迴圈
Go 中, 我們可以使用 loop with range 的方式遍歷對映或陣列. 類似 Ruby 中的 each.
#Ruby {name: 'Nikita', lastname: 'Acharya'}.each do |k, v| puts k, v end
// Go kvs := map[string]string{"name": "Nikita", "lastname": "Acharya"} for k, v := range kvs { fmt.Printf("%s -> %s\n", k, v) }
Each with index
#Ruby ["a", "b", "c", "d", "e"].each_with_index do |value, index| puts "#{index} #{value}" end
// Go arr := []string{"a", "b", "c", "d", "e"} for index, value := range arr { fmt.Println(index, value) }
異常處理
Ruby 中, 我們使用 begin rescue 來處理異常.
#Ruby begin #code rescue => e #rescue exception end
在 Go 中, 我們有 Panic Defer 和 Recover 這幾種概念來捕獲與處理異常. 然而, 我們程式碼中是極少需要手動處理 panic 的, 通常由框架或庫來處理.
我們可以看看下面這個例子, 一個 error 是怎麼被 Panic 丟擲, 然後通過 defer 語句中 recover 關鍵字捕獲的. defer 語句會延遲到所在的函式結束之前執行. 所以, 在下面這個例子裡, 即使 panic 導致程式流程中斷, defer 語句仍然會執行. 因此, 將 recover 語句放到 defer 當中可以做出與 Ruby 中的 rescue 類似的效果.
//Go package main import "fmt" func main() { f() fmt.Println("Returned normally from f.") } func f() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered in f", r) } }() fmt.Println("Calling g.") g(0) fmt.Println("Returned normally from g.") } func g(i int) { if i > 3 { fmt.Println("Panicking!") panic(fmt.Sprintf("%v", i)) } defer fmt.Println("Defer in g", i) fmt.Println("Printing in g", i) g(i + 1) }
值得一提的是, 我們日常寫程式碼通常不會直接使用 panic. 我們會使用 error 介面去檢查錯誤, 並且做相應的處理. 如下例子:
// Go func Increment(n int) (int, error) { if n < 0 { // return error object return nil, errors.New("math: cannot process negative number") } return (n + 1), nil } func main() { num := 5 if inc, err := Increment(num); err != nil { fmt.Printf("Failed Number: %v, error message: %v", num, err) }else { fmt.Printf("Incremented Number: %v", inc) } }
Defer
比較像 Ruby 裡的 ensure.
Defer 通常是用來做確保程式結束前會執行的部分, 比如清理工作. 像關閉檔案, 關閉 HTTP 連線.
Functions
Ruby 中, 與方法的引數型別一樣, 我們也不用指定返回型別. 但在 Go 中, 對函式的引數與返回都必須定義型別. 另外要注意 return 關鍵字在 Go 中是必須要寫的, 不像 Ruby 裡面可有可無. 因為 Ruby 自動 return 方法的最後一行.
# Ruby def multi_return(n) [n/10, n%10] end divisor, remainder = multi_return(5)
// Go func MultiReturn(n int) (int, int) { return (n/10), (n % 10) } divisor, remainder := MultiReturn(5)
Pointer(指標)
在 Ruby 中, 我們沒有指標的概念. 在 Go 中, 我們用指標來持有某個值所在的記憶體地址. 程式碼中體現為 * 操作符. 另一個操作符是 &, 用來獲取某個值的記憶體地址, 也就是生成一個指標. 下面這個例子會展示指標的引用與反引用.
// Go //pointer initialization | 指標初始化 var a *int b := 12 // point to the address of b | 將 a 指向 b 的記憶體地址 a = &b // read the value of b through pointer | 通過指標讀取相應記憶體地址的值 fmt.Println(*a) // => 12 // set value of b through pointer | 通過指標設定相應記憶體地址的值 *a = 5 fmt.Println(b) // => 5
面向物件程式設計
Go 是一種輕量級的面嚮物件語言. 可以使用 struct 提供封裝與型別成員函式, 但是沒有面向物件語常見的整合. 我們可以把一個函式繫結到 struct 上. 然後當我建立 struct 例項後, 就可以擁有訪問例項變數與繫結在其上的方法的能力
# Ruby class Festival def initialize(name, description) @name = name @description = description end def is_dashain? if @name == "Dashain" @description = "Let's fly kites." true else false end end def to_s "#{@name}: #{@description}" end
package main import "fmt" type Festival struct { Name string Description string } // Festival type method func (f *Festival) IsDashain() bool { if f.Name == "Dashain" { f.Description = "Let's fly kites." return true } else { return false } } // Festival type method func (f *Festival) ToString() string { return fmt.Sprintf("%v: %v", f.Name, f.Description) } func main(){ festival := Festival{"Tihar", "Tihar is the festival of lights."} if festival.IsDashain() { fmt.Println("Festival:", festival.ToString()) } else { fmt.Println("Let's celebrate", festival.ToString()) } }
在上面這個例子中, 我們用 struct 定義了一個新型別 Festival. 可以看到, 對於 Festival 我們也定義了兩種函式 IsDashain ToString. 這些函式對於 Festival 例項都是可訪問的. Go, 這類函式稱之為方法 . 分別方法與函式, 可以看它是否依附於某個物件, 在其他語言中大致也是一樣的約定. 另外我們可以看到對於方法定義, 我們是針對 Festival 指標建立的方法. 對於指標接受者, 有兩個用法值得注意.
-
當需要修改方法接受者, 接受者就必須是指標型別. 在上面這個例子中, 當 festival 的 Name 屬性為 Dashain, 這個方法就會修改 Description 的值, 並且也可以被 main 函式訪問.
-
如果引數太大, 可以考慮改為傳入指標的方式以避免可能的效能問題.
注意: Go 的 interface(介面) 可以提供類似 Ruby 中繼承/混合的感覺以及更多 OOP 的靈活性.
公有/私有 方法與變數
Ruby 中, 我們用 private 關鍵字來定義私有方法. 在 Go, 定義私有方法或私有變數, 只需要將名稱首字母小寫就可以了.
#Ruby class User attr_accessor :user_name def initialize(user_name, password) @user_name = user_name @password = password end def get_user_password secret_password end private def secret_password @password end end u = User.new("hello", "password") puts "username: #{u.user_name}" puts "password: #{u.get_user_password}"
// Go type User struct { Username string password string } func (u User) GetPassword() string{ return u.secretPassword() } func (u User) secretPassword() string { return u.password } func main(){ user := User{"username", "password"} fmt.Println("username: " + user.Username) fmt.Println("password: " + user.GetPassword()) }
JSON 序列化/反序列化
Go 中, 我們用 json 的 Marshal 與 Unmarshal 來做 序列號與反序列化.
JSON 序列化
#Ruby hash = {apple: 5, lettuce: 7} #encoding hash to json json_data = hash.to_json puts json_data
// Go package main import ( "encoding/json" "fmt" ) func main() { // Go mapA := map[string]int{"apple": 5, "lettuce": 7} // encoding map type to json strings mapB, _ := json.Marshal(mapA) fmt.Println(string(mapB)) }
JSON 反序列化
#ruby str = `{"page": 1, "fruits": ["apple", "peach"]}` JSON.parse(str)
// Go package main import ( "encoding/json" "fmt" ) // Go type response struct { Pageint`json:"page"` Fruits []string `json:"fruits"` } func main() { str := `{"page": 1, "fruits": ["apple", "peach"]}` res := response{} json.Unmarshal([]byte(str), &res) fmt.Println(res.Page) //=> 1 }
注意: 上面這個例子裡,json:"page"
命令用於將 json 中的 page 對應到 struct 的 Page 屬性.
Package(庫)
Go 的 Package 與 Ruby 中的 Gem 類似. Ruby 中, 我們用gem install <gem name> 安裝一個 gem . Go 中, 是go get <package path> .
我們也有一些內建的基礎庫. 常見的有:
fmt
顧名思義, 這個庫用於做程式碼格式化. 這是一個很好用的庫. 在 Ruby 中, 我們遵循 rubocop 的程式碼規範做程式碼格式化. 但在 Go 中, 我們不用太操心這些事. fmt 庫會幫我們處理. 只需要在寫完程式碼以後, 執行一下gofmt
或go fmt
就好了.
gofmt -w yourcode.go //OR go fmt path/to/your/package
程式碼會自動格式化.
log
這個庫與 Ruby 的 logger 很像. 它定義了型別 Logger, 包含格式化輸出相關的各種方法.
net/http
用於建立 HTTP 請求. 與 Ruby 的 net http 很像.
Go 的併發
不像 Ruby, 我們如果要做併發就需要引入一些額外的併發庫比如celluloid
. Go 自帶了併發能力. 我們只需要在需要併發操作的地方在前面加上 go 關鍵字就好了. 下面這個例子裡, 我們在 main 函式中用 go 關鍵字併發執行了 f("goroutine").
package main import( "fmt" "time" ) func f(from string) { for i := 0; i < 3; i++ { time.Sleep(1 * time.Millisecond) fmt.Println(from, ":", i) } } func main() { // executing the function in another goroutine concurrently with the main. go f("goroutine") // calling synchronously // processing in current main thread f("main") fmt.Println("done") }
[Running] go run "/home/lux/gogo/goroutine_example.go" main : 0 goroutine : 0 goroutine : 1 main : 1 main : 2 done [Done] exited with code=0 in 0.392 seconds [Running] go run "/home/lux/gogo/goroutine_example.go" goroutine : 0 main : 0 goroutine : 1 main : 1 goroutine : 2 main : 2 done [Done] exited with code=0 in 0.181 seconds
還有一個概念稱之為 Channels (管道), 用於兩個 goroutine 之間的通訊.