吴良超的学习笔记

A Tour of Go 摘记

最近在学习 golang,本文主要是 A Tour of Go 的一些摘记,涵盖了 go 的一些基本语法与数据类型、通过 struct 和 method 实现类的特性、以及 go 中重要的 concurrency 特性。

Basics

packages, variables and functions

  • 程序的入口在 main 这个 package 中, 需要在文件头声明
  • import 通过 () 引入多个 package

    1
    2
    3
    4
    import (
    "fmt"
    "math/rand"
    )
  • 定义变量的几种形式(定义常量只能用第一种方法,且 var 改成 const)

    1
    2
    3
    4
    5
    6
    7
    // 方式 1
    var i, j int
    var i, j int = 1, 2
    var i, j int = 1, 2

    // 方式 2
    i, j := 1, 2
  • golang 中的变量类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    bool

    string

    int int8 int16 int32 int64
    uint uint8 uint16 uint32 uint64 uintptr

    byte // alias for uint8

    rune // alias for int32
    // represents a Unicode code point

    float32 float64

    complex64 complex128
  • 函数声明时类型放在最后:参数类型放在参数后,返回类型放在函数名后; 且连续多个参数类型一样时可只为最后一个写类型,如下

    1
    2
    3
    4
    5
    6
    7
    func add(x int, y int) int {
    return x + y
    }
    // same as above
    func add(x , y int) int {
    return x + y
    }
  • 函数可返回多个值, 且返回值可以被命名(此时的返回值相当函数里两个命名的变量)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 多个返回值
    func swap(x, y string) (string, string) {
    return y, x
    }

    // 具名返回值, 返回 x 和 y
    func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
    }
  • 函数可作为参数传入其他函数,也可作为返回值, 如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import (
    "fmt"
    "math"
    )

    func compute(fn func(float64, float64) float64) float64 {
    return fn(3, 4)
    }

    func main() {
    hypot := func(x, y float64) float64 {
    return math.Sqrt(x*x + y*y)
    }
    fmt.Println(hypot(5, 12))

    fmt.Println(compute(hypot))
    fmt.Println(compute(math.Pow))
    }
  • 闭包(一个函数访问并能更新在其函数域外的变量)一般会将函数作为返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    func fibonacci() func() int {
    a, b := -1, 1
    return func() int {
    a, b = b, a + b
    return b
    }
    }

    func main() {
    f := fibonacci()
    for i := 0; i < 10; i++ {
    fmt.Println(f())
    }
    }

for, if else, switch and defer

  • go 里面没有 while,for 相当于 while
  • 包含 for 的三个元素的圆括号 () 是没有的(不仅是 for 语句,if else 等其他语句也没有),且花括号 {} 总是必须的(哪怕只有一条语句)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    sum := 0
    for i := 0; i < 10; i++ {
    sum += i
    }

    // 三个元素的头尾元素可缺省
    sum := 1
    for ; sum < 1000; {
    sum += sum
    }
  • for 跟 range 结合可以遍历 slice 和 map, 每次返回两个值,对于 map 是 key:value,对于 slice 则是 index:value

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

    func main() {
    for i, v := range pow {
    fmt.Printf("2**%d = %d\n", i, v)
    }
    // 不要第一个元素
    for _, v := range pow {

    }
    // 不要第二个元素
    for i := range pow {
    }
    }
  • if 语句的判断条件中可带有一个短的声明语句

    1
    2
    3
    if v := math.Pow(3, 5); v < 100 {
    return v
    }
  • switch 语句不需要 break(实际上是go自动添加了)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    import (
    "fmt"
    "runtime"
    )

    func main() {
    fmt.Print("Go runs on ")
    switch os := runtime.GOOS; os {
    case "darwin":
    fmt.Println("OS X.")
    case "linux":
    fmt.Println("Linux.")
    default:
    // freebsd, openbsd,
    // plan9, windows...
    fmt.Printf("%s.", os)
    }
    }
  • defer 后面的语句直到其所在的函数返回才执行, 实际上 defer 后面的语句被 push 到了 stack 中,返回时就出栈

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 输出 hello world
    func main() {
    defer fmt.Println("world")
    fmt.Println("hello")
    }

    // 从 9 到 0 输出
    for i := 0; i < 10; i++ {
    defer fmt.Println(i)
    }

