我的Go语言之旅

前言

关于本篇文章的几点说明:

  • 本文的内容基本都来自网站:Go旅行中文版

  • 同时文章中,也加入了少量自己的认知(结合自身目前所 “擅长” 的Java语言的特点进行了简单的类比),这些认知,或多或少的也会存在一定的误区或者不当的地方,欢迎擅长Go语言的你,来为我指正或纠错,我定不胜感激

  • 至于为什么会写下这篇看似 “剽窃” 的博文呢,因为我本人坚信 “好记性,不如烂笔头” 的观点(当然我本人也没有完全履行这个信念,但是我还是很认可这个!)。

  • 最后呢,我也希望这篇文章或多或少的能给正在学习Go我们一点记忆上的帮助或对Go的理解!

初见Go语言

  • 在Go语言的编程中,并不强制要求在单条语句结束后,通过“;”号来结尾,这种使用“;”号结尾的方式采取程序员自愿,存在与否对程序并没有什么改变!

    特别声明: 如果将多条语句写在了一行中,那么必须通过“;”来进行分割,否则会出现程序错误提示

  • 导出:在Go语言中,大写字母开头的方法名或者变量名表示是已导出的,当我们在程序中导出一个包后,我们只能引用已导出的名字,未导出的名字在是不允许在包外被引用的

  • 函数:函数可以没有参数或接收多个参数,定义函数参数变量时,变量名字在前,类型在后(这点与java声明变量方式完全不同), 函数对应的返回值在函数的参数列表后声明(这点与java语言的函数命名规则也不同),当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。示例如下:

    func add(x int, y int) int {
        return x + y
    }
    
    // 下面的程序中,函数add_1和add_2具有相同的能力,只是在命名方式和定义上存在差异而已
    func add_1(x int, y int) int {
        return x + y
    }
    
    func add_2(x,y int) int {
        return x + y
    }
    
  • 函数返回值:在Go语言中,函数可以返回任意数量的返回值这一点与lua很类似噢~),如下示例中,swap 函数返回了两个字符串:

    package main
    
    import "fmt"
    
    func swap(s1,s2 string) (string, string) {
        return s2, s1
    }
    
    func main() {
        a, b := swap("str_1", "str_2")
        fmt.Printf("a=%s,b=%s", a, b)
    }
    
  • 命名返回值: Go 的返回值可被命名,它们会被视作定义在函数顶部的变量。返回值的名称应当具有一定的意义,它可以作为文档使用.没有参数的 return 语句返回已命名的返回值。也就是直接返回。直接返回语句应当仅用在下面这样的短函数中。在长的函数中它们会影响代码的可读性。

    package main
    
    import "fmt"
    
    func namedReturnValue(sum int) (x, y int) {
        x = sum * 4 - 9
        y = sum - x;
        return
    }
    
    func main() {
        x, y := namedReturnValue(17)
        fmt.println(x, y)
    }
    
  • 变量定义:var 语句用于声明一个变量列表,跟函数的参数列表一样,类型在最后。就像在这个例子中看到的一样,var 语句可以出现在包或函数级别。

    package main
    
    import "fmt"
    
    var c, python, java bool
    
    func main() {
        var i int
        fmt.Println(i, c, python, java)
    }
    
    
  • 变量的初始化:变量声明可以包含初始值,每个变量对应一个。如果初始化值已存在,则可以省略类型;变量会从初始值中获得类型。

    package main
    
    import "fmt"
    
    var i, j int = 1, 2
    
    func main() {
        var c, python, java = true, false, "no!"
        fmt.Println(i, j, c, python, java)
    }
    
  • 短变量声明: 在函数中,简洁赋值语句:=可在类型明确的地方代替var声明。函数外的每个语句都必须以关键字开始(var, func 等等),因此 :=结构 不能在函数外使用

package main

import "fmt"

