Mongoose House Technical Edition

给 Java 程序员写的 Go 语言入门

Go,又称 golang,是 Google 开发的一种静态强类型、编译型,并发型,并具有垃圾回收功能的编程语言。

罗伯特·格瑞史莫、罗勃·派克及肯·汤普逊于 2007 年 9 月开始设计 Go 语言,于 2009 年 11 月正式宣布推出,成为开放源代码项目。

1. 环境

1.1. GOROOT和GOPATH

  • GOROOT:Go 的安装路径;
  • GOPATH:Go 的工作路径,类比 Java 中的 CLASSPATH。GOPATH 可以有多个,Windows-based OS 以分号分隔;Unix-based OS 以冒号分隔。

注: GOROOT 不能和 GOPATH 相同。

设置 PATH = $GOROOT/bin:$GOPATH/bin:$PATH 方便直接运行程序。

1.2. 目录结构

GOPATH 下包含三个目录:binpkgsrc

  • bin:存放编译后的可执行文件;
  • pkg:存放引用的包(package),类比 Java CLASSPATH 引用的第三方库;
  • src:存放源代码。

一般的工程目录为 $GOPATH/src/github.com/<github_username>/<project_name>

1.3. Go 命令行

go build   // 编译源代码,编译后的目标文件放在 pkg 目录下
go run     // 编译源代码,并从入口函数执行程序
go install // 从入口函数程序编译代码,编译后的可执行程序放在 bin 目录下

1.4. 测试程序

源文件后加 _test 的文件是此代码的单体测试代码。例如,string.go 的单体测试程序是 string_test.go

使用 go test 运行测试程序。

1.5. 开发IDE

2. 语法

2.1. 语言结构

2.1.1. 包

Go 语言由包(package)组成。Go 语言中的包类比 Java 中的 类(class)。

首字母大写的名称被导出包。 类比 Java 中的 public 方法和 public 类的成员变量。

程序运行的入口是 package main

// 写法一
import "fmt"
import "math"

// 写法二
import (
    "fmt"
    "math"
)

2.1.2. 语句

Go 语言语句后可以用分号结尾,也可以不用分号结尾。

2.1.3. 注释

C 风格,和 Java 相同。

2.1.4. 运算符

C 风格,和 Java 相同。

2.2. 变量

2.2.1. 数据类型

基本数据类型有布尔(bool)、数字(int, float, …)和字符串(string)三种。

数字类型又包含 uint, uint8(byte), uint16, uint32, uint64, int, int8, int16, int32(rune), int64, float, float32, float64, complex64, complex128, uintptr等。

衍生类型又包括 指针(pointer)、数组(array)、结构体(struct)、联合体(union)、函数(func)、切片(slice)、接口(interface)、管道(channel)等。

Go 语言需要显式转换变量类型。

aaa := 42
bbb := float64(aaa)
ccc := uint(bbb)

2.2.2. 声明变量

使用 var 声明变量,类型写在变量名后面。

当声明多个变量连续为同一类型时,除最后一个类型外,其他可以省略。

var aaa, bbb, ccc bool

// 另一种写法
var (
    aaa bool
    bbb bool
    ccc bool
)

变量可以定义在包级别(函数外部)和函数级别。类比 Java 中类的成员变量和方法内部的局部变量。

2.2.3. 初始化变量

声明变量时,可以初始化变量。每个变量依次对应一个值。

初始化变量不指定类型时,变量从初始值中获得类型。

var i, j int = 1, 2
var aaa, bbb, ccc = true, 0, "no!"

在函数内部,使用 := 初始化变量时,可以省略 var。在函数外部不可省略。

aaa, bbb, ccc := true, 0, "no!"

变量没有初始化,系统自动赋零值。数值的零值是 0,布尔是 false,字符串是 ""(空字符串)。

2.2.4. 常量

常量使用关键字 const 声明。

常量只能是字符串、布尔和数字三种类型。

