• 函数和方法:
    • 定义函数:func split(sum int) (x, y int) {
    • 定义结构体的方法:func (v Vertex) Abs() float64 {

# 教程

A Tour of Go (opens new window)

# 变量声明

var x int       // x is int
var p *int      // p is pointer of int
var a [3]int    // a is array[3] of int

var (
    x int       // x is int
    p *int      // p is pointer of int
    a [3]int    // a is array[3] of int
)

var x int = 1
var p *int = &x

var x = 1
var p = &x

x := 1
p := &x

const Pi = 3.14

# Go 奇异的变量声明方式

第一眼看到,觉得很怪异。不过看了这篇 关于 Go 语法声明的文章 (opens new window),觉得挺有意思。

Go 的声明语句是为了和自然语言(英语)保持一致:

x int       // x is int
p *int      // p is pointer of int
a [3]int    // a is array[3] of int

这主要针对的是 C 的指针,特别是加入了函数指针,一切都复杂起来:

int (*(*fp)(int (*)(int, int), int))(int, int)

这段代码定义了一个函数指针 fp,其接收两个参数,第一个是一个函数指针(类型是 int (*)(int, int),接收两个 int 并返回 int),第二个参数是 intfp 返回一个函数指针,类型是最外层的 int (*(...))(int, int),表示接收两个 int 并返回 int)。

如果用 Go 改写,将会变成:

f := func(func (int, int) int, int) func(int, int) int

按照从左往右以 函数接收 xx 参数,返回 xx 的形式解读,可以知道:f 是一个函数,其接收两个参数,第一个参数是一个函数(func (int, int) int,接收。两个 int 并返回 int),第二个参数是一个 int。返回类型是一个函数 func(int, int) int,表示接收两个 int 并返回一个 int。

这里仅对文章核心作出解释,英文原文有更循序渐进的解释。

# 流程控制

# for

for i:= 0; i < 10; i++ {
    sum += i
}

nums := []int{2, 3, 4}
sum := 0
for i, num := range nums {
    fmt.Printf("%d\n", i)
    sum += num
}

for i := range nums {
}

for _, num := range nums {
}

# while

// while
for i < 10 {
    sim += i
    i++
}

// while true
for {

}

# if

if v < lim {

}

if v := math.Pow(x, n); v < lim {

}

# defer

defer 语句会将函数推迟到外层函数返回之后执行。

func main() {
    defer fmt.Printf("world!")
    fmt.Printf("hello, ")
}
// hello, world!

也可以写一个匿名函数然后调用它:

func main() {
    defer func() { fmt.Printf("world!") }()
    fmt.Printf("hello, ")
}

# 更多类型:指针、结构体、切片和映射

# 指针 pointer

i := 42
p = &i

fmt.Println(*p) // 通过指针 p 读取 i
*p = 21 // 通过指针 p 设置 i

# 结构体 struct

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

var (
    v1 = Vertex{1, 2}  // 创建一个 Vertex 类型的结构体
    v2 = Vertex{X: 1}  // Y:0 被隐式地赋予
    p = &v1
)

v1.X
(*p).X
p.X        // 等价于 (*p).X
// 硬是把指针玩成了引用

# 数组 array 和切片 slice

// 数组
// 数组的长度是其类型的一部分,因此数组不能改变大小
var a [10]int
// 切片
var s []int


// [low: high], 左闭右开
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]    // [3 5 7]
// 默认 low = 0, high = length
var s []int = primes[:]    // [2 3 5 7 11 13]


// 数组
// `类型{}` 可以理解成 Go 的构造函数,后面是构造函数的参数。被称为复合字面量 (Composite literals) 
a := [3]bool{true, true, false}
a := [...]bool{true, true, false}
// 创建一个和上面相同的数组,然后构建一个引用了它的切片
s := []bool{true, true, false}