pointer, struct, slice and map

  • go 中的指针跟 C/C++ 中的类似

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    import "fmt"

    func main() {
    i, j := 42, 2701

    var p1 *int
    p1 = &i
    fmt.Println(*p1) // read i through the pointer
    *p1 = 21 // set i through the pointer
    fmt.Println(i) // see the new value of i

    p2 := &j // point to j
    *p2 = *p2 / 37 // divide j through the pointer
    fmt.Println(j) // see the new value of j
    }
  • go 中的结构体也跟 C/C++ 的类似,只是声明方式不一样,多了 type 这个关键字,且类型 struct 放在最后

    1
    2
    3
    4
    5
    6
    7
    8
    9
    type Vertex struct {
    X int
    Y int
    }

    func main() {
    v := Vertex{1, 2}
    fmt.Println(v.X)
    }
  • 指向结构体的指针访问结构体的元素的方式跟 C/C++ 不一样,C/C++要用 ->, go 中可直接使用 .

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    type Vertex struct {
    X int
    Y int
    }

    func main() {
    v := Vertex{1, 2}
    p := &v
    p.X = 1e9
    fmt.Println(v)
    }
  • array 与 slice 是两个不同的类型,区别在于 array 长度固定,slice 长度可变,声明时一个指定长度,一个不指定长度, 语法均是 []type

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    func main() {
    // array 声明方式一
    var a [2]string
    a[0] = "Hello"
    a[1] = "World"
    fmt.Println(a[0], a[1])
    fmt.Println(a)

    // array 声明方式二
    primes := [6]int{2, 3, 5, 7, 11, 13}
    fmt.Println(primes)

    // slice 声明方式一
    s := prime[:4]

    // slice 声明方式二
    var s []int

    // slice 声明方式三
    s := []int{2, 3, 5, 7, 11, 13}

    // slice 声明方式四
    s := make([]int, 5)
  • 需要注意的是,每个 slice 都有一个 underlying array, slice 就是这个 array 的 reference ,当 slice 被其他 array 的部分元素初始化时,修改 slice 就是在修改这个 array;slice 有两个方法:len()cap(), len() 是 slice 的长度,cap() 则是 slice 对应的 underlying array 从 slice 的第一个元素开始计算的长度

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    func printSlice(s []string) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
    }

    func main() {
    names := [4]string{
    "John",
    "Paul",
    "George",
    "Ringo",
    }
    fmt.Println(names)

    a := names[0:2]
    b := names[1:3]
    printSlice(a)
    printSlice(b)

    b[0] = "XXX"
    fmt.Println(a, b)
    fmt.Println(names)
    }

    // 输出如下
    [John Paul George Ringo]
    len=2 cap=4 [John Paul]
    len=2 cap=3 [Paul George]
    [John XXX] [XXX George]
    [John XXX George Ringo]
  • slice 是可变长的,通过 append 函数往 slice 末尾添加元素,如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    func main() {
    var s []int
    printSlice(s)

    // The slice grows as needed.
    s = append(s, 1)
    printSlice(s)

    // We can add more than one element at a time.
    s = append(s, 2, 3, 4)
    printSlice(s)
    }
  • map 初始化方式有以下几种, map 声明的中括号里面是 key 的类型,外面是 value的类型,即 map[key]value

    1
    2
    3
    4
    5
    // method 1
    m := make(map[int]int)

    // method 2
    var m = map[int]int{1:1, 2:2}
  • 删除 map 中某个元素可直接使用 delete (map, key)

  • 测试某个 key 是否在 map 中可通过 map[key] 返回一个两元组实现,即elem, ok := m[key], 如果 ok 为 true,则表示 key 里面