常量不能使用 := 符号赋值声明。

const aaa, bbb, ccc = 1, false, "str"

常量的枚举写法,

const (
    UNKNOWN = 0
     FEMALE = 1
       MALE = 2
)

常量赋值时,如果使用函数,函数必须为内置函数。

const (
    aaa = "abc"
    bbb = len(aaa)
    ccc = unsafe.Sizeof(aaa)
)

iota 是一个可以被编译器修改的常量。在每一个 const 关键字出现时,被重置为 0,然后再下一个 const 出现之前,每出现一次 iota,其所代表的数字会自动增加 1

// 例一
const (
    aaa = iota // aaa = 0
    bbb = iota // bbb = 1
    ccc = iota // ccc = 2
)

// 例二
const (
    aaa = iota   // aaa = 0
    bbb          // bbb = 1
    ccc          // ccc = 2
    ddd = "ha"   // ddd = "ha", iota += 1
    eee          // eee = "ha", iota += 1
    fff = 100    // fff = 100, iota +=1
    ggg          // ggg = 100, iota +=1
    hhh = iota   // hhh = 7
    iii          // iii = 8
)

2.3. 流程控制语句

2.3.1. 循环

2.3.1.1. for 循环
for init; condition; post { ... }

// 例
sum := 0
for i := 0; i < 10; i++ {
    sum += i
}
  • init 初始化语句,在第一次循环执行前被执行;
  • condition 循环条件表达式,每轮迭代开始前被求值;
  • post 后置语句,每轮迭代后被执行。

循环初始化语句和后置语句都是可选的。

2.3.1.2. while 循环
for condition { ... }

// 例
sum := 1
for sum < 1000 {
    sum += sum
}
2.3.1.3. 集合迭代
for key, value := range old_collection {
    new_collection[key] = value
}

Go 支持 breakcontinuegoto 语句。三种语句的用法和 Java 相同。

2.3.2. 分支

2.3.2.1. if 语句
if init; condition { ... }

// 例
if aaa := math.Pow(bbb, n); aaa < ccc {
    // ...
}

// init 可省略
aaa := math.Pow(bbb, n)
if aaa < ccc {
    // ...
}

Go 语言支持 if-else,用法和 Java 相同。

2.3.2.2. switch 语句

switch 的条件从上到下的执行,当匹配成功的时候停止,不需要 break

没有条件的 switchswitch true 一样。这一构造使得可以用更清晰的形式来编写长的 if-then-else 链。

var grade string = "B"
var marks int = 90

switch marks {
   case 90: grade = "A"
   case 80: grade = "B"
   case 50,60,70 : grade = "C"
   default: grade = "D"  
}

switch {
   case grade == "A" :
      fmt.Printf("优秀!\n" )     
   case grade == "B", grade == "C" :
      fmt.Printf("及格\n" )     
   case grade == "D":
      fmt.Printf("不及格\n" )
   default:
      fmt.Printf("差\n" );
}

2.3.3. 延迟

defer 语句会延迟函数的执行直到上层函数返回。

延迟函数的参数会立刻被执行,但是在上层函数返回前函数都不会被调用。

延迟的函数调用被压入一个栈中。当函数返回时, 会按照后进先出的顺序调用被延迟的函数调用。

// 以下代码输出 hello world
defer fmt.Println("world")
fmt.Println("hello")

2.4. 函数

2.4.1. 参数

函数参数的类型在变量名之后。函数参数的类型可以省略,除了最后一个参数。

func add(x, y int) int {
    return x + y
}

Go 语言使用值传递,函数对形参的修改不会影响到实参。引用传递需要依赖传递参数的指针实现。

// 引用传递
func swap(x *int, y *int) {
   var temp int
   temp = *x    /* 保持 x 地址上的值 */
   *x = *y      /* 将 y 值赋给 x    */
   *y = temp    /* 将 temp 值赋给 y */
}

2.4.2. 返回值