// 切片的长度是 ... 就是它的长度
len(s)
// 切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数
// 可能是为了方便的检查 append 操作是否越界
cap(s)

make 用于创建切片/动态数组:

s := make([]int, 3)
// cap(s) = len(s) = 3

append 用于在切片后增加元素。

func append(s []T, vs ...T) []T
  1. append 不会改变原来的切片,所以需要再赋值给 ss = append(s, 1)
  2. 如果 s 的底层数组大小足够,append 会直接修改底层数组;否则,会重新分配一个更大的数组,返回的切片会指向这个新数组,原数组不变

Go 切片:用法和本质 - Go 语言博客 (opens new window)

Go的数组是值语义。一个数组变量表示整个数组,它不是指向第一个元素的指针(不像 C 语言的数组)。当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。(为了避免复制数组,你可以传递一个指向数组的指针,但是数组指针并不是数组)
一个切片可以看作一个结构体,包含三个元素:指向数组的指针、片段的长度和容量。

# make

Making slices, maps and channels (opens new window)

个人认为 make 是一个非常神奇的存在。

内置函数 make 接受的第一个参数是类型 T(仅限于 slicemapchannel)。第二个(可选)参数是一个表达式列表,因 T 而异。make 返回一个 T 的值(而非指针)。

Call             Type T     Result

make(T, n)       slice      slice of type T with length n and capacity n
make(T, n, m)    slice      slice of type T with length n and capacity m

make(T)          map        map of type T
make(T, n)       map        map of type T with initial space for approximately n elements

make(T)          channel    unbuffered channel of type T
make(T, n)       channel    buffered channel of type T, buffer size n

可以理解成 make 是一个初始化的工具函数。

# 映射 map

// 一个 string -> Vertex 的映射
// 未初始化,不能直接使用
var m  = map[string]Vertex

// make 初始化和复合字面量初始化
var m = make(map[string]Vertex)
var m = map[string]Vertex{}
var m = map[string]Vertex{
    "Bell Labs": {40.68433, -74.39967},
    "Google":    {37.42202, -122.08408},
}

// 初始化以后就可以用了
m["a"] = Vertex{};                              // 增
delete(m, "a")                                  // 删
fmt.Println(m["Bell Labs"])                     // 查
m["Bell Labs"] = Vertex{40.68433, -74.39968}    // 改

# 函数和方法

函数:

// 接受两个参数,返回一个参数
func add(x int, y int) int {
    return x + y
}

// 相同类型可以简写
func add(x, y int) int {}

// 可以返回多个值
func swap(x, y string) (string, string) {
    return y, x
}
func main() {
    a, b := swap("hello", "world")
    fmt.Println(a, b)
}

// 返回值可以被命名,赋值后直接 return
func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

方法:

type Vertex struct {
    X, Y float64
}

// go 的方法是写在类外的,所属的类(被称为接收者)需要在函数前指明
// 但也不是哪里都可以定义方法:类型定义和方法声明必须在同一包内
func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// 指针接收者的用法和普通接收者完全一样
// 但可以进行引用传递而不是值传递,这很像 C++ 的引用
func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func Scale(v *Vertex, f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    /*
     * 调用方法不需要考虑传指针还是传变量,直接调用就行
     * 编译器会自动根据方法签名,将代码解释为 (*v).Scale() 或 (&v).Scale()
     */
    v.Scale(10)
    Scale(&v, 10)   //而普通函数传参就需要写 &v
    fmt.Println(v.Abs())
}

# 接口

没学过 Java 的我看 Go 的官方教程 (opens new window)的接口部分看得我一脸懵,于是找了其他的教程,看到菜鸟教程 (opens new window)的示例不错。

package main

import (
    "fmt"
)