Methods and interfaces

  • go 没有提供类,但是可以通过为 struct 定义 method 来提供类相近的特性;method 就是在普通函数基础上定义一个 receiver 参数(定义在 func 和函数名之间),表明这个方法是属于某个 struct 的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    type Vertex struct {
    X, Y float64
    }

    func (v1 Vertex) Abs() float64 {
    return math.Sqrt(v1.X*v1.X + v1.Y*v1.Y)
    }

    func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
    }
  • reveiver 参数也可以是指针类型,这意味着通过 receiver 参数能够直接修改 struct 的值(函数的普通参数也是需要指针才能修改实参的值), 同时也不用在调用 method 时创建新的内存来存储临时对象,因此指针类型的 receiver 也更加常用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    type Vertex struct {
    X, Y float64
    }

    func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
    }

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

    func main() {
    v := Vertex{3, 4}
    v.Scale(10) // equal to (&v).Scale(10)
    fmt.Println(v.Abs())
    }
  • go 中的接口(interface) 概念与 Java中的类似,声明一系列的方法,实现了这些方法就是实现了这个接口(无需显式声明)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    type I interface {
    M()
    }

    type T struct {
    S string
    }

    // This method means type T implements the interface I,
    // but we don't need to explicitly declare that it does so.
    func (t T) M() {
    fmt.Println(t.S)
    }

    func main() {
    var i I = T{"hello"}
    i.M()
    }
  • 一个非常普遍的 interface 是 Stringer, 这是由 fmt 这个 package 中定义的,一旦实现过了这个接口里面的 String() string 方法, fmt.Println() 时就会调用这个方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    type Stringer interface {
    String() string
    }

    type Person struct {
    Name string
    Age int
    }

    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)
    }

    // 输出
    Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)
  • 类似于上面的接口 fmt.Stringer, error 也是一个常用的接口,实现该接口需要实现 Error 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import (
    "fmt"
    "time"
    )

    type MyError struct {
    When time.Time
    What string
    }

    func (e *MyError) Error() string {
    return fmt.Sprintf("at %v, %s",
    e.When, e.What)
    }

    func run() error {
    return &MyError{
    time.Now(),
    "it didn't work",
    }
    }

    func main() {
    if err := run(); err != nil {
    fmt.Println(err)
    }
    }
  • go 的很多标准库都实现了 io.Reader 这个接口,接口主要定义了这个方法 func (T) Read(b []byte) (n int, err error), 如下是一个简单地用法,每次从 string 中读取 8 个 byte

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import (
    "fmt"
    "io"
    "strings"
    )

    func main() {
    r := strings.NewReader("Hello, Reader!")

    b := make([]byte, 8)
    for {
    n, err := r.Read(b)
    fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
    fmt.Printf("b[:n] = %q\n", b[:n])
    if err == io.EOF {
    break
    }
    }
    }

Concurrency

  • goroutine 相当于是轻量级的线程,使用方法很简单 go f() 就启动了一个 goroutine 来执行函数 f()
  • channel 类似于一个队列,但是出列时要等到所有入列的操作已完成则可进行,反之亦然,这就为 goroutine 提供了 synchronize 的功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    ch := make(chan int) // create a channel of type int
    ch <- v // Send v to channel ch.
    v := <-ch // Receive from ch, and assign value to v.

    // simple example
    func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
    sum += v
    }
    c <- sum // send sum to c
    }

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

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // receive from c

    fmt.Println(x, y, x+y)
    }
    // output
    -5 17 12
  • Buffered channel:可以为 channel 指定长度(如chan := make(chan int, 100)),这样当 channel 满了之后不能再往其中写数据(再写会报错),这种 channel 也被称为 buffered channel;也可以 close 一个 channel, 这样也不能继续入列

  • 通过 range 来遍历 channel 会自动判断 channel 是否已经为空,即 for v := range channel
  • select 包含的代码块中有多个 case 语句,当其中的任一条件被满足时才会执行,否则会阻塞(可以添加 default 选项使得 select 不会被阻塞),当有多个被满足时则随机选一个;通常 select 也被用来协调多个 goroutine 的通信, 如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
    select {
    case c <- x:
    x, y = y, x+y
    case <-quit:
    fmt.Println("quit")
    return
    }
    }
    }

    func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
    for i := 0; i < 10; i++ {
    fmt.Println(<-c)
    }
    quit <- 0
    }()
    fibonacci(c, quit)
    }
  • channel 是 goroutine 通信的一个有效工具,但是除了通信,多个 goroutine 往往还会存在着同时读写一个变量的情况,这时候就要加锁,os 中也将这样的锁机制成为互斥量(mutex),go 的 sync.Mutex 就是一个能够实现加锁和解锁的互斥量;如下是多个 goroutine 共享一个 Counter 的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    import (
    "fmt"
    "sync"
    "time"
    )

    // SafeCounter is safe to use concurrently.
    type SafeCounter struct {
    v map[string]int
    mux sync.Mutex
    }

    // Inc increments the counter for the given key.
    func (c *SafeCounter) Inc(key string) {
    c.mux.Lock()
    // Lock so only one goroutine at a time can access the map c.v.
    c.v[key]++
    c.mux.Unlock()
    }

    // Value returns the current value of the counter for the given key.
    func (c *SafeCounter) Value(key string) int {
    c.mux.Lock()
    // Lock so only one goroutine at a time can access the map 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"))
    }