函数可以有任意返回值,返回值类型在函数之后。

func swap(x, y string) (string, string) {
    return y, x
}

Go 的返回值可以被命名,被命名的返回值相当于在函数体开头声明的变量。

没有参数的 return 语句返回各个返回变量的当前值。

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

2.4.3. 函数句柄

Go 语言中函数也是值。他们可以像其他值一样传递,比如,函数值可以作为函数的参数或者返回值。

// 声明 myfunc 为一匿名函数的句柄
myfunc := func(x int) int {
    return x * x
}

// 定义一个接收函数句柄为参数的函数
func yourfunc (fn func(int) int) int {
    return fn(3)
}

// 打印 9
fmt.Println(yourfunc(myfunc))

2.4.4. 闭包

Go 函数可以是一个闭包。闭包是一个函数值,它引用了函数体之外的变量。 这个函数可以对这个引用的变量进行访问和赋值;换句话说这个函数被“绑定”在这个变量上。

// 声明一个闭包函数,返回值是另一个匿名函数体
func getSequence() func() int {
   i := 0
   return func() int {
      i += 1
     return i  
   }
}

// 设置匿名函数句柄
nextNumber := getSequence()

// 打印闭包中 i 的值,输出为1,2,3
fmt.Println(nextNumber())
fmt.Println(nextNumber())
fmt.Println(nextNumber())

3. 数据结构

3.1. 指针和结构体

3.1.1. 指针

类型 *T 是指向类型 T 的值的指针,其零值是 nilnil 指针也称为空指针。

& 符号(取地址符)会生成一个指向其作用对象的指针。* 符号(取值符)表示指针指向的对象的值。

一个指针变量通常缩写为 ptr

Go 支持指向指针的指针。

与 C 不同,Go 没有指针运算。

var a int = 20   /* 声明实际变量 */
var ptr *int     /* 声明指针变量 */
ptr = &a         /* 指针变量 ptr 的值为变量 a 的地址 */
var pptr **int   /* 指向 ptr 指针的指针 */
pptr = &ptr      /* 指向 ptr 的地址 */

3.1.2. 结构体

结构体使用 type 关键字声明。结构体使用大括号赋值,使用点访问成员变量。结构体字段可以通过结构体指针透明访问。

type Vertex struct {
    X int
    Y int
}

var v Vertex
v = Vertex{1, 2}
v.X = 4

p := &v
p.Y = 5

3.2. 集合

3.2.1. 数组

数组的长度不可改变。

// 声明数组
var balance [10] float32

// 初始化
var balance = [5] float32 {1000.0, 2.0, 3.4, 7.0, 50.0}

// 访问数组
float32 salary = balance[2]

// 声明三维数组
var threedim [5][10][4] int

// 初始化二维数组
aaa = [3][4] int {  
 {0, 1, 2, 3} ,   /*  第一行索引为 0 */
 {4, 5, 6, 7} ,   /*  第二行索引为 1 */
 {8, 9, 10, 11}   /*  第三行索引为 2 */
}

// 访问二维数组
int val = a[2][3]

3.2.2. 切片

切片是对数组的抽象。切片的长度可变。

// 声明切片
var aaa [] int

// 使用 make 函数创建切片,长度为 10
var aaa []int = make([]int, 10)

// 初始化切片,初始化值依次是1, 2, 3。其cap=len=3
aaa :=[] int {1, 2, 3}

// 从数组 aaa 创建切片 bbb
bbb := aaa[:]

// 从切片 bbb 的第 2 个元素开始到第 5 个元素结束,创建切片 ccc
// 截取规则: 前包含,后不包含
ccc := bbb[1:6]

len 函数可以获取切片的长度,cap 函数可以获取切片最长可达多长。

copy 函数用来拷贝切片,可以增加切片的容量;append 函数向切片中追加元素。

切片的零值是 nil,一个 nil 的切片的长度和容量是 0

3.2.3. range