type Phone interface {
    call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

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

func main() {
    var phone Phone

    phone = new(NokiaPhone)
    phone.call()

    phone = new(IPhone)
    phone.call()
}

C++ 没有接口的概念,Java 有,可参考 Java 接口 (opens new window)

如果用 C++ 的方式理解,interface 可以裂解为一个“父类”,它声明了很多“虚函数”,由“子类”靠重载进行实现嘛!所以也就出现 interface “接口类型可以被赋值”这种听起来匪夷所思的事情,其实也就是把子类值赋给了父类变量,之后便可以调用父类的成员函数了。

这也是 Go 为了弥补没有类而产生的语法吧。

顺便一提,main 函数不使用 interface 写法也是可以的:

func main() {
    phone1 := new(NokiaPhone)
    phone1.call()

    phone2 := new(IPhone)
    phone2.call()
}

# 空接口 empty interface

接口还不止于此:一个不包含任何方法的接口被称为空接口 empty interface。

空接口可保存任何类型的值(因为每个类型都至少实现了零个方法)。

空接口被用来处理未知类型的值。例如,fmt.Print 可接受类型为 interface{} 的任意数量的参数。

# 类型断言和类型选择 type assertion and switch

所以又接着产生了类型断言 type assertion 和类型选择 type switch

类型断言用于断言这个 interface 里到底是什么东西。其语法及对应输出如下:

package main

import "fmt"

func main() {
    var i interface{} = "hello"

    s := i.(string)
    fmt.Println(s)
    // hello

    s, ok := i.(string)
    fmt.Println(s, ok)
    // hello true

    f, ok := i.(float64)
    fmt.Println(f, ok)
    // 0 false

    f = i.(float64)
    fmt.Println(f)
    /*
    panic: interface conversion: interface {} is string, not float64

    goroutine 1 [running]:
    main.main()
        /tmp/sandbox906950040/prog.go:17 +0x1fe
    */
}

类型选择就是综合了类型断言和 switch

switch v := i.(type) {
case int:
    // v 的类型为 int
case float64:
    // v 的类型为 float64
default:
    // 没有匹配,v 与 i 的类型相同
}

# 常用接口 Stringer

fmt (opens new window) 包中定义的 Stringer (opens new window) 是最普遍的接口之一。

type Stringer interface { String() string } Stringer (opens new window) 是一个可以用字符串描述自己的类型。fmt (opens new window) 包(还有很多包)都通过此接口来打印值。

类似于 Java 的 toString(),定义了这个函数以后就可以调用 fmt 输出了。

# Go 包

导入包:

import "fmt"
import "math"

import (
    "fmt"
    "math"
)

大写开头的变量和函数会被自动导出,小写的则不能被导出。

所以,Go 的命名方法是,需要导出的东西使用 PascalCase,而内部的东西使用 camelCase。看起来很怪异,因为这和 C++/Java 的类使用 PascalCase、对象使用 camelCase 不同。

# Go 语言代码风格

go fmt <filename>.go 永远滴神!

虽然 go fmt 的风格是用 tab,还是八个空格的 tab,但至少有一个官方排版方案,所以比 C++、Java、Python 各种民间规范更能让人接受。

# Go 的指针

指针和引用的功能是类似的,所以很多语言语言只实现了指针(如 C、Go),或只实现了引用(如 Java、Python)。如果二者都实现了,可能开发者也偏向于使用单一的一种(如 C++ STL中基本都是使用指针)。

Go 只实现了指针。但是不同的是,它的指针结构体有点意思:(*p).X 可以简写为 p.X

这种写法,使得结构体指针访问成员可以写成 p.X,这种写法反而更像是引用。所以,Go 虽然使用的是指针,但其语法也借鉴了引用的优点。

# Go 并发

# Go 多线程

Go 开多线程也太香了吧,直接 go <function> 就可以了。

# Go 线程同步:信道

Go 的线程同步使用的是信道 channel,类似于《操作系统——精髓与设计原理》里进程同步的 消息传递 方法。

# 无缓冲区信道

默认的信道是无缓冲区的,这种情况下采用的是“阻塞发送、阻塞接收”的方式:发送方和接收方先准备好的一方会被阻塞,直至另一方也准备好了。

创建一个无缓冲区的 int 信道可以使用 c := make(chan int)

// 将求数组和问题分配到两个进程完成(分别计算前 3 个和、后 3 个和)
package main

import "fmt"

func sum(a []int, c chan int) {
    s := 0
    for _, v := range a {
        s += v
    }
    c <- s // 将和送入 c
}

func main() {
    a := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(a[:len(a)/2], c)
    go sum(a[len(a)/2:], c)
    x, y := <-c, <-c // 从 c 中接收

    fmt.Println(x, y, x+y)
}

c 无缓冲区,因此在主线程执行 x, y := <-c, <-c 之前就准备好的 c<-ssum 会被阻塞。

对于无缓冲区的信道,这么写会报死锁:

package main

import "fmt"

func main() {
    ch := make(chan int)
    ch <- 1
    fmt.Println(<-ch)
}

/*
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /tmp/sandbox780334017/prog.go:7 +0x59

Program exited.
*/

# 有缓冲区信道

下面这种使用缓冲区信道,则是:当缓冲区满时阻塞发送方、当缓冲区空时阻塞接收方。又像是生产者、消费者问题模型了。

缓冲区大小为 n 的 int 信道定义方法为 ch := make(chan int, n)。顺便一提,无缓冲区的代码也可以用 ch := make(chan int, 0) 定义。

package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

有意思的是,如果加两行,也会报死锁:

package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    ch <- 3
    fmt.Println(<-ch)
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

/*
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /tmp/sandbox780334017/prog.go:7 +0x59

Program exited.
*/

# 源源不断从信道接收值

发送者可通过 close(c) 关闭一个信道,表示没有需要发送的值了。

接收者可以使用 v, ok := <-ch 判断信道是否被关闭:若没有值可以接收且信道已被关闭,那么执行后 ok 会被设置为 false

循环 for i := range c 会不断从信道接收值,直到它被关闭。

注意:

  1. 信道关闭后,之前传进缓冲区的值仍可被接收;
  2. 信道关闭并没有值可以接收后,再次接收会接收到零值(如果信道没有关闭,该线程会被阻塞);
  3. 只有发送者才能关闭信道,而接收者不能。因为向一个已经关闭的信道发送数据会引发程序恐慌 (panic)。
  4. 关闭信道不是必需操作。只有在需要终止一个 range 循环等情况下需要关闭。
// 万能的斐波那契数列
package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    for i := range c {
        fmt.Println(i)
    }
}