func main() {
    var i, j int = 1, 2
    k := 3
    c, python, java := true, false, "no!"
    fmt.Println(i, j, k, c, python, java)
}
  • 常量:在Go语言中,定义常量与普通变量的区别在于关键字不同,常量通过关键字cost声明,常量可以是字符、字符串、布尔值或数值。常量不能用 := 语法声明
package main

import "fmt"

cost pi = 3.14

func main() {
    fmt.Println("cost var pi=", pi)
}
  • for循环:Go语言中的for循环与C,Java,JavaScript的使用方式存在一定不同点,那就是他的for语句不需要“()”,但是“{}”却是必须的。初始化语句和后置语句是可选的。for 是 Go 中的 “while”,此时你可以去掉分号,因为 C 的 while 在 Go 中叫做 for。如果省略了循环条件,那么循环语句就变成了“无限循环”,也就是广为人知的死循环!

    package main
    
    import "fmt"
    
    func main() {
      sum := 0
      for i := 0; i < 10; i++ {
          sum += i
      }
      fmt.Println(sum)
      sum_2 := 0
      // 省略了初始化语句和后置语句的for语句
      for ; i < 100;{
          sum += i
      }
      fmt.Println(sum_2)
      sum_3 := 1
      // 这就是Go语言的的while语句,伪for语句
      for sum_3 < 1000 {
          sum_3 += sum_3
      }
      fmt.Println(sum_3)
      // 死循环的书写方式
      for {}
    }
    
  • if语句:Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。if 的简短语句
    同 for 一样, if 语句可以在条件表达式前执行一个简单的语句。该语句声明的变量作用域仅在 if 之内。

    package main
    
    import (
      "fmt"
      "math"
    )
    
    func sqrt(x float64) string {
        if x < 0 {
            return sqrt(-x) + "i"
        }
        return fmt.Sprint(math.Sqrt(x))
    }
    
    func main() {
        fmt.Println(sqrt(2), sqrt(-4))
    }
    
  • switch语句:switch 是编写一连串 if - else 语句的简便方法。它运行第一个值等于条件表达式的 case 语句。Go 的 switch 语句类似于 C、C++、Java、JavaScript 和 PHP 中的,不过 Go 只运行选定的 case,而非之后所有的 case。 实际上,Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough 语句结束,否则分支会自动终止。witch 的 case 语句从上到下顺次执行,直到匹配成功时停止。 Go 的另一点重要的不同在于 switch 的 case 无需为常量,且取值不必为整数。没有条件的 switch 同 switch true 一样。这种形式能将一长串 if-then-else 写得更加清晰。代码使用样例如下:

    package main
    
    import (
        "fmt"
        "runtime"
    )
    
    func main() {
        fmt.Print("当前程序所运行的操作系统环境是:")
        switch os := runtime.GOOS; os {
        case "darwin":
            fmt.Println("OS X.")
        case "linux":
            fmt.Println("Linux.")
        default:
            fmt.Printf("%s.\n", os)
        }
    
      t := time.Now()
      switch {
      case t.Hour() < 12:
          fmt.Println("Good morning!")
      case t.Hour() < 17:
          fmt.Println("Good afternoon.")
      default:
          fmt.Println("Good evening.")
    }
    
    
  • switch-true:在Go语言中的switch-true语句等同于一条if-then-else语句,这种写法可以将一连串的if-then-else语句写的更加清晰可见

    package main
    
    import "fmt"
    
    func main() {
        i := 20
        switch {
        case i < 10:
          fmt.Println("the num < 10")
        case i == 15:
          fmt.Println("the num == 15")
        case i >= 20:
          fmt.Println("the num >= 20")
        default:
          fmt.Println("the num is:", i)
        }
    }
    
  • defer关键字:defer语句会将函数推迟到外层函数返回之后执行。推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

    package main
    
    import "fmt"
    
    func main() {
        defer fmt.Println("golang!")
        fmt.Print("hello ")
    }
    
  • defer栈:推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。

    package main
    
    import "fmt"
    
    func main() {
        fmt.Println("counting")
        for i :=0; i < 10; ++i {
            defer fmt.Println(i)
        }
        fmt.Println("done")
    }
    
  • 指针:Go拥有指针。指针保存了值的内存地址。类型*T是指向T类型值的指针。其零值为nil&操作符会生成一个指向其操作数的指针。*操作符表示指针指向的底层值。这也就是通常所说的“间接引用”或“重定向”。与 C 不同,Go 没有指针运算。

    package main
    
    import "fmt"
    
    func main() {
        i, j := 42, 2701
        p := &i // 指向i
        fmt.Println(*p) // 通过指针读取i的值
        *p := 21 // 通过指针设置i的值
        fmt.Println(i) // 查看i的值
    
        p := &j // 指向j
        *p := *p / 37 // 通过指针对j进行除法运算
        fmt.Printlb(j) // 查看j的值
    }
    
  • 切片的创建方式:通过Go提供的内建make函数来创建,切片的数据也可以是一个切片,同样可以通过append内建函数来动态的像切片中添加数据。当切片容量不足时,内部会创建一个新的大容量数据来接收数据

    package main
    
    import "fmt"
    
    func main() {
        pow := make([]int, 10)
        for i := 0; i < cap(pow); i++ {
            pow[i] = 1 << uint(i) // 移位运算
        }
        fmt.Printf("type=%T, len=%d, cap=%d, pow=%v", pow, len(pow), cap(pow),  pow)
        // 追加一个元素
        pow = append(pow, 1024)
        fmt.Printf("type=%T, len=%d, cap=%d, pow=%v", pow, len(pow), cap(pow),  pow)
    }
    
  • for-range迭代:这种遍历方式用于对切片映射进行数据遍历。rang遍历会返回两个数据结果,第一个是当前元素的key(在映射中是key,在切片中式索引下标),第二个元素是数据副本。我们在使用该方式进行遍历的同时,可以有选择的接收返回值,忽略不需要的返回值信息,忽略的方式有两种:一种是采用_来占位来替代我们不需要的返回值,另一种是仅接收索引值

    package main
    
    import "fmt"
    
    func main() {
        pow := make([]int, 10)
        for i := 0; i < cap(pow); i++ {
            pow[i] = 1 << uint(i) // 移位运算
        }
        // 忽略索引
        for _, val := range pow {
            fmt.Print(val, "\t")
        }
        fmt.Println()
        // 忽略value副本
        for idx, _ := range pow {
            fmt.Print(idx, "\t")
        }
        fmt.Println()
        // 仅接收索引
        for idx := range pow {
            fmt.Print(idx, "\t")
        }
    }
    

    range的遍历有点像js,python中的for-in的用法了,但是两者还是存在区别的

  • 映射: 映射将键映射到值。映射的零值为nilnil映射既没有键,也不能添加键。make函数会返回给定类型的映射,并将其初始化备用。映射的文法与结构体相似,不过必须有键名。

    package main
    
    import "fmt"
    
    type Person struct {
        name string
        age int
    }
    
    var m map[string]Person
    
    func main() {
        // make函数定义
        m = make(map[string]Person)
        m["zhangsan"] = Person{name: "张三", age: 23}
        fmt.Println(m)
        fmt.Println(m["zhangsan"])
    
        // 采用文法定义
        m2 := map[string]Person{
            "zhangsan": Person { // 这里的Person可以省略
                name: "张三",
                age: 23, // 这里的逗号不能少
            },
            "lisi": Person {
                name: "李四",
                age: 22,
            }, // 这里的逗号不能少
        }
        fmt.Println(m2)
        fmt.Println(m2["zhangsan"], "\t", m2["lisi"])
        // 添加元素
        m2["key-1"] = Person{"wangwu", 16}
        // 获取元素
        key1Val = m2["key-1"]
        // 删除元素
        delete(m2, "key-1")
        // 通过双赋值检测某个键是否存在:
        elem, ok := m["key-2"]
        // 若 key 在 m 中,ok 为 true ;否则,ok 为 false。
        // 若 key 不在映射中,那么 elem 是该映射元素类型的零值。
        // 同样的,当从映射中读取某个不存在的键时,结果是映射的元素类型的零值。
    }
    

    映射这种数据结构类似java中的map。

  • 函数值: 函数也是值。它们可以像其它值一样传递。函数值可以用作函数的参数或返回值。用例代码如下:

    package main
    
    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))
    }
    
  • 函数的闭包:Go函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。

    package main
    
    import "fmt"
    
    func adder() func(int) int {
        sum := 0
        return func (x int) int {
            sum += x
            return sum
        }
    }
    
    // 斐波那契数列
    func fibonacci() func() int {
        n1, n2 := 0, 1
        return func() int {
            t := n1
            n1, n2 := n2, (n1 + n2)
        }
    }
    
    func main() {
        pos, neg := adder(), adder()
        for i := 0; i < 10; i++ {
            fmt.Println(pos(i), neg(-2 * i))
        }
    
        fmt.Println("---------斐波那契数列--------")
        f := fibonacci()
        for i := 0; i < 10; i++ {
            fmt.Print(f(), "\t")
        }
    }
    
  • 方法:Go 没有类。不过你可以为结构体类型定义方法。方法就是一类带特殊的 接收者 参数的函数。方法接收者在它自己的参数列表内,位于func关键字和方法名之间。在此例中,run方法拥有一个名为v,类型为Animal的接收者。方法即函数,记住:方法只是个带接收者参数的函数。现在这个run2的写法就是个正常的函数,功能并没有什么变化。你也可以为非结构体类型声明方法。在此例中,我们看到了一个带Abs方法的数值类型 MyFloat。你只能为在同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括 int 之类的内建类型)的接收者声明方法。(就是接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明方法。

    package main
    
    import "fmt"
    
    type Animal struct {
        name string
    }
    
    // 定义自己的类型
    type MyFloat float64
    
    // 携带接收者的方法
    func (a Animal) run() {
        fmt.Println(a.name, ", 在奔跑!")
    }
    
    // 普通方法
    fun run2(a Animal) {
        fmt.Println(a.name, ", 在疾跑!")
    }
    
    // 为自己的类型定义方法
    func (mf MyFloat) Abs() float64 {
        if f < 0 {
            return float64(-f)
        }
        return float64(f)
    }
    
    func main() {
        // run与run2在功能上并没有什么差异
        cat := Animal{"蓝猫"}
        cat.run()
        run2(cat)
    
        fmt.Println("----------华丽的分割线---------")
        f := MyFloat(-math.Sqrt2)
        fmt.Println(f.Abs())
    }
    
    • 当我们在为自定义类型进行方法声明时,必须在包内作用域,可把包看作一个可见度的定义,同包内的类型都是互相可见的,即不需要包的声明或导入来引用类型
    • 这里我们简单的解释一下何为方法的接收者,当我们定义一个方法时,这个方法没有定义具体的接收者,那么可以把它看作是定义在当前包方位内的方法(全局方法,这里的全局为包的范围)。(如果你对javascript语言有一定的了解,你可能会看到有些开发人员会给window对象进行方法定义,这里只是类比一下,并不是说两种方式完全的等价)
  • 指针接收者与值接收者:首先在Go的语言中存在两类主要的值:一种是引用值(通过引用具体值所对应的内存地址,来达到数据引用的作用);另一种是具体值(为什么叫具体值,因为表示的是具体的数据,而非数据的一种间接引用表示,区别于指针、映射、切片等类型)。简单的学习了Go语言之后,我们都知道,在这个Go的世界里,并不存在类的概念,当时却存在方法的概念,且方法适合具体的接收者所绑定在一起的。这就诞生了两类接收者:指针接收者值接收者,这两类接收者的具体区别并不在对方法的调用上,因为值接收者可以调用该值类型指针所绑定的方法,反之一样可以!如何更好的去理解这两类接收者呢?其实很简单:就是你要实现的方法(绑定的方法)是要需要进行原始数据共享,还是进行数据Copy,也就是方法在操作时不需要或不会去修改方法所绑定的接收者对应所对应的原始数据值

    • 在Go的底层实现中,会自动帮我们进行类型匹配转化操作,这种做法的好处是,当我们通过接口来调用方法时,就无需关心接口方法绑定的是值接收者还是指针接收者
    • 这里对两种方法接收者的阐述,仅属于我个人的小见解,希望对你还是有所帮助(也欢迎指正交流~)
  • 接口:接口类型 是由一组方法签名定义的集合。接口类型的变量可以保存任何实现了这些方法的值。我们可以在代码中通过interface关键字来声明接口。类型通过实现一个接口的所有方法来实现该接口(不可以偷懒噢~)。接口也是值。它们可以像其它值一样传递。在内部,接口值可以看做包含值和具体类型的元组:·(value, type),接口值保存了一个具体底层类型的具体值。接口值调用方法时会执行其底层类型的同名方法。接下来,我们通过下面的代码示例来看看如何使用和定义接口:

      package main
    
      import (
          "fmt"
          "math"
      )
    
      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
    
          // 这里段代码在执行时,会出现报错:cannot use v (variable of type Vertex) as type Abser in assignment:Vertex does not implement Abser (Abs method has pointer receiver)
          // 之所以报错是因为,我们的值类型 v 并没有实现 Abs 方法
          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)
      }
    
      // 以下代码演示了接口作为值应如何使用
      type I interface {
          M()
      }
    
      type T struct {
          S string
      }
    
      func (t *T) M() {
          fmt.Println(t.S)
      }
    
      type F float64
    
      func (f F) M() {
          fmt.Println(f)
      }
    
      func main() {
          var i I
    
          i = &T{"Hello"}
          describe(i)
          i.M()
    
          i = F(math.Pi)
          describe(i)
          i.M()
      }
    
      func describe(i I) {
          fmt.Printf("(%v, %T)\n", i, i)
      }
    
    • 接口值可以用作函数的参数或返回值Go中的接口可以看作是一种规则约束或者功能接口定义,任何接收者实现了某个接口定义的方法,都可以接口来调用接收者实现的方法。这种定义和使用方式与常规的面向对象的编程语言还是有一定的差异化的!__
    • 保存了nil具体值的接口其自身并不为nil,依然可以通过接口值来调用目标方法
    • nil接口值 既不保存值也不保存具体类型。为nil接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个具体方法的类型。
    • 空接口:指定了零个方法的接口值被称为“空接口”,空接口可保存任何类型的值,空接口被用来处理未知类型的值。 示例:interface{}(有点像Java中的Object基类)
  • 类型断言:提供了访问接口值底层具体值的方式。为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。演示代码如下:

     package main
    
     import "fmt"
    
     func main() {
         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)
    
         f := i.(float64) // 报错,panic: interface conversion: interface {} is string, not float64
         fmt.Println(f)
     }
    
  • 类型选择:是一种按顺序从几个类型断言中选择分支的结构。类型选择与一般的switch语句相似,不过类型选择中的case 为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。类型选择中的声明与类型断言i.(T)的语法相同,只是具体类型T替换成了关键字type。此选择语句判断接口值 i 保存的值类型是 T 还是 S。在 TS 的情况下,变量v 会分别按 TS类型保存 i 拥有的值。在默认(即没有匹配)的情况下,变量 vi 的接口类型和值相同。演示代码如下:

    package main
    
    import "fmt"
    
    func typeMatch(i interface{}) {
        switch v := i.(type) {
        case int: fmt.Println("type is int! value=", v)
        case bool: fmt.Println("type is bool! value=", v)
        case string: fmt.Println("type is string! value=", v)
        default: fmt.Println("type is interface{}! value=", v)
        }
    }
    
    func mian() {
        typeMatch("hello, golang")
        typeMatch(true)
        typeMatch(32)
        typeMatch(10e2)
    }
    
    
  • Stringerfmt包中定义的Stringer是最普遍的接口之一。Stringer是一个可以用字符串描述自己的类型。fmt包(还有很多包)都通过此接口来打印值。该接口定义及示例代码如下:

    // type Stringer interface {
    //    String() string
    // }
    package main
    
    import "fmt"
    
    type Person struct {
        Name string
        Age int
    }
    
    // 实现Stringer接口
    func (p Person) String() string {
        return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
    }
    func main() {
        p1 := Person{"tom", 17}
        p2 := Person{"alice", 21}
        fmt.Println(p1, p2)
    }
    
  • error值:与 fmt.Stringer类似,error类型是一个内建接口。通常函数会返回一个error值,调用的它的代码应当判断这个错误是否等于nil来进行错误处理。error 为 “nil” 时表示成功;非 “nil” 的error表示失败。error使用样例如下:

      package main
    
      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)
          }
      }
    
  • Reader:io 包指定了 io.Reader 接口,它表示从数据流的末尾进行读取。Go 标准库包含了该接口的许多实现,包括文件、网络连接、压缩和加密等等。io.Reader 接口有一个Read方法:func (T) Read(b []byte) (n int, err error)。Read用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个 io.EOF 错误。示例代码创建了一个 strings.Reader 并以每次 8 字节的速度读取它的输出。

      package main
    
      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
              }
          }
      }
    
  • Go 程Go 程(goroutine)是由 Go 运行时管理的 轻量级线程。Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。sync 包提供了这种能力,不过在 Go 中并不经常用到,因为还有其它的办法。一个简单的Go程示例如下所示:

      package main
    
      import (
          "fmt"
          "time"
      )
    
      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")
      }
    
  • 信道:信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。(这里的“箭头”就是数据流的方向。有没有想到linux中的“<”符号)和映射与切片一样,信道在使用前必须创建:ch := make(chan int)。默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。带缓冲的信道,信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道:ch := make(chan int, 100)。仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。以下示例对切片中的数进行求和,将任务分配给两个 Go 程。一旦两个 Go 程完成了它们的计算,它就能算出最终的结果。

      package main
    
      import "fmt"
    
      //ch <- v    // 将 v 发送至信道 ch。
      //v := <-ch  // 从 ch 接收值并赋予 v。
    
      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}
    
          c := make(chan int)
          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)
          fmt.Println("----------华丽的分割线---------")
          // 修改示例填满缓冲区,然后看看会发生什么。
          ch := make(chan int, 2)
          ch <- 1
          ch <- 2
          //ch <- 3
          //ch <- 4
          fmt.Println(<-ch)
          fmt.Println(<-ch)
      }
    
  • range 和 close:发送者可通过 close 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完:v, ok := <-ch,之后 ok 会被设置为 false。循环 for i := range c 会不断从信道接收值,直到它被关闭。

      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看看会发生什么
          close(c)
      }
    
      func main() {
          c := make(chan int, 10)
          go fibonacci(cap(c), c)
          for i := range c {
              fmt.Println(i)
          }
      }
    
    • 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。
    • 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环
  • select 语句:select 语句使一个 Go 程可以等待多个通信操作。select 会阻塞某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。默认选择,当 select 中的其它分支都没有准备好时,default 分支就会执行。为了在尝试发送或者接收时不发生阻塞,可使用 default 分支。

      package main
    
      import (
          "fmt"
          "time"
      )
    
      // select 语句的模板:
      //select {
      //case i := <-c:
      //    // 使用 i
      //default:
      //    // 从 c 中接收会阻塞时执行
      //}
    
      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程的c,quit通道执行顺序看看
          go func() {
              for i := 0; i < 10; i++ {
                  fmt.Println(<-c)
              }
              quit <- 0
          }()
          fibonacci(c, quit)
          fmt.Println("----------华丽的分割线---------")
          // 执行下面的代码,可以尝试注释上面的代码(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)
              }
          }
      }
    

多出去走走,你会发现每次都有不同的心境!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值