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 运行

> ruby hello_world.rb
Hello World!!!

Go

把如下这段 Go 代码复制到 hello_world.go 文件中. ".go" 是 Go 源码文件的扩展名. 我们有两种方式运行下面这段代码.

> go build hello-world.go
> ./hello-world
Hello World!!!

或者

> go run hello-world.go
Hello 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 指针创建的方法. 对于指针接受者, 有两个用法值得注意.

  1. 当需要修改方法接受者, 接受者就必须是指针类型. 在上面这个例子中, 当 festival 的 Name 属性为 Dashain, 这个方法就会修改 Description 的值, 并且也可以被 main 函数访问.

  2. 如果参数太大, 可以考虑改为传入指针的方式以避免可能的性能问题.

注意: 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 {
    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
}

注意: 上面这个例子里, json:"page" 命令用于将 json 中的 page 对应到 struct 的 Page 属性.

Package(库)

Go 的 Package 与 Ruby 中的 Gem 类似. Ruby 中, 我们用 gem install 安装一个 gem . Go 中, 是go get .

我们也有一些内置的基础库. 常见的有:

fmt

顾名思义, 这个库用于做代码格式化. 这是一个很好用的库. 在 Ruby 中, 我们遵循 rubocop 的代码规范做代码格式化. 但在 Go 中, 我们不用太操心这些事. fmt 库会帮我们处理. 只需要在写完代码以后, 执行一下 gofmtgo 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 之间的通信.

关于 Go 的更多内容

  1. https://golang.org/doc/effective_go.html

  2. https://play.golang.org/

  3. https://tour.golang.org

  4. https://gobyexample.com/

  5. https://www.safaribooksonline.com/videos/mastering-go-programming/9781786468239

你可能感兴趣的:(Go for Ruby developers(译))