# select 选择最先就绪的执行

select 会阻塞当前线程,直至某个分支可以继续执行,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。

也可以添加 default,如果当前没有分支可以执行,就不会阻塞当前线程而是执行 default 语句。

package main

import (
    "fmt"
    "time"
)

func main() {
    tick := time.Tick(100 * time.Millisecond)
    boom := time.After(500 * time.Millisecond)
    for {
        select {
        case <-tick:
            fmt.Println("tick.")
        case <-boom:
            fmt.Println("BOOM!")
            return
        default:
            fmt.Println("    .")
            time.Sleep(50 * time.Millisecond)
        }
    }
}

输出:

    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
BOOM!

# 互斥锁

互斥方案就类似于《操作系统——精髓与设计原理》里进程同步的信号量了。

package main

import (
    "fmt"
    "sync"
    "time"
)

// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
    v   map[string]int
    mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
    c.mux.Lock()
    // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
    c.v[key]++
    c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
    c.mux.Lock()
    // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
    defer c.mux.Unlock()
    return c.v[key]
}

func main() {
    c := SafeCounter{v: make(map[string]int)}
    for i := 0; i < 1000; i++ {
        go c.Inc("somekey")
    }

    time.Sleep(time.Second)
    fmt.Println(c.Value("somekey"))
}