The tour of go

应原生态coroutine需求!

  • 代码被分组到包中(即包收集相关函数),包被分组到模块中

  • go mod init <file>创建go.mod来跟踪代码的依赖项,使用go mod init命令来创建新模块

    • go mod init example.com/myproject将会在当前目录下初始化名为example.com/project的模块
  • 名称以大写字母开始的函数可以由不在同一包中的函数调用

  • :=运算会自动推导右侧变量类型。

    1
    2
    3
    4
    message := 1
    // 等价于
    var message int
    message = 1
  • go run .会编译并运行当前目录下的go文件;go build .会编译当前目录下的go文件并生成可执行文件。

  • go mod tidy用于管理go的依赖项,根据源项目代码中实际引用的依赖项,自动更新go.mod文件的依赖列表,以及删除未引用的依赖

  • go的模块命名`/

  • $ go mod edit -replace example.com/greetings=../greetings,将模块重定向到本地路径

  • go中连续类型相同的参数,除了最后一个类型以外都可以省略类型名x int, y intx, y int

  • 使用var声明变量,类型在最后var i, j int = 1, 2可以用这种方式声明变量

  • go中只允许显式类型转换,如int(a)

  • 常量不能:=声明(因为:=涉及类型推导,常量可以是字符、字符串、布尔值、数值)

  • 基本类型包括

    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 // uint8 的别名

    rune // int32 的别名
    // 表示一个 Unicode 码点

    float32 float64

    complex64 complex128
  • for语句,go中三个构成外面没有小括号,if也一样

    1
    2
    3
    4
    // go中只有后置++
    for i := 0; i < 10; i++ {
    sum += i
    }
  • for也可以充当c中的关键字while, go中没有while

    1
    2
    3
    4
    5
    6
    7
     for sum < 1000 {
    sum += sum
    }
    // 无限循环
    for {

    }
  • if可以在条件判断前执行一个简单的语句,该语句声明的变量作用域在if内

    1
    2
    3
     if v := math.Pow(x, n); v < lim {
    return v
    }
  • switch语句,go会在每个case后自动加一个break,go中switch的case无需为常量,且取值不必为整数,也可以在判断条件之前加入一个简单的语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     switch os := runtime.GOOS; os {
    case "darwin":
    fmt.Println("OS X.")
    case "linux":
    fmt.Println("Linux.")
    default:
    // freebsd, openbsd,
    // plan9, windows...
    fmt.Printf("%s.\n", os)
    }
  • for中的无限循环和switch true一样,默认不写条件就代表true,更加清晰

    1
    2
    3
    4
    5
    6
    7
    8
    switch {
    case t.Hour() < 12:
    fmt.Println("Good morning!")
    case t.Hour() < 17:
    fmt.Println("Good afternoon.")
    default:
    fmt.Println("Good evening.")
    }
  • defer语句会函数延迟到作用域结束后执行,但其参数会立即求值

  • defer语句会将函数压入栈中,返回时按照弹栈的顺序调用执行

  • struct结构体要用type标识

    1
    2
    3
    4
    5
    6
    7
    8
    9
    type Vertex struct {
    X int
    Y int
    }
    // 初始化结构体对象
    v := Vertex{1, 2}
    v2 = Vertex{X: 1} // Y:0 被隐式地赋予
    v3 = Vertex{} // X:0 Y:0
    v.X = 4
  • 数组,**[n]T表示拥有n个T类型的值的数组声明时括号在前**

    1
    2
    3
    var a [2]string
    a[1] = "hello" // 元素赋值的方式初始化数组元素
    primes := [6]int{2, 3, 5, 7, 11, 13} // 列表方式初始化数组元素
  • 切片(没有长度的数组),**[]T表示一个元素类型为T的切片**。切片的零值是nil(长度和容量为0且没有底层数组)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var b [3]string
    // c为切片类型,将b的最后两个元素赋给c
    var c []string = b[1:2]

    var a [10]int
    // 以下切片是等价的
    a[0:10]
    a[:10]
    a[0:]
    a[:]
    • 可以通过len(s),和cap(s)来获取切片的长度和容量。类似于C++vector的长度和容量,即size()和capacity(),同样有扩容机制
    • 切片可以用make来创建
      1
      2
      a := make([]int, 5)     // 长度为5
      b := make([]int, 0, 5) // 长度为0,容量为5
    • 切片可以用append方法添加元素
      1
      2
      3
      4
      5
      6
      // 添加一个空切片
      s = append(s, 0)
      // 这个切片会按需增长
      s = append(s, 1)
      // 可以一次性添加多个元素
      s = append(s, 2, 3, 4)
  • for范围循环,遍历切片时,每次迭代都会返回两个值<下标,值的副本>,关键字有range

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

    // 只获取索引
    for i := range pow {}

    // 可以将下标赋予"_"来忽略它(防止编译器报错)
    for i, _ := range pow {}
  • map,声明hash := make(map[Keytype]ValueType),要使用make来创建

    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
     type Vertex struct {
    Lat, Long float64
    }

    var m = map[string]Vertex{
    "Bell Labs": Vertex{
    40.68433, -74.39967,
    },
    "Google": Vertex{
    37.42202, -122.08408,
    },
    }

    // 初始化map对象m,键类型为string,值为int
    m := make(map[string]int)
    // 可以用map[type]bool代表set,它是无序的

    // 插入键值对
    m["Answer"] = 42
    // 修改键的值
    m["Answer"] = 48
    // 删除键值对
    delete(m, "Answer")
    // 返回的ok是bool类型,用于检测某个键是否存在
    v, ok := m["Answer"]
    • strings.Fields()函数用于将字符串按照空格分割成一个字符串切片(即由多个子串组成的序列),其中每个子串都是一个单词
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // 统计单词次数
      func WordCount(s string) map[string]int {
      hash := make(map[string]int)
      // 将字符串按空格分割成单词
      words := strings.Fields(s)
      for _, ch := range words {
      hash[ch]++
      }
      return hash
      }
  • 函数指针,其类型需要加上参数类型和返回类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 可以注意到传入的函数指针变量是带有,其函数的返回类型和参数类型的
    func compute(fn func(float64, float64) float64) float64 {
    // 调用函数指针变量fn
    return fn(3, 4)
    }

    func main() {
    // hypot为函数指针变量
    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))
    }
  • lambda表达式

    1
    2
    3
    4
    5
    6
    7
    8
    func adder() func(int) int {
    sum := 0
    // 返回为lambda表达式(即作为一个函数指针变量)
    return func(x int) int {
    sum += x
    return sum
    }
    }
  • go中没有类

  • go中的方法(即成员函数), 即在func关键字和方法名之间放入方法接收者(即调用者) ;函数返回为空不需要写返回类型

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

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

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

    func main() {
    v := Vertex{3, 4}
    // 调用方法
    v.Abs()
    // 调用函数
    Abs(v)
    }
  • go中要想实现像cpp那样传引用或指针修改值,总的说要想传指针,取决于参数,go语言会自动这里帮助推导类型,省去很多麻烦

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
     // 注意指针的声明
    func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
    }
    v := Vertex{3, 4}
    // 方法调用时也不需要写成->
    // 调用者既能作为值又能作为指针
    v.Scale(10) // 会被解释为(&v).Scale(10)

    // 或者
    func (v Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
    }
    s := &Vertex{2, 3}
    s.Scale(10) // 会被解释为(*s).Scale(10)
  • 接口

    • 接口是一组方法签名定义的集合,将对应的方法放进接口中,即方法接收者后面的方法名和返回类型,然后将调用者赋给接口变量,这时候就可以利用接口变量调用对应的方法(但注意这时候调用者没有像上面那样的推导,得传入对应的值或指针)
      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
      type Abser interface {
      Abs() float64
      }

      func main() {
      // 接口变量
      var a Abser
      f := MyFloat(-math.Sqrt2)
      v := Vertex{3, 4}

      a = f // a MyFloat 实现了 Abser
      a = &v // a *Vertex 实现了 Abser

      // 下面一行,v 是一个 Vertex(而不是 *Vertex),没有推导
      // 所以没有实现 Abser。
      a = v

      // 根据接口变量来调用对应版本的方法
      fmt.Println(a.Abs())
      }

      type MyFloat float64

      func (f MyFloat) Abs() float64 {
      if f < 0 {
      return float64(-f)
      }
      return float64(f)
      }

      type Vertex struct {
      X, Y float64
      }

      func (v *Vertex) Abs() float64 {
      return math.Sqrt(v.X*v.X + v.Y*v.Y)
      }
    • 可以利用格式符%v%T来获得接口变量的值和返回类型
    • 如果接口变量没有指定具体的调用者,即不能指明具体调用哪个方法,则会返回nil运行时错误
    • 空接口,可以保存任何类型的值,接口也是值!
      1
      interface{}
    • 使用类型断言 t:= i.(T)可以获得接口的值,以及是否保存该特定类型的bool变量
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      var i interface{} = "hello"

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

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

      f, ok := i.(float64)
      fmt.Println(f, ok) // ok为false,f将为float64类型的零值,不会产生panic
    • i.(type),使用关键字type可以获得当前接口变量的类型
    • fmt包通过Stinger接口来打印值
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      // type Stringer interface {
      // String() string
      // }
      type Person struct {
      Name string
      Age int
      }

      // 调用者为Person
      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)
      }

  • go协程

    1
    2
    // 新建一个协程并执行该函数
    go f(x, y, z)
  • channel,类似于管道,没有数据传输会阻塞,也可以指定带缓冲的管道的缓冲大小

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
    sum += v
    }
    c <- sum // 将和送入 c
    }

    func main() {
    // 新建一个数组
    s := []int{7, 2, 8, -9, 4, 0}

    // 使用chan新建传输int类型的channel
    c := make(chan int)
    // 带缓冲的channel,缓冲区填满之后再往里面填会阻塞(这时候会报错)
    ch := make(chan int, 100)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // 从 c 中接收

    fmt.Println(x, y, x+y)
    }
  • go中可以直接使用IO多路复用select,就像用switch一样舒服,如果没有任务就会阻塞(当然也可以设置以下default,channel上没有信号的时候就执行default)

    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
    func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
    select {
    // 利用IO多路复用,等待channel上的信号
    case c <- x:
    x, y = y, x+y
    case <-quit:
    fmt.Println("quit")
    return
    defualt:
    XXX
    }
    }
    }

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