如果你是一個(gè) Ruby 開(kāi)發(fā)者, 并且想要開(kāi)始使用 Go, 這篇文章可以幫助你入門(mén). 在這篇文章里, 我們會(huì)以 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
我們先從國(guó)際慣例 hello world 開(kāi)始:
Ruby
puts 'hello world'
Go
package main
// similar to require in ruby
import "fmt"
func main(){
fmt.Println("Hello World!!!")
}
Ruby 中我們只用了一行,而 Go 里寫(xiě)了一大段. 先別對(duì) Go 失去信心, 我們一起看看這段 Go 代碼都做了什么.
在 Go 語(yǔ)言里面, 關(guān)鍵字 package 定義了代碼與引入庫(kù)的作用域. 在上面這個(gè)例子里, 我們用 package main 告訴 Go 編譯器這段代碼是可執(zhí)行的. 所有可執(zhí)行的 Go 程序都必須要有 package main .
關(guān)鍵字 import 與 Ruby 里的 require 有些像, 用來(lái)引入外部庫(kù). 這里我們引入了 fmt 庫(kù), 用來(lái)對(duì)輸入輸出做格式化.
main 函數(shù)是程序的執(zhí)行入口. 所有在 Go 里面執(zhí)行的代碼, 都會(huì)進(jìn)入 main 函數(shù)中. 在 Go 中, 可執(zhí)行的代碼被包在一對(duì)大括號(hào)之間. 并且, 左大括號(hào)必須與塊條件放在一行, 比如函數(shù), 分支或循環(huán)條件等.
命令行中執(zhí)行代碼
Ruby
把如下這段 Ruby 代碼復(fù)制到 hello_world.rb 文件中. ".rb" 是 Ruby 源碼文件的擴(kuò)展名. 我們可以用 ruby <filename> 運(yùn)行
> ruby hello_world.rb
Hello World!!!
Go
把如下這段 Go 代碼復(fù)制到 hello_world.go 文件中. ".go" 是 Go 源碼文件的擴(kuò)展名. 我們有兩種方式運(yùn)行下面這段代碼.
> go build hello-world.go
> ./hello-world
Hello World!!!
或者
> go run hello-world.go
Hello World!!!
第一種方式, 先使用 build 命令編譯了源碼文件同時(shí)生成了一個(gè)可執(zhí)行的二進(jìn)制文件然后運(yùn)行. 第二種方式, 我們直接使用 go run 命令執(zhí)行源碼. 這個(gè)命令實(shí)際上是在后臺(tái)編譯出了可執(zhí)行文件并運(yùn)行.
代碼注釋
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 是動(dòng)態(tài)類型語(yǔ)音, 所以并不需要定義變量類型. 但是 Go 作為一個(gè)靜態(tài)類型語(yǔ)言, 就必須在聲明變量的同時(shí)定義類型.
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
在這個(gè)例子中可以看到, 盡管 Go 是個(gè)靜態(tài)類型語(yǔ)言, 但由于類型推導(dǎo)自動(dòng)定義了類型, 寫(xiě)起來(lái)可以像動(dòng)態(tài)語(yǔ)言一樣爽.
數(shù)據(jù)類型
這是 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 里面, 我們可以給一個(gè)變量賦值兩個(gè)不同類型的值, 但在 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 對(duì)象, 但 Go 里面叫 map. map 的語(yǔ)法是 map[string] int, 中括號(hào)內(nèi)用于指定 key 的類型, 而右邊這個(gè)用于指定 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"]
檢查一個(gè) key 是否在 map 中
我們可以通過(guò) 多重賦值的方式檢查一個(gè) key 是否存在. 在下面這個(gè)例子里, 如果 name 對(duì)象確實(shí)有 last_name 這個(gè) key , 那么 ok 變量將是 true 且 lastname 變量會(huì)賦值為 last_name 的值, 反之 ok 會(huì)是 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(數(shù)組)
跟 Ruby 一樣, 我們也有數(shù)組. 但在 Go 里面, 我們需要在聲明的時(shí)候定義數(shù)組長(zhǎng)度.
#Ruby
array = [1,2,3]
array := [3]int{1,2,3}
names := [2]string{"Nikita", "Aneeta"}
Slice(切片)
數(shù)組有個(gè)限制是不能在運(yùn)行時(shí)被修改. 也沒(méi)有提供嵌套數(shù)組的能力. 對(duì)于這個(gè)問(wèn)題, Go 有個(gè)數(shù)據(jù)類型叫 slices (切片). 切片有序的保存元素, 并且可以隨時(shí)被修改. 切片的定義與數(shù)組類似, 但不用聲明長(zhǎng)度.
var b []int
好, 接下來(lái)我們通過(guò)一些酷炫的玩法來(lái)對(duì)比一下 Ruby
給數(shù)組添加新元素
在 Ruby 中, 我們使用 + 給數(shù)組新增新元素. 在 Go 中 , 我們使用 append 函數(shù).
#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]
截取子數(shù)組
#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]
復(fù)制數(shù)組(切片)
在 Ruby 中, 我們可以通過(guò)把數(shù)組賦值給另一個(gè)變量的方式直接復(fù)制數(shù)組. 在 Go 中, 我們不能直接賦值. 首先, 初始化一個(gè)與目標(biāo)數(shù)組長(zhǎng)度一致的數(shù)組, 然后使用 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)
注意: 復(fù)制數(shù)組的元素?cái)?shù)量取決于目標(biāo)數(shù)組定義的長(zhǎng)度:
a := []int{1,2,3,4}
b := make([]int, 2)
copy(b, a) // copy a to b
fmt.Println(b)
//=> [1 2]
這里只有兩個(gè)元素被復(fù)制了, 因?yàn)?b 數(shù)組變量的長(zhǎng)度只有 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)
}
如果一個(gè)變量的作用域只在 if 塊之內(nèi), 那我們可以在 if 條件中給變量賦值, 如下:
if name, ok := address["city"]; ok {
// do something
}
Switch Case
Go 中 switch case 的執(zhí)行方式與 Ruby 很類似 (只執(zhí)行符合條件的 case 子句, 而不會(huì)包括之后所有的 case). Go 中我們使用 switch...case 語(yǔ)法, 在 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 語(yǔ)法.
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)
}
循環(huán)
在 Go 中, 我們只有 loop. 但是, 我們可以用 loop 實(shí)現(xiàn) Ruby 中支持的各種不同的循環(huán). 我們來(lái)看一下:
基本循環(huán)
//Go
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum)
While 循環(huán)
Go 中沒(méi)有 while 關(guān)鍵字. 但可以用下面這種方式實(shí)現(xiàn) while.
#Ruby
sum = 1
while sum < 1000
sum += sum
end
puts sum
//Go
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)
無(wú)限循環(huán)
#Ruby
while true
#do something
end
//Go
for {
// do something
}
Each 循環(huán)
Go 中, 我們可以使用 loop with range 的方式遍歷映射或數(shù)組. 類似 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 來(lái)處理異常.
#Ruby
begin
#code
rescue => e
#rescue exception
end
在 Go 中, 我們有 Panic Defer 和 Recover 這幾種概念來(lái)捕獲與處理異常. 然而, 我們代碼中是極少需要手動(dòng)處理 panic 的, 通常由框架或庫(kù)來(lái)處理.
我們可以看看下面這個(gè)例子, 一個(gè) error 是怎么被 Panic 拋出, 然后通過(guò) defer 語(yǔ)句中 recover 關(guān)鍵字捕獲的. defer 語(yǔ)句會(huì)延遲到所在的函數(shù)結(jié)束之前執(zhí)行. 所以, 在下面這個(gè)例子里, 即使 panic 導(dǎo)致程序流程中斷, defer 語(yǔ)句仍然會(huì)執(zhí)行. 因此, 將 recover 語(yǔ)句放到 defer 當(dāng)中可以做出與 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)
}
值得一提的是, 我們?nèi)粘?xiě)代碼通常不會(huì)直接使用 panic. 我們會(huì)使用 error 接口去檢查錯(cuò)誤, 并且做相應(yīng)的處理. 如下例子:
// 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 通常是用來(lái)做確保程序結(jié)束前會(huì)執(zhí)行的部分, 比如清理工作. 像關(guān)閉文件, 關(guān)閉 HTTP 連接.
Functions
Ruby 中, 與方法的參數(shù)類型一樣, 我們也不用指定返回類型. 但在 Go 中, 對(duì)函數(shù)的參數(shù)與返回都必須定義類型. 另外要注意 return 關(guān)鍵字在 Go 中是必須要寫(xiě)的, 不像 Ruby 里面可有可無(wú). 因?yàn)?Ruby 自動(dòng) 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 中, 我們沒(méi)有指針的概念. 在 Go 中, 我們用指針來(lái)持有某個(gè)值所在的內(nèi)存地址. 代碼中體現(xiàn)為 * 操作符. 另一個(gè)操作符是 &, 用來(lái)獲取某個(gè)值的內(nèi)存地址, 也就是生成一個(gè)指針. 下面這個(gè)例子會(huì)展示指針的引用與反引用.
// Go
//pointer initialization | 指針初始化
var a *int
b := 12
// point to the address of b | 將 a 指向 b 的內(nèi)存地址
a = &b
// read the value of b through pointer | 通過(guò)指針讀取相應(yīng)內(nèi)存地址的值
fmt.Println(*a) // => 12
// set value of b through pointer | 通過(guò)指針設(shè)置相應(yīng)內(nèi)存地址的值
*a = 5
fmt.Println(b) // => 5
面向?qū)ο缶幊?/h3>
Go 是一種輕量級(jí)的面向?qū)ο笳Z(yǔ)言. 可以使用 struct 提供封裝與類型成員函數(shù), 但是沒(méi)有面向?qū)ο笳Z(yǔ)常見(jiàn)的集成. 我們可以把一個(gè)函數(shù)綁定到 struct 上. 然后當(dāng)我創(chuàng)建 struct 實(shí)例后, 就可以擁有訪問(wèn)實(shí)例變量與綁定在其上的方法的能力
# 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())
}
}
在上面這個(gè)例子中, 我們用 struct 定義了一個(gè)新類型 Festival. 可以看到, 對(duì)于 Festival 我們也定義了兩種函數(shù) IsDashain ToString. 這些函數(shù)對(duì)于 Festival 實(shí)例都是可訪問(wèn)的. Go, 這類函數(shù)稱之為 方法. 分別方法與函數(shù), 可以看它是否依附于某個(gè)對(duì)象, 在其他語(yǔ)言中大致也是一樣的約定. 另外我們可以看到對(duì)于方法定義, 我們是針對(duì) Festival 指針創(chuàng)建的方法. 對(duì)于指針接受者, 有兩個(gè)用法值得注意.
當(dāng)需要修改方法接受者, 接受者就必須是指針類型. 在上面這個(gè)例子中, 當(dāng) festival 的 Name 屬性為 Dashain, 這個(gè)方法就會(huì)修改 Description 的值, 并且也可以被 main 函數(shù)訪問(wèn).
如果參數(shù)太大, 可以考慮改為傳入指針的方式以避免可能的性能問(wèn)題.
注意: Go 的 interface(接口) 可以提供類似 Ruby 中繼承/混合的感覺(jué)以及更多 OOP 的靈活性.
公有/私有 方法與變量
Ruby 中, 我們用 private 關(guān)鍵字來(lái)定義私有方法. 在 Go, 定義私有方法或私有變量, 只需要將名稱首字母小寫(xiě)就可以了.
#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 來(lái)做 序列號(hào)與反序列化.
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 {
Page int `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
}
注意: 上面這個(gè)例子里, json:"page" 命令用于將 json 中的 page 對(duì)應(yīng)到 struct 的 Page 屬性.
Package(庫(kù))
Go 的 Package 與 Ruby 中的 Gem 類似. Ruby 中, 我們用 gem install <gem name> 安裝一個(gè) gem . Go 中, 是go get <package path>.
我們也有一些內(nèi)置的基礎(chǔ)庫(kù). 常見(jiàn)的有:
fmt
顧名思義, 這個(gè)庫(kù)用于做代碼格式化. 這是一個(gè)很好用的庫(kù). 在 Ruby 中, 我們遵循 rubocop 的代碼規(guī)范做代碼格式化. 但在 Go 中, 我們不用太操心這些事. fmt 庫(kù)會(huì)幫我們處理. 只需要在寫(xiě)完代碼以后, 執(zhí)行一下 gofmt 或 go fmt就好了.
gofmt -w yourcode.go
//OR
go fmt path/to/your/package
代碼會(huì)自動(dòng)格式化.
log
這個(gè)庫(kù)與 Ruby 的 logger 很像. 它定義了類型 Logger, 包含格式化輸出相關(guān)的各種方法.
net/http
用于創(chuàng)建 HTTP 請(qǐng)求. 與 Ruby 的 net http 很像.
Go 的并發(fā)
不像 Ruby, 我們?nèi)绻霾l(fā)就需要引入一些額外的并發(fā)庫(kù)比如 celluloid . Go 自帶了并發(fā)能力. 我們只需要在需要并發(fā)操作的地方在前面加上 go 關(guān)鍵字就好了. 下面這個(gè)例子里, 我們?cè)?main 函數(shù)中用 go 關(guān)鍵字并發(fā)執(zhí)行了 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
還有一個(gè)概念稱之為 Channels (管道), 用于兩個(gè) goroutine 之間的通信.