当使用 for 循环遍历一个切片时,每次迭代 Range 将返回两个值。 第一个是当前下标(序号),第二个是该下标所对应元素的一个拷贝。

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
for i, v := range pow {
    fmt.Printf("2**%d = %d\n", i, v)
}

可以通过赋值给 _ 来忽略序号和值;如果只需要索引值,去掉 , value 的部分即可。

pow := make([]int, 10)
for i := range pow {
    pow[i] = 1 << uint(i)
}
for _, value := range pow {
    fmt.Printf("%d\n", value)
}

Range 用在 map 上。

kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
    fmt.Printf("%s -> %s\n", k, v)
}

Range 用在字符串上。

for i, c := range "go" {
    fmt.Println(i, c)
}

3.2.4. map

Map 在使用之前必须用 make 来创建;值为 nil 的 map 是空的,并且不能对其赋值。

Map 是无序的。

type Vertex struct {
    Lat, Long float64
}

// 初始化 map 方式一
var aaa map[string]Vertex
aaa = make(map[string]Vertex)
aaa["Bell Labs"] = Vertex{40.68433, -74.39967,}
aaa["Google"]    = Vertex{37.42202, -122.08408,}

// 初始化 map 方式二
var aaa = map[string]Vertex{
    "Bell Labs": Vertex{40.68433, -74.39967,},
    "Google":    Vertex{37.42202, -122.08408,},
}

// 初始化 map 方式三
var aaa = map[string]Vertex{
    "Bell Labs": {40.68433, -74.39967},
    "Google":    {37.42202, -122.08408},
}
// 在 map 中插入或修改元素
aaa["Bell Labs"] = Vertex{37.42202, -122.08408,}

// 获取 map 中的元素
var v = aaa["Bell Labs"]

// 删除 map 中的元素
delete(aaa, "Bell Labs")

// 检测 map 中是否存在某个元素。如果存在 ok 返回 true;否则 ok 返回 false,v 是零值
v, ok := aaa["Bell Labs"]

3.3. 面向对象

3.3.1. 方法

Go 没有类,Go 中的方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。

// 命名类型
type MathInt int

func (f MathInt) Abs() int {
    if f < 0 {
        return int(-f)
    }
    return int(f)
}

// 结构体类型
type Vertex struct {
    X, Y int
}

// 有两个原因需要使用接受者指针:首先避免在每个方法调用时拷贝值;其次,方法可以修改接收者指向的值。
func (v *Vertex) Abs() int {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

3.3.2. 接口

接口是一组方法定义的集合。它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

/* 定义接口 */
type Phone interface {
    call()
}

/* 定义结构体 */
type NokiaPhone struct {
    // ...
}

type IPhone struct {
    // ...
}

/* 实现接口方法 */
func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}

/* 调用接口 */
var phone Phone

phone = new(NokiaPhone)
phone.call()

phone = new(IPhone)
phone.call()

隐式接口解耦了实现接口和定义接口,无需显式 implements 声明。例如,在 Java 中重载 toString 方法,在 Go 中如下,

// toString 在 Go 中的原型是
/*
type Stringer interface {
    String() string
}
*/

type Person struct {
    Name string
    Age  int
}

// 实现 Person 的 Stringer 接口,重载 String 方法
func (p Person) String() string {
    return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
    a := Person{"Arthur Dent", 42}
    z := Person{"Zaphod Beeblebrox", 9001}
    fmt.Println(a, z)
}

4. 错误

4.1. 错误处理

Go 不支持 try…catch…finally 处理异常,而使用 error 值来表示错误状态。

通常函数会返回一个 error 值,调用的它的代码应当判断这个错误是否等于 nil, 来进行错误处理。error 为 nil 时表示成功;非 nil 的 error 表示错误。函数通常在最后的返回值中返回错误信息。

i, err := strconv.Atoi("42")
if err != nil {
    fmt.Printf("couldn't convert number: %v\n", err)
    return
}
fmt.Println("Converted integer:", i)

