Files
rikako-note/Golang/Golang基础.md
2022-05-19 21:49:25 +08:00

27 KiB
Raw Blame History

Golang基础

  • Golang中的包机制

    • Golang程序是由包组成的并且在名为main的包中开始运行
    • 包的导入
      • 包可以通过import语句进行导入在导入多个包时可以用()将导入的多个包扩起来,并且每个包占据一行
      import 
          fmt"
          "math"
          )
      
  • 包的导出
    • 在包中如果一个名字以大写字母开头那么这个名字将会被导出如math.Pi会被导出而math.pi则不会被导出
    • 在导入包时,只能够引用那些包中被导出的变量
  • Golang中函数

    • 在Golang中函数可以接受一个或者多个参数但是函数参数中参数类型位于形参名称之后
    func f(msg string,n int) {
        fmt.Println(msg,n)
    }
    
    • 在Golang中连续的函数参数如果都是同一种类型那么这些连续参数的类型都可以省略但是在相同类型的最后一个参数末尾要指明参数类型
    func f(a,b int,msg string) {
        fmt.Println(a,b,msg)
    }
    
    • Golang函数的多值返回
      • Golang函数可以返回任意数量的返回值
    func f() (string,string) {
        return "fuck","shit"
    }
    
    a,b:=f()
    // a:"fuck",b:"shit"
    fmt.Println(a,b)
    
    • Golang函数可以为返回值进行命名其会被视作在函数顶部定义的变量可以在函数体中引用变量并且对其进行修改
    • Golang函数中如果使用了命名返回值那么在return语句后没有跟随其他值时会将命名返回值变量当前的值
    func f() (retVal int) {
        // retVal值默认是0
        fmt.Println(retVal)
        retVal=20
        // return语句会默认返回retVal变量的值返回值为20
        return
    }
    
  • Golang中的变量

    • Golang中变量的声明可以通过var关键字并且在i变量名之后跟随类型关键字
    var a int
    
    • Golang在声明变量时可以为变量指定一个初始化器在为每个变量指定初始化器后变量的类型可以被省略
    var a,b,c=1,2,"kazusa"
    fmt.Println(a,b,c)
    
    • Golang允许在函数内通过:=符号来声明一个变量,并且其变量的默认类型为:=后跟随的值类型
    // inside a func
    a := 1
    fmt.Println(a)
    // 在函数外部,这种变量声明的方式不被允许
    
  • Golang中的基本类型

    • Golang中基本类型如下
      • bool
      • string
      • int int8 int16 int32 int64
      • uint uint8 uint16 uint32 uint64 uintptr
      • byte 和uint8相同
      • rune 和uint32相同用来代表unicode字符码位
      • float32 float64
      • complex64 complex128 (复数)
      // 在Golang中可以采用和import一样的格式来声明
      //  多个不同类型的变量
      var (
          a,b int=1,2
          msg string="fuck"
      )
      
    • 各类型变量在未赋初始值时的默认值
      • 数值型(整数、浮点数) 0
      • bool false
      • string 空字符串
    • Golang中的类型转换
      • 在Golang中在变量声明时如果显式指定了变量的类型那么在后续为该变量指定初始化器或者赋值时等式右边的类型必须和等式左边的类型相同否则应该显式制定类型转换
      var a int=1
      var b int8=int8(a)
      
    • Golang中类型推断
      • 如果在声明变量时,没有为变量显式指定类型,那么将会根据等式右边的值来推断变量类型
        • 通过 := 表达式通过等式右边表达式进行推断
        • 在var关键字右边通过初始化器来推断类型
      // 默认情况下整数类型的字面量将会被推断为int
      // 浮点数类型的字面量将会被推断为float64
      // 复数类型将会被推断为complex128
      var a = 1 // a被推断为int类型
      var f = 1.0 // f被推断为float64类型
      
  • Golang中的常量

    • Golang中常量的声明和变量类似但是使用const关键字
    • 常量可以是bool、string、数值、字符
    • 常量的声明无法使用 = 方式
    const (
        a,b int=1,2
        c string="shiro"
    )
    fmt.Println(a,b,c)
    
  • Golang中的循环

    • 在Golang中只存在有一种循环for循环。
    • Golang中for循环结构如下
    // 其结构类似与java和c中的for循环
    //    其中init和post部分是可选的
    //    for ;condition expression; {}   
    for init statement;condition expression;post statement {
        // loop body
    }
    
    • 在Golang中while循环结构如下
    for expression {
    
    }
    
    • 在Golang中如果想实现无限循环expression可以被省略
    for {
        // infinite loop
    
    }
    
  • Golang中的条件分支

    • 在Golang中if的条件表达式不需要用小括号扩起来但是if后跟的语句块必须用大括号包起来
    if condition-A-expression {
        // condition A
    } else if condition-B-expression {
        // condition B
    } else {
        // condition else
    }
    /**
      *  此时else if/else和上一个条件的'}'符号必须位于同一行
    **/
    
    • 在Golang中的if语句中可以在条件表达式之前添加一个执行语句在执行语句中定义的变量只能在if和else if/else块中被访问
    if msg:="msg content";expr1 {
        fmt.Println(msg)
    } else {
        fmt.Println(msg)
    }
    
  • Golang中的switch语句

    • 在Golang中并不需要为switch中的每个case手动添加breakGolang的switch只会执行第一个匹配的case后的语句
    • 在Golang中case条件后跟的不一定非要是常量也可以是变量或者是函数调用
    • 在Golang中case后跟的也不一定是整形而可以是其他类型
    func f() {
        return 0
    }
    
    // ...
    a:=0
    switch a {
        case f():
          fmt.Println("matched")
        default:
          fmt.Println("missed")
    }
    
    • 在Golang中switch语句可以不跟条件表达式其默认和switch true相同
    switch{
        case condition-1:
          // ...
        case condition-2:
          // ...
        default:
          // ...
    }
    
    if condition-1 {
        //...
    }else if condition-2 {
        //...
    }else {
        //...
    }
    相同
    
  • Golang中的defer

    • 在Golang中可以用defer关键字将函数的执行推迟到当前defer语句所处的函数执行完成之后
    • 在Golang中defer关键字修饰的函数调用其参数是立即被计算的但是函数调用会被推迟到外围函数执行结束之后
    • Golang中defer推迟的函数调用将会被压入到栈结构中如果在同一个函数中存在多个defer调用那么在函数执行结束后后被压入栈结构的defer调用会先被调用
    func f() {
        fmt.Println("function begin")
    
        defer fmt.Println(1)
        defer fmt.Println(2)
        defer fmt.Println(3)
    
        fmt.Println("function end")
    }
    // 在调用f()后,其输出会按如下顺序
    //    function begin
    //    funtion end
    //    3
    //    2
    //    1
    
  • Golang中的指针

    • Golang中指针的声明
    var p *int
    
    • Golang中的取址操作
    var (
        i int = 1
        p *int = &i
    )
    
    • Golang中的解引用操作
    var (
        i int = 1
        p *int = &i
    )
    fmt.Println(*p)
    *p=2
    fmt.Println(*p)
    
    • Golang中并不存在指针的算数如通过+或者-一个指针将指针地址向前、向后推移
  • Golang中的struct

    • Golang中struct的定义
    type Node struct {
        val int
        next *int
    }
    
    fmt.Println(Node{1,2})
    
    • Golang结构体中域的引用
    type Result struct {
        retCode int
        msg string
    }
    
    var result Result=Result{200 "response success"}
    fmt.Println(result.retCode,result.msg)
    
    • 通过指针访问结构体中的内容
    node := Node{0,nil}
    // 要想通过指针访问结构体中的域,可以如下所示
    fmt.Println((*node).val)
    // 也可以直接在指针后调用.
    fmt.Println(node.val)
    
    • 结构体字面量
    // 可以通过为每个域按顺序指定值
    var result Result=Result{
        200,
        "请求成功"
    }
    // 可以通过filedName:value来为域的子集指定值
    //    其他未指定的值为默认值
    var result Result=Result{
        // retCode未指定默认为0
        msg:"请求成功"
    }
    // 可以不为域指定值,域中所有值都为默认值
    result := Result{}
    
  • Golang中的数组

    • Golang中数组类型为[n]T代表长度为n的T类型的数组
    • Golang中的数组无法被扩容
    • sliceslice中包含数组中元素的子集可以作为数组的一个动态视图通过slice比数组使用的更为广泛
    • slice的类型为[]TT是元素的类型
    • slice本身并不存储任何元素其只是一个针对数组中一定范围内元素的引用
    • 在slice中对元素进行的更改在数组中也可见在数组中对元素进行的更改同样在slice中可见
    • 可以对slice创建slice
    • 在创建slice之前无需先创建数组可以直接创建slice创建slice的同时会创建数组并且让slice引用创建的数组
    // 声明一个数组
    arr := [5]int{1,2,3,4,5}
    // 对数组进行切片获取数组的slice
    //    其中以array[lo : hi]形式来切片
    //    slice中包含下标为lo的元素
    //    但是slice不包含下标为hi的元素
    slice := arr[1:3]
    
    // 直接创建slice
    slice_dir=[]int{1,2,3,4,5}
    
    • 在进行slice操作时可以对下标的下界和上界进行省略在省略下界时默认下界为0,在省略上界时,默认值为数组长度
    arr := []int{1,2,3,4,5,6,7}
    fmt.Println(arr[:4][1:]) // 返回[2,3,4]
    
    • slice的length和capacity
    // slice的length是指slice中元素的个数
    // slice的capacity是值从slice中第一个元素开始算起
    //    array中的最后一个元素一共有多少个元素
    arr := []int{1,2,3,4,5,6,7,8,9}
    
    slice := arr[1:4]                     
    fmt.Println(len(arr),cap(arr))
    // 输出length为3
    // 输出cap为8
    
    • slice可以对其进行重新切片虽然无法通过数组下标访问位于slice范围外的元素但是可以通过重新切片来将slice范围扩大到数组范围外
    arr := []{1,2,3,4,5,6,7,8,9}
    
    // 切片[2,3,4]
    arr = arr[1:4]
    // 重新切片为[2,3,4,5]
    arr = arr[:4]
    
    • slice变量的默认值是nil其cap和len都是0,并且没有底层的数组
    • Golang中创建动态大小的数组
    // make函数类似于c++中的new T[n],返回一个slice
    // 返回数组中元素值都是默认值
    
    // 创建一个slice并且指定len和cap为相同值
    // 其中len为5,cap为5
    arr := make([]int,5)
    
    // 创建一个slice并分别指定len和cap
    // 其中len为5,cap为10
    arr := make([]int,5,10)
    
    • Slice中可以包含slice类似于多维数组
    slice = [][]string {
        []string {"touma","kazusa"},
        []string {"kitahara","haruki"}
    }
    
    • Golang中向slice添加元素
      • 可以通过append方法向slice中添加元素如果slice中位于len后的cap可以容纳添加的所有元素就在当前数组中修改位于slice之后的元素
      • 如果当前slice的cap无法容纳append中所有的元素会重新创建一个数组拷贝当前slice中所有的元素在添加元素后返回一个指向新分配数组的slice
    arr := []int{1,2,3,4}
    t := append(arr[:3],-1,2)
    fmt.Println(arr,t)
    // 输出结果如下
    // arr [1,2,3,4]
    // t   [1,2,3,-1,2]
    // arr和t并不指向同一个底层数组
    
  • Range的形式迭代slice

    • 可以通过range的形式对slice进行迭代
    // 其中i是当前迭代元素的下标
    // v是当前迭代元素值的拷贝
    for i,v := range slice {
        fmt.Println(i,v)
    }
    
    • 在只想在range中访问index或是value时可以使用_作为占位符
    // 省略value
    for i,_ := range slice {
        fmt.Println(i)
    }
    
    // 省略value时_也可以省略
    for i := range slice {
        fmt.Println(i)
    }
    
    // 省略index
    for _,v := range slice {
        fmt.Println(v)
    }
    
  • Golang中的map

    • 在Golang中map的默认值是nil既没有键值对也无法加入键值对
    • 可以通过make函数返回指定类型的map返回的map已经被初始化并且准备好被使用
    m := make(map[string]string)
    m["shiro"]="kana"
    m["black"]="kazusa"
    fmt.Println(m)
    
    • map字面量
    m = map[string]string{
        "shiro":"kana",
        "black":"kazusa",
    }
    fmt.Println(m)
    
    • map中如果字面量中元素的值是struct类型那么可以省略值类型中的类型名称
    type Point struct {
        X float64
        Y float64
    }
    
    m := map[string]Point{
        "point-a":{1,1},
        "point-b":{2,2}
    }
    
    fmt.Println(m)
    
    • 对map进行操作
    // 插入或者更新map中元素
    m["touma"]="kazusa"
    
    // 获取map中元素,如果map中不存在该元素返回该元素类型的默认值
    elem = m["touma"]
    
    // 判断map中是否存在key为指定值的键值对
    elem,ok := m["shiro"]
    // 如果不存在elem为默认值ok为false
    
    // 从map中删除key为指定值的元素
    delete(m,key)
    
  • Golang中函数的值传递

    • Golang中函数可以作为值传递给变量或者其他函数的参数也可以作为其他函数的返回值
    func operateOnArgs(f func (float64,float64) float64,a,b float64) float64 {
        return f(a,b)
    }
    
    var f=func(a,b float64) float64{
        return math.Sqrt(a*a+b*b)
    }
    fmt.Println(operateOnArgs(f,3,4))
    
    • Golang中函数的闭包
      • Golang中支持函数的闭包通过将一个函数作为函数的返回值返回的函数可以访问外层函数中定义的局部变量位于外层函数之外只能通过返回的函数来访问外层函数内部的局部变量如此保证了局部变量的安全性
      func adder() func(int) int {
          sum := 0
          return func(a int) int {
              sum+=a
              return sum
          }
      
      }
      
  • Golang中的方法

    • 虽然Golang中并不像其他语言一样存在类class但是我们可以在Golang中为类型type定义方法
    • 在Golang中为类型type定义方法是定义一个接受特定类型参数的函数
    type Point struct {
      X,Y float64
    }
    
    // 为Point类型定义方法
    func (p Point) Abs() float64 {
      return math.Sqrt(p.X*p.X+p.Y*p.Y)
    }
    
    // 可以通过如下形式调用为类型定义的方法
    p := Point{3,4}
    fmt.Println(p.Abs())
    
    • 在Golang中不仅可以为struct类型指定方法而且可以为基本类型指定方法例如type MyFloat float64后为MyFloat类型指定方法
    • 在Golang中只能够为位于同一个包中的类型指定方法无法为位于其他包中的类型指定方法
    type MyFloat float64
    
    func (f MyFloat) fuck() {
      fmt.Println("fucking float64")
    }
    
    • 在Golang中除了可以为类型指定方法外还可以为类型的指针指定方法此时特殊参数接受一个指针可以通过指针修改传输类型的值
    type Point struct {
      X,Y float64
    }
    
    // 定义函数接受一个Point类型的指针
    func (p *Point) modify() {
      p.X *= 10
      p.Y *= 10
    }
    
    // 调用时无需先将类型对象转化为指针,可以直接在对象上调用
    p := Point{3,4}
    p.modify()
    // 调用后p为{30,40}
    
    // 接受一个Point类型的拷贝
    func (p Point) modifyCopy() {
      p.X *= 10
      p.Y *= 10
    }
    
    // 调用时传入函数的是对象的一个拷贝,不会修改原先对象的值
    p := Point{3,4}
    p.modifyCopy()
    // 调用后仍然为{3,4},修改的只是拷贝对象的值
    
    • 在Golang中普通函数参数必须向接受指针的形参中传入指针向接受值的形参中传入值但是在接受特殊参数的位置如果想要接受的是值类型那么传入指针和传入值均可如果想在特殊参数位置接受指针类型那么传入指针和传入值类型也均可
    func (p *Point) modify() {
      // func body
    }
    
    // 对于如下调用形式均可
    p := Point{3,4}
    
    p.modify() // 可行的调用方式
    (&p).modify() //可行的调用方式
    
    func (p Point) modifyCopy() {
      // func
    }
    
    p := Point{3,4}
    // 对接受值的特殊参数,下列调用方式也均可
    p.modifyCopy() // 可行
    (&p).modifyCopy() //可行
    
  • Golang中的接口

    • 在Golang中interface中定义了一系列的方法签名对于实现了interface中定义所有方法签名的类型其都可以被赋值给interface类型
    • 如果对一个类型的指针实现了interface接口那么只能将该类型的指针赋值给interface变量无法将该类型赋值给interface变量
    type Car interface {
      drive()
    }
    
    type AE86 struct {
    }
    
    // 为AE86类型的指针实现接口中drive方法
    func (car *AE86) drive() {
    
    }
    
    // 此时,可以将*AE86类型赋值给Car但是无法将AE86赋值给Car
    var car Car
    car = AE86{} // 失败
    car = &AE86{} // 成功
    
    • 接口中保存的信息通常可以看做一个二元组:(具体的值,值的类型)
    // 接口中保存的值如果是nil但是其类型已经确定那么接口的值就不为nil
    var car Car
    car=(*AE86)(nil)
    fmt.Println(car==nil) // 输出为false
    // 此时虽然car仍然不指向具体的对象但是其指向的类型确定为Car故而其不为nil
    // 在明确接口所指向的具体类型后即使其值仍然为nil但仍可以调用类型的具体方法
    car.drive() // 调用成功
    // 虽然car作为interface值时car==nil并不成立
    // 但是调用car.drive()时向drive函数中传递的*AE86类型的特殊参数仍然是nil
    // 故而在func (car *AE86) drive() {}函数的实现中,需要显式定义空指针判断
    func (car *AE86) drive() {
      if car==nil {
        // operations if nil...
      }
      // operations if non-nil...
    }
    
    • 如果在接口中及没有保存值也没有保存类型那么接口值为nil在为nil的接口值上调用方法会引发空指针异常
    var car Car
    // 此时car接口中既没有值信息又没有类型信息
    fmt.Println(car==nil) // 输出结果为true
    
    // 抛出空指针异常
    // 由于car接口中并没有保存当前的值和类型信息
    // 故而不知道应该对那个类型的drive方法进行调用
    car.drvie()
    
    • 空接口类型interface{}
      • 由于interface{}接口中没有定义任何方法故而任何类型都实现了interface{}中所有的方法interface{}接口可以持有仍和类型的值
      • 通常可以在函数形参中接受interface{}类型的变量,代表接受任何类型的值
    // 定义新的类型any其相当于interface{},可接受任何类型的值
    type any interface{}
    
    • Golang中的类型断言
      • 在Golang中接口可以在运行时持有不同类型的值类似与面向对象中的多态
      • 通过类型断言可以在运行时动态的判断interface持有值的类型
    // 运行时动态判断接口中持有值的类型
    var car Car=(*AE86)(nil)
    // 断言判断
    // 1.如果interface中持有的值不是指定类型抛出panic
    val := car.(*FD) // 此时会抛出panic终止程序
    
    // 2.判断interface中持有的值是否是制定类型如果不是不抛出panic继续运行
    val,ok := car.(*FD)
    // 如果是ok为true如果不是ok为false
    if ok {
      // val为持有的值
    } else {
      // val为该*FD类型的默认值指针类型默认值为nil
    }
    
    • Golang中根据接口持有类型来进行switch
    // 进行type switch
    var car Car=&AE86{}
    
    switch v := car.(type) {
      case *AE86:
        // 如果car接口持有的类型是*AE86则v的类型是*AE86值为car持有的值
      case *FD:
        // 如果car接口持有的类型是*FD那么v的类型是*FD值为car持有的值
      default
        // 如果car接口持有的类型不是上述类型那么v的类型仍然是接口类型值为car持有的值
    }
    
    • Golang中的Stringer接口
      • Golang中通常通过Stringer中的String()方法来返回一个描述自身的字符串而fmt包在打印信息时通常通过调用该接口的String方法来实现的
    type Waifu struct{
      firstName,lastName string
    }
    
    func (waifu Waifu) String() string {
      return "my waifu: "+waifu.firstName+" "+waifu.lastName
    }
    
    fmt.Println(Waifu{"touma","kazusa"})
    
  • Golang中的错误处理

    • Golang中通常在方法执行时都会返回一个error值调用方通过检查error返回值是否为nil来判断执行是否出错
    i,err := strconv.Atoi("1")
    if err==nil {
      	// 转换成功
      	fmt.Println(i)
      } else {
      // 转换失败
      	fmt.Println(err)
      }
    
    • 在Golang中通常通过error接口来获取失败的错误信息
    type error interface {
      Error() string
    }
    
  • Golang中的字节流

    • Golang中通过io.Reader接口来完成对文件的读取
    type Reader interface {
      // Reader接口中含有一个read方法
      // read方法接受一个字节slice作为参数
      // 并且其会将读取的字节填充到slice中
      // read方法的返回值为n和err
      // n为读取的字节数
      // err为返回的异常信息如果err返回的值是io.EOF则代表读取到字节流的末尾
      Read(b []byte) (n int,err error)
    }
    
  • Golang中的泛型

    • Golang中支持泛型函数可以为函数指定一个类型参数指定函数接受参数的类型
    // 定义泛型参数,添加对类型的约束条件,要求其可以比较
    func firstIndex[T comparable](arr []T,t T) int {
      for i,v := range arr {
        if v==t {
          return i
        }
      }
      return -1
    }
    
    fmt.Println(firstIndex[int]([]int{1,3,2,5,4},5))// 输出3
    
    • Golang中除了支持泛型函数还支持泛型struct
    // 通过泛型struct定义单链表节点
    type Node[T any] struct {
      val T
      next *Node[T]
    }
    
  • Golang中的并发

    • Goroutine
      • Goroutine是由Go的运行时环境所管理的轻量级线程
        • Goroutine运行在相同的地址空间中故而在Goroutinue在对地址中资源进行访问时需要对其进行同步
      • Goroutine的开启
      // 开启一个新的Goroutine
      // 其中f、x、y、z的计算发生在当前的Goroutine中
      //  但是f(x,y,z)的执行发生在新创建的Goroutine中
      go f(x,y,z)
      
    • Channel
      • Channel是一个类型化的管道通过<-操作符可以从Channel中接收数据或向Channel发送数据
      • Channel的创建可以通过make函数来实现
      // 创建channel
      ch := make(chan int)
      
      // 与channel的数据传输
      ch<-v // 将v中的数据发送到channel管道
      v := <-ch // 从channel中读取数据并且将其赋值给v
      
      • Channel中发送和接收的同步
        • 默认情况下(没有缓冲时向channel发送数据时需要确认接受数据的一方已经准备好
        • 在从channel接受数据时需要确认发送数据的一端已经准备好
        • 通过上述条件可以让Goroutine在没有显式锁和条件变量的情况下完成同步
      • 在创建channel时可以为channel指定一个缓冲区大小
        • 当缓冲区中的数据满之后向缓冲区填充数据的Goroutine将会被阻塞
        • 当缓冲区中的数据为空时从缓冲区读取数据的Goroutine将会被阻塞
      // 如果在同一个Goroutine中多次向channel中加入数据导致缓冲区被填满
      // 并且没有其他Goroutine来取出数据那么当前添加数据的Goroutine将会
      // 被一直阻塞,从而产生死锁
      func main {
        ch := make(chan int,2)
        ch <- 1
        ch <- 2
        ch <- 3 // 此时缓冲区已满会阻塞当前Golang造成死锁
      }
      
      • Channel的关闭和range迭代
        • Channel的发送方可以调用close函数对channel进行关闭
        • 向已经关闭的channel发送数据会抛出panic
        • 从已经关闭的channel中读取数据时第二个参数ok返回为false
        • 在对panic进行读取操作时也可以用第二个参数来读取channel是否被关闭
      ch := make(chan int,2)
      ch <- 1
      ch <- 2
      
      close(ch)
      
      // 可以通过读取channel时的第二个参数来读取channel是否被关闭
      i,ok := <-ch
      // 此时ok代表是否ch已经关闭i代表读取的值
      fmt.Println(i,ok) // 1 true
      i,ok = <-ch
      fmt.Println(i,ok) // 2 true
      i,ok =<-ch
      fmt.Println(i,ok) // 0 false
      
      // 可以对channel中的所有数据进行range循环
      // 该循环会遍历channel中所有元素直到channel被关闭
      for i := range ch {
        fmt.Println(i)
      }
      
    • Golang中的select块
      • Golang中select块可含有多个被阻塞的通信操作当任意一个通信操作结束时select结束阻塞并且执行其通信操作。
      • 如果在select块中有多个通信操作都处于就绪状态其随机选择一个执行
      • 如果在select块中存在default则在其他case均未处于就绪状态时default条件下被执行
    select {
      case v:=<-ch:
        fmt.Println(v)
      case <-quit
        return
      default:
        fmt.Println("waiting...")
        time.Sleep(time.Second * 1)
    }
    
  • Golang中的互斥锁
    • Golang中为了保证临界区共享资源的安全可以通过sync.Mutex来保证多个Goroutine访问临界资源时的并发安全
    • Mutex有Lock和Unlock方法用于加锁和释放锁
    • 为了保证解锁的方法一定被执行可以指定Unlock方法为defer此时即使发生了panic也会保证defer的Unlock方法一定被执行锁资源被释放
    mut := sync.Mutex{}
    cur := 0
    go func() {
      for cur<9 {
        mut.Lock()
        if cur<9 && cur%3 == 0 {
          fmt.Println("Goroutine 1")
          cur++
        }
        mut.Unlock()
      }
    }()
    go func() {
      for cur<9 {
        mut.Lock()
        if cur<9 && cur%3 == 1 {
          fmt.Println("Goroutine 2")
          cur++
        }
        mut.Unlock()
      }
    }()
    go func() {
      for cur<9 {
        mut.Lock()
        if cur<9 && cur%3 == 2 {
          fmt.Println("Goroutine 3")
          cur++
        }
        mut.Unlock()
      }
    }()
    // 输出会显示如下
    // 1
    // 2
    // 3
    // 1
    // 2
    // 3