自定义 error 应实现内建的 error 接口。

type error interface {
    Error() string
}

4.2. panic 和 recover

除了多值返回处理错误以外,Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理。

// 主函数中,defer 一个匿名函数用来错误处理
defer func() {
    fmt.Println("第二步")
    if err := recover(); err != nil {
        fmt.Println(err) // 通过 recover 获取的 err 就是 panic 传入的值:"I'm Error!"
    }
    fmt.Println("第三步")
}()
myfunc()

// 以下是 myfunc
func myfunc() {
    fmt.Println("第一步")
    panic("I'm Error!")
    fmt.Println("走不到的第四步")
}

5. 并发

5.1. 协程

协程(goroutine),是由 Go 运行时环境管理的轻量级线程。

使用 go 关键字 go myfunc(x, y, z) 开启一个新的协程执行 myfunc(x, y, z)myfunc 和其参数是当前协程中定义的,但是在新的协程中运行。

Go 的协程在相同的地址空间中运行,因此访问共享内存必须进行同步。

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

5.2. 管道

协程之间使用管道(channel)通讯。管道是有类型的。使用管道操作符 <- 发送或接收值。箭头表示数据流的方向。

ch <- v     // 把变量 v 发送到管道 ch
v := <- ch  // 从管道 ch 中接收,并赋值给 v

管道使用前必须创建。

ch := make(chan int) // 创建 int 型管道

默认情况下,在另一端准备好之前,发送和接收都会阻塞。这使得协程可以在没有明确的锁或竞态变量的情况下进行同步。

管道可以带缓冲(buffer)。make 函数提供第二个参数作为缓冲长度来初始化一个缓冲管道。

ch := make(chan int, 100) // 创建缓冲区为 100 个整型的 int 型管道

向带缓冲的管道发送数据的时候,只有在缓冲区满的时候才会阻塞。 而当缓冲区为空的时候接收操作会阻塞。

发送者可以关闭一个管道来表示再没有值会被发送了。接收者可以通过赋值语句的第二参数来测试管道是否被关闭。

只有发送者才能关闭管道,但是通常情况下管道无需关闭。

v, ok := <- ch // 管道关闭,ok 返回 false

for v := rang ch {...} // 通过 range 循环,可以不断从管道中取值,直到管道关闭

5.3. select

select 语句使得一个协程在多个通讯操作上等待。类似于 switch 语句。select 语句的每个 case 必须是一个通信操作,要么是发送要么是接收。

select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。

  1. 每个 case 都必须是一个通信;
  2. 所有管道表达式都会被求值;
  3. 所有被发送的表达式都会被求值;
  4. 如果任意某个通信可以进行,它就执行,其他被忽略;
  5. 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行;
  6. 如果所有 case 都不可执行,如果有 default 子句,则执行该语句;
  7. 如果没有 default 字句,select 将阻塞,直到某个通信可以运行。
func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x: // 当 push 到 c 中的值没有被 fmt.Println 消费掉的时候,会阻塞
            x, y = y, x+y
        case <-quit: // quit 管道一直为空,阻塞,直到 go func() 协程循环结束
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)    // c 的长度只有 1 个 int 值
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

5.4. 互斥

Go 标准库中提供了 sync.Mutex 类型及 LockUnlock 个方法实现互斥。

通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。用 defer 语句来保证互斥锁一定会被解锁。

type SafeCounter struct {
    v   map[string]int // 数据对象
    mux sync.Mutex     // 互斥锁
}

// 计数器
func (c *SafeCounter) Inc(key string) {
    c.mux.Lock()
    c.v[key]++ // 每次只有一个协程可以访问此代码
    c.mux.Unlock()
}

// 返回计数值
func (c *SafeCounter) Value(key string) int {
    c.mux.Lock()
    defer c.mux.Unlock() // defer 确保解锁在数据返回后执行
    return c.v[key]
}