begin-golang

golang小白入门

Posted by Mickey on July 16, 2018

最近开始了golang的学习,在这篇博客中记录一下知识点,有利于以后温故知新

大道至简,少即是多

  • iota的用法

    在go中,iota为特殊常量,多用于枚举值,第一个iota等于0,每当iota在新的一行被使用时,它的值都会自动加1

    package main
    
    import "fmt"
    
    func main() {
      const (
        a = iota   // 0
        b          // 1
        c          //2
        d = "ha"   //独立值,iota += 1
        e          //"ha"   iota += 1
        f = 100    //iota +=1
        g          //100  iota +=1
        h = iota   //7,恢复计数
        i          //8  
      )
      fmt.Println(a,b,c,d,e,f,g,h,i)
    }
    

    上述代码的运行结果为:

    0 1 2 ha ha 100 100 7 8
    
  • a是值为hello的字符串,为啥unsafe.Sizeof(a)输出结果为16,同理,切片和数组需要24字节的内存

    字符串类型在go里是个结构,包含指向底层数组的指针和长度,这两部分每部分都是 8 个字节,所以字符串类型大小为 16 个字节

    对切片和数组来说,指针字段需要8字节,长度和容量字段分别需要8字节,共24字节

  • golang中字符串和int的互相转换

    int -> string: fmt.Sprint(int)

    int -> string: strconv.Itoa(int)

    string -> int: strconv.Atoi(string)

  • switch关键字可以有如下两种使用方式

    package main
    
    import "fmt"
    
    func main() {
      /* 定义局部变量 */
      var grade string = "B"
      var marks int = 90
    
      switch marks {
          case 90: grade = "A"
          case 80: grade = "B"
          case 50,60,70 : grade = "C"
          default: grade = "D"  
      }
    
      switch {
          case grade == "A" :
            fmt.Printf("优秀!\n" )     
          case grade == "B", grade == "C" :
            fmt.Printf("良好\n" )      
          case grade == "D" :
            fmt.Printf("及格\n" )      
          case grade == "F":
            fmt.Printf("不及格\n" )
          default:
            fmt.Printf("差\n" );
      }
      fmt.Printf("你的等级是 %s\n", grade );      
    }
    
  • 结构体类型方法

    package main
    
    import (
      "fmt"  
    )
    
    /* 定义结构体 */
    type Circle struct {
      radius float64
    }
    
    func main() {
      var c1 Circle
      c1.radius = 10.00
      fmt.Println("Area of Circle(c1) = ", c1.getArea())
    }
    
    //该 method 属于 Circle 类型对象中的方法
    func (c Circle) getArea() float64 {
      //c.radius 即为 Circle 类型对象中的属性
      return 3.14 * c.radius * c.radius
    }
    

    上述代码中,getArea方法前(c Circle)表明getArea方法是Circle结构体的方法

  • go语言函数引用传值和c一样,是传递一个地址上去,这样就能在函数中同步

    package main
    
    import "fmt"
    
    func main() {
      /* 定义局部变量 */
      var a int = 100
      var b int = 200
    
      fmt.Printf("交换前,a 的值 : %d\n", a )
      fmt.Printf("交换前,b 的值 : %d\n", b )
    
      /* 调用 swap() 函数
      * &a 指向 a 指针,a 变量的地址
      * &b 指向 b 指针,b 变量的地址
      */
      swap(&a, &b)
    
      fmt.Printf("交换后,a 的值 : %d\n", a )
      fmt.Printf("交换后,b 的值 : %d\n", b )
    }
    
    func swap(x *int, y *int) {
      *x, *y = *y, *x
    }
    
  • go的指针太有意思了,可以直接访问struct中的数据

    package main
    
    import "fmt"
    
    type Student struct {
      name string
      age int
    }
    
    func main() {
      var stu Student
      stu.name = "mickey"
      stu.age = 23
      printName(&stu)
    }
    
    func printName(stu *Student) {
      fmt.Println(stu, *stu, stu.name)
    }
    

    上述代码的输出为

    &{mickey 23} {mickey 23} mickey
    
  • go虽然是一门编译性的语言,但是可以动态对数组(切片)进行append操作,与slice有关的api有

    • make:建立一个slice
    • append:数组追加
    • copy:数组拷贝
    • len:获取slice的长度
    • cap:获取slice的最大容量
    • newSlice = slice[i:j:k],len = j - i,cap = k - i,如未指定k的话,则默认k为原slice的cap
    package main
    
    import "fmt"
    
    func main() {
      var numbers []int
      printSlice(numbers)
    
      /* 允许追加空切片 */
      numbers = append(numbers, 0)
      printSlice(numbers)
    
      /* 向切片添加一个元素 */
      numbers = append(numbers, 1)
      printSlice(numbers)
    
      /* 同时添加多个元素 */
      numbers = append(numbers, 2,3,4)
      printSlice(numbers)
    
      /* 添加一个数组,需要使用...运算符 */
      arr := []int {1, 3, 5}
      numbers = append(numbers, arr...)
      printSlice(numbers)
    
      /* 创建切片 numbers1 是之前切片的两倍容量*/
      numbers1 := make([]int, len(numbers), (cap(numbers))*2)
    
      /* 拷贝 numbers 的内容到 numbers1 */
      copy(numbers1,numbers)
      printSlice(numbers1)  
    }
    
    func printSlice(x []int){
      fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
    }
    
  • 在对切片进行截取操作的时候,需要注意,截取出来的slice和原slice指针指向的是同一个数组,为了杜绝错误的数据修改,可以将新slice的长度和容量设置成一样的,这样append的时候,会重新生成一个更大长度的数组,与原slice指向的数组不同

    // 创建字符串切片
    // 其长度和容量都是 5 个元素
    source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
    // 对第三个元素做切片,并限制容量
    // 其长度和容量都是 1 个元素
    slice := source[2:3:3]
    // 向 slice 追加新字符串
    slice = append(slice, "Kiwi")
    

    常见的错误

    a := []int{1, 2, 3, 4}
    b := append(a[:1], a[2:]...)
    fmt.Println(a, b) [1, 3, 4, 4] [1, 3, 4] // 这行代码的本意是复制 a 中除了下标为 1 的全部元素,却发现打印出来 a 改变了,原因就是 a[:1] 和 a 指向相同的 ptr,因此 cap > len
      
    b = append(a[:1:1], a[2:]...)
    fmt.Println(a, b) [1, 2, 3, 4] [1, 3, 4]
    
  • go声明数组和声明切片的不同,如果在[]运算符里指定了一个值,那么创建的就是数组而不是切片,只有不指定值的时候,才会创建切片

    // 创建有3个元素的整型数组
    array := [3]int{10, 20, 30}
    
    // 创建长度和容量都是3的整形切片
    slice := []int{10, 20, 30}
    
  • golang可以使用内建函数 make 也可以使用 map 关键字来定义Map,但是不初始化map,那么就会创建一个nil map,nil map不能用来存放键值对

    /* 声明变量,默认 map 是 nil */
    var map_variable map[key_data_type]value_data_type
    
    /* 使用 make 函数 */
    map_variable := make(map[key_data_type]value_data_type)
    
  • go中从map中获取值并判断键是否存在

    value, exists := colors["Blue"]
    if exists {
      fmt.Println(value)
    }
    
    value := colors["Blue"]
    if value != "" {
      fmt.Println(value)
    }
    
  • golang的range关键字可以用于遍历string,slice和map,需要注意的是,遍历string的时候,返回的是字符的unicode形式

    nums := []int {2, 3, 4}
    for _, num := range nums {
      fmt.Println(num)
    }
    
    kvs = map[string]string{"a": "apple", "b": "banana"}
    for k, v := range kvs {
      fmt.Println(k, v)
    }
    
    for i, c := range "go" {
      fmt.Println(i, c)
    }
    
  • go中的interface

    /* 定义接口 */
    type interface_name interface {
      method_name1 [return_type]
      method_name2 [return_type]
    }
    
    package main
    
    import "fmt"
    
    type Phone interface {
      call()
    }
    
    type IPhone struct {
      name string
    }
    
    func (p IPhone) call() {
      fmt.Println(p.name)
    }
    
    type INT int
    
    func (i INT) call() {
      fmt.Println(i)
    }
    
    func main() {
      var a INT = 76
      var p Phone = a
      p.call()
      iphone := IPhone {name: "abc"}
      p = iphone
      p.call()
    }
    
  • go中的new操作符

    new(type)获得type类型的一个地址,多用于指针数组初始化

    var pointerArr = [2]*int {new(int), new(int)}
    
    *pointerArr[0] = 1
    
  • go的method和func的区别

    method基于struct,只有用.运算符才能调用

    func就是普通函数

  • go中import包的顺序如下

    1. 首先编译器会查找Go的安装目录
    2. 按顺序查找GOPATH变量里列出的目录
  • go的import操作是import一个包,包下所有文件的大写字母开头的变量都能在包外拿到,包内的文件可以引用包内所有的变量,因此在一个包内不能定义同名的全局func和var

  • go interface的方法集

    struct可以定义值接受者声明的方法和指针接受者声明的方法,需要注意的是,如果使用值接收者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口,如果使用指针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口

    type notifier interface {
      notify()
    }
    
    type user struct {
      name string
      age int
    }
    
    func (u *user) notify() {
      fmt.Println(u.name, u.age)
    }
    
    func main() {
      u := user {"name", 23}
      sendNotity(u) // 报错
      sendNotity(&u) // 成功
    }
    
    func sendNotity(n notifier) {
      n.notify()
    }
    
  • go的struct引入另外一个struct有两种声明方式,变量声明和嵌套声明,嵌套声明的话存在提升,即可以直接在外层变量访问内部变量的参数

    package main
    
    import "fmt"
    
    type student struct {
      id int
    }
    
    type person struct {
      student
      name string
    }
    
    type person1 struct {
      stu student
      name string
    }
    
    func main() {
      per := person{
        student{
          id: 21,
        },
        "mickey",
      }
    
      per1 := person1{
        student{
          id: 21,
        },
        "mickey",
      }
    
      fmt.Println(per.id)
      fmt.Println(per1.stu.id)
    }
    
  • golang的goroutine并发,runtime.GOMAXPROCS(1)意思是为每个调度器分配一个逻辑处理器,首先定义一个sync.WaitGroup的变量,设置需要等待的goroutine数量,在一个goroutine中执行defer wg.Done(),在需要阻塞执行的地方执行wg.Wait()

    package main
    
    import (
      "sync"
      "runtime"
      "fmt"
    )
    
    func main() {
      runtime.GOMAXPROCS(1)
      // runtime.GOMAXPROCS(runtime.NumCPU())
      var wg sync.WaitGroup
      wg.Add(2)
    
      go func() {
        defer wg.Done()
    
        for count := 0; count < 3; count++ {
          for char := 'a'; char < 'a' + 26; char++ {
            fmt.Printf("%c 1 ", char)
          }
        }
      }()
    
      go func() {
        defer wg.Done()
    
        for count := 0; count < 3; count++ {
          for char := 'a'; char < 'a' + 26; char++ {
            fmt.Printf("%c 2 ", char)
          }
        }
      }()
    
      fmt.Println("Wait")
      wg.Wait()
      fmt.Println("finish")
    }
    
  • Golang 的 协程调度机制 与 GOMAXPROCS 性能调优

  • go的runtime模块提供runtime.Gosched()方法使得当前goroutine退出线程,返回调度器等待序列

  • go中goroutine可能同时修改同一个变量,需要锁住共享资源

    • 原子函数(以int64举例子),LoadInt64函数用于获取一个int64变量,StoreInt64函数用于存储一个int64变量

      import "sync/atomic"
      
      var shutdown int64
      
      atomic.LoadInt64(&shutdown)
      atomic.StoreInt64(&shutdown, 1)
      
    • 互斥锁(mutex)

      package main
      
      import (
        "fmt"
        "runtime"
        "sync"
      )
      
      var (
        // counter是所有goroutine都要增加其值的变量
        counter int
      
        // wg用来等待程序结束
        wg sync.WaitGroup
      
        // mutex 用来定义一段代码临界区
        mutex sync.Mutex
      )
      
      func main() {
        wg.Add(2)
      
        go incCounter(1)
        go incCounter(2)
      
        wg.Wait()
      }
      
      func incCounter(id int) {
        defer wg.Done()
      
        for count := 0; count < 2; count++ {
          mutex.Lock()
          {
            // 捕获counter的值
            value := counter
            runtime.Gosched()
            value++
            counter = value
          }
          mutex.UnLock()
        }
      }
      

      即使goroutine执行runtime.Gosched()退出当前线程,调度器也会再次分配这个goroutine继续运行

  • break/continue 标识符,用于在多重循环中跳转

    next:
    for i := .... {
      for j := ... {
        continue/break next
      }
    }
    
  • 通道(用于在goroutine中同步数据)

    • 无缓冲的通道

      channel := make(chan int)

    • 有缓冲的通道

      tasks := make(chan string, num int)

    • 通道分为可读可写通道,只读通道,只写通道,如上所示的 chan int 是可读可写通道

      import "time"
      
      ticker := time.NewTicker(time.Second * 10)
      
      var ch <-chan time.Time // 定义一个只读通道
      
      ch = ticker // right
      
      var ch1 chan time.Time
      
      ch1 = ticker // wrong
      
  • golang make(chan int) 和 make(chan int, 1) 的区别

    不带缓冲的channel写完就阻塞,这种情况只有其他协程中有对应的读才能解除阻塞。而带缓冲的channel要直到写满+1才阻塞

    如下所示是可以的,不会报死锁

      func main() {
      	ch := make(chan int)
      	go func() {
      		time.Sleep(time.Second * 5)
      		<-ch
      	}()
      	select {
      	case ch <- 1:
      		fmt.Println("asd")
      	}
      }
    

    而下面的写法会报死锁

      func main() {
      	ch := make(chan int)
        
      	select {
      	case ch <- 1:
      		fmt.Println("asd")
      	}
        
      	go func() {
      		time.Sleep(time.Second * 5)
      		<-ch
      	}()
      }
    
  • golang中提供一个vender目录用于存放第三方的golang包,我们可以使用go get命令拉取相应package到$GOPATH目录,然后cp到vender目录

  • golang中main、init方法的调用

    init方法是在任何package中都可以出现,但是建议每个package中只包含一个init函数比较好,容易理解。但是main方法只能用在package main中。

    Go程序会自动调用init和main,所以你不需要在任何地方调用这两个函数

    main函数引入包初始化流程图

  • go-import下划线的作用

    在go中,import的作用是导入其他package,import 下划线(如: import _ “hello/go”)的作用:当导入一个包时,该包下的文件里所有init函数都会被执行,然而,有时候我们并不是要把整个包都导入进来,仅仅是希望它执行init函数而已,这个时候就可以使用import _ 来引用该包,需要记住的是,import _ 无法通过包名来调用包中的其他函数

  • js中有Number.MAX_SAFE_INTEGER,python有float(‘inf’)来代表最大数值,golang没有自带的最大数常量,可以用位运算来表示

      无符号整型uint
      const UINT_MIN uint = 0
      const UINT_MAX = ^uint(0)
        
      有符号整型int
      根据补码,其最大值二进制表示,首位0,其余1
      const INT_MAX = int(^uint(0) >> 1)
      const INT_MIN = ^INT_MAX
    
  • go中的int类型并不是等于int32或者int64,而是随着平台位数变化的,strconv.Atoi方法得到的是int类型,如果要得到确切位数的int变量,需要使用strconv.ParseInt(s string, base int, bitSize int),在golang内部,是根据如下的代码来确定平台的int位数的,32位的系统(^uint(0)»63)就是32bit的0,64位则为前63位的0以及最后1位的1

      const intSize = 32 << uint(^uint(0)>>63)
      const IntSize = intSize // number of bits in int, uint (32 or 64)
    
  • golang中的随机数rand.Intn()方法每次build运行得到的结果是一样的,除非使用rand.Seed(),而且Seed()的参数最好是变化的,例如time.Now().Unix()这样的,才能每次build运行的时候做到真正的随机

    参考资料

  • go中sync.Mutex和sync.RWMutex的区别

    Mutex为互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁也叫做全局锁

    RWMutex是一个读写锁,该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景.

    • func (rw *RWMutex) Lock()  写锁,如果在添加写锁之前已经有其他的读锁和写锁,则lock就会阻塞直到该锁可用,为确保该锁最终可用,已阻塞的 Lock 调用会从获得的锁中排除新的读取器,即写锁权限高于读锁,有写锁时优先进行写锁定
    • func (rw *RWMutex) Unlock() 写锁解锁,如果没有进行写锁定,则就会引起一个运行时错误
    • func (rw *RWMutex) RLock() 读锁,当有写锁时,无法加载读锁,当只有读锁或者没有锁时,可以加载读锁,读锁可以加载多个,所以适用于"读多写少"的场景

    • 读锁解锁,RUnlock 撤销单次 RLock 调用,它对于其它同时存在的读取器则没有效果
  • go中defer、return、返回值之间执行顺序的坑,主要是返回值为匿名的时候,defer无法修改返回值,除非返回的是指针,以及defer声明时会先计算确定参数的值,defer推迟执行的仅是其函数体

    Golang中defer、return、返回值之间执行顺序的坑

  • 在go中,如果需要大量的字符串拼接操作,可以使用bytes的buffer来减少消耗

      buffer := bytes.NewBufferString(str)
      for _, s := range strs {
          buffer.WriteString(s)
      }
      str = buffer.String()
    
  • golang里int转string有两种常用的方法,fmt.Sprintf(“%d”, i)以及strconv.Itoa(i), 后者的性能远好于前者

      startTime := time.Now()
      for i := 0; i < 10000; i++ {
          fmt.Sprintf("%d", i)
      }
      fmt.Println(time.Now().Sub(startTime))
    
      startTime = time.Now()
      for i := 0; i < 10000; i++ {
          strconv.Itoa(i)
      }
      fmt.Println(time.Now().Sub(startTime))
        
      1.214578ms
      400.521µs
    
  • golang中的string的底层实际是byte数组,由于golang默认是utf8的,中文占三个字节,当使用len函数的时候,会发现一个汉字len为3,使用utf8.RuneCountInString()可以正确得到结果

  • golang为了方便开发者使用,将I/O操作封装到了如下几个包中

    • io 为IO原语,提供了基本的IO接口
    • ioutil 封装一些实用的IO函数,例如ReadFile
    • fmt 实现格式化I/O,类似于c中的printf和scanf
    • bufio 实现带缓存的I/O
  • golang中heap —— container/heap

      type Interface interface {
          sort.Interface
          Push(x interface{})
          Pop() interface{}
      }
    

    堆接口继承自sort.Interface,因此需要定义sort的三个方法以及heap的两个方法

      type IntHeap []int
    
      func (h IntHeap) Len() int { return len(h) }
      func (h IntHeap) Less(i, j int) { return h[i] < h[j] }
      func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
    
      func (h *IntHeap) Push(x interface{}) {
          *h = append(*h, x.(int))
      }
    
      func (h *IntHeap) Pop() interface{} {
          old := *h
          n := len(old)
          x := old[n - 1]
          *h = old[0 : n - 1]
          return x
      }
    
      h := &IntHeap{2, 1, 5}
    
      heap.Init(h)
      heap.Push(h, 3)
      heap.Pop(h)
    
  • golang中ring —— container/ring

      type Ring struct {
          next, prev *Ring
          Value interface{}
      }
    

    环的结构有点特殊,环的尾部就是头部,所以每个元素实际上就可以代表自身的这个环。它不需要像list一样保持list和element两个结构,只需要保持一个结构就行

      ring := ring.New(3)
    
      for i := 1; i <= 3; i++ {
          ring.Value = i
          ring = ring.Next()
      }
    
      // 计算1+2+3
      s := 0
      ring.Do(func(p interface{}){
          s += p.(int)
      })
      fmt.Println("sum is", s)
    
  • golang 常用的时间函数

    • time.Unix(sec, nsec int64) 通过Unix时间戳生成time.Time实例
    • time.Time.Unix() 得到Unix时间戳
    • time.Time.UnixNano() 得到Unix时间戳的纳秒表示
    • time.Parse、time.ParseInLocation以及time.Time.Format 时间文本序列化/反序列化

        t, _ := time.Parse("2006-01-02 15:04:05", "2016-06-13 09:14:00")
        fmt.Println(time.Now().Sub(t).Hours())
      

      2016-06-13 09:14:00 这个时间可能是参数传递过来的。这段代码的结果跟预期的不一样,原因是 time.Now() 的时区是 time.Local,而 time.Parse 解析出来的时区却是 time.UTC(可以通过 Time.Location() 函数知道是哪个时区)。在中国,它们相差 8 小时,所以,一般的,我们应该总是使用 time.ParseInLocation 来解析时间,并给第三个参数传递 time.Local

      至于为什么要使用2006-01-02 15:04:05 这个字符串,这是固定写法,类似于其他语言中 Y-m-d H:i:s 等,而选择这个时间点,也是出于好记的考虑,官方的例子:Mon Jan 2 15:04:05 MST 2006,另一种形式 01/02 03:04:05PM ‘06 -0700,对应是 1、2、3、4、5、6、7;常见的格式:2006-01-02 15:04:05,很好记:2006年1月2日3点4分5秒~

    • time.Time.Round(最接近)和time.Time.Truncate(向下取整)

        t, _ := time.ParseInLocation("2006-01-02 15:04:05", "2016-06-13 15:34:39", time.Local)
        // 整点(向下取整)
        fmt.Println(t.Truncate(1 * time.Hour))
        // 整点(最接近)
        fmt.Println(t.Round(1 * time.Hour))
              
        // 整分(向下取整)
        fmt.Println(t.Truncate(1 * time.Minute))
        // 整分(最接近)
        fmt.Println(t.Round(1 * time.Minute))
              
        t2, _ := time.ParseInLocation("2006-01-02 15:04:05", t.Format("2006-01-02 15:00:00"), time.Local)
        fmt.Println(t2)
      
  • golang中的sync.Cond

    sync.Cond是用来控制某个条件下,goroutine进入等待时期,等待信号到来,然后重新启动,比如:

      package main
        
      import (
          "fmt"
          "sync"
          "time"
      )
        
      func main() {
          locker := new(sync.Mutex)
          cond := sync.NewCond(locker)
          done := false
        
          cond.L.Lock()
        
          go func() {
              time.Sleep(2e9)
              done = true
              cond.Signal()
          }()
        
          if (!done) {
              cond.Wait()
          }
        
          fmt.Println("now done is ", done);
      }
    

    sync.Cond初始化的时候,传入的一定是Locker的指针,否则在c.L.Lock()和c.L.Unlock()的时候会频繁复制锁,导致失效甚至死锁

    这里当主goroutine进入cond.Wait的时候,就会进入等待,当从goroutine发出信号之后,主goroutine才会继续往下面走

    sync.Cond还有一个BroadCast方法,用来通知唤醒所有等待的goroutine

      package main
        
      import (
          "fmt"
          "sync"
          "time"
      )
        
      var locker = new(sync.Mutex)
      var cond = sync.NewCond(locker)
        
      func test(x int) {
        
          cond.L.Lock() // 获取锁
          cond.Wait()   // 等待通知  暂时阻塞
          fmt.Println(x)
          time.Sleep(time.Second * 1)
          cond.L.Unlock() // 释放锁,不释放的话将只会有一次输出
      }
      func main() {
          for i := 0; i < 40; i++ {
              go test(i)
          }
          fmt.Println("start all")
          cond.Broadcast() //  下发广播给所有等待的goroutine
          time.Sleep(time.Second * 60)
      }
    

    主gouroutine开启后,可以创建多个从gouroutine,从gouroutine获取锁后,进入cond.Wait状态,当主gouroutine执行完任务后,通过BroadCast广播信号。 处于cond.Wait状态的所有gouroutine收到信号后将全部被唤醒并往下执行。需要注意的是,从gouroutine执行完任务后,需要通过cond.L.Unlock释放锁, 否则其它被唤醒的gouroutine将没法继续执行。

  • channel可以实现大多数sync.Cond的功能(close -> broadcase,insert一个数值 -> signal),但是channel不能reopen wait

      package main
    
      import (
          "fmt"
          "sync"
          "time"
      )
        
      var count int = 4
        
      func main() {
          ch := make(chan struct{}, 5)
        
          // 新建 cond
          var l sync.Mutex
          cond := sync.NewCond(&l)
        
          for i := 0; i < 5; i++ {
              go func(i int) {
                  // 争抢互斥锁的锁定
                  cond.L.Lock()
                  defer func() {
                      cond.L.Unlock()
                      ch <- struct{}{}
                  }()
        
                  // 条件是否达成
                  for count > i {
                      cond.Wait()
                      fmt.Printf("收到一个通知 goroutine%d\n", i)
                  }
                    
                  fmt.Printf("goroutine%d 执行结束\n", i)
              }(i)
          }
        
          // 确保所有 goroutine 启动完成
          time.Sleep(time.Millisecond * 20)
            
          // 锁定一下
          fmt.Println("broadcast...")
          cond.L.Lock()
          count -= 1
          cond.Broadcast()
          cond.L.Unlock()
        
          time.Sleep(time.Second)
          fmt.Println("signal...")
          cond.L.Lock()
          count -= 2
          cond.Signal()
          cond.L.Unlock()
        
          time.Sleep(time.Second)
          fmt.Println("broadcast...")
          cond.L.Lock()
          count -= 1
          cond.Broadcast()
          cond.L.Unlock()
        
          for i := 0; i < 5; i++ {
              <-ch
          }
      }
    

    github上关于remove sync.Cond的issue proposal: Go 2: sync: remove the Cond type

  • 临时连接池 sync.Pool

    有时候在go中,不同的goroutine需要同时创建一些对象,而对象又都是占用内存的,进而导致的就是内存回收的GC压力陡增,造成”并发大-内存占用大-GC缓慢-处理并发能力降低-并发更大这样的恶性循环”,在这个时候,我们非常迫切需要有一个对象池,每个goroutine不再自己单独创建对象,而是从对象池中获取出一个对象(如果池中已经有的话),这就是sync.Pool出现的目的了

      // keyBufPool returns []byte buffers for use by PickServer's call to
      // crc32.ChecksumIEEE to avoid allocations. (but doesn't avoid the
      // copies, which at least are bounded in size and small)
      var keyBufPool = sync.Pool{
      	New: func() interface{} {
      		b := make([]byte, 256)
      		return &b
      	},
      }
        
      func (ss *ServerList) PickServer(key string) (net.Addr, error) {
      	ss.mu.RLock()
      	defer ss.mu.RUnlock()
      	if len(ss.addrs) == 0 {
      		return nil, ErrNoServers
      	}
      	if len(ss.addrs) == 1 {
      		return ss.addrs[0], nil
      	}
      	bufp := keyBufPool.Get().(*[]byte)
      	n := copy(*bufp, key)
      	cs := crc32.ChecksumIEEE((*bufp)[:n])
      	keyBufPool.Put(bufp)
        
      	return ss.addrs[cs%uint32(len(ss.addrs))], nil
      }
    

    这是实际项目中的一个例子,这里使用keyBufPool的目的是为了让crc32.ChecksumIEEE所使用的[]bytes数组可以重复使用,减少GC的压力。

但是这里可能会有一个问题,我们没有看到Pool的手动回收函数。 那么是不是就意味着,如果我们的并发量不断增加,这个Pool的体积会不断变大,或者一直维持在很大的范围内呢?

答案是不会的,sync.Pool的回收是有的,它是在系统自动GC的时候,触发pool.go中的poolCleanup函数

  • golang基础库

  • golang unsafe.Pointer和uintptr

  • golang基础库 flag 的用法

    flag可以用来解析命令行的参数,多用于使用flagSet初始化config配置

      func main() {
         flagSet := flag.NewFlagSet("flagSet", flag.ExitOnError)
    
         flagSet.String("name", "mickey0524", "input the name")
         flagSet.Int("age", 23, "input the age")    
         flagSet.Bool("isBoy", true, "input the sex")
    
         flagSet.parse(os.Args[1:])
    
         fmt.Println(flagSet.Lookup("name").Value.String())
         fmt.Println(flagSet.Lookup("age").Value.(flag.Getter).Get().Int)
      }
    
      go run main.go mic 23
        
      mic
      23
    
  • go-svc框架

    在阅读NSQ源码的时候,发现nsqlookupd模块使用开源框架svc来开启进程以及控制进程的退出,在这记录一下

    svc使用起来非常简单,定义一个struct实现svc的service接口即可

    简单使用

      package main
        
      import (
          "fmt"
          "syscall"
          "time"
        
          "github.com/judwhite/go-svc/svc"
      )
        
      type program struct{}
        
      func main() {
          p := &program{}
          if err := svc.Run(p, syscall.SIGINT, syscall.SIGTERM); err != nil {
              fmt.Println("svc err")
          }
      }
        
      func (p *program) Init(env svc.Environment) error {
          fmt.Println("init", env)
          return nil
      }
        
      func (p *program) Start() error {
          fmt.Println("start")
          go func() {
              time.Sleep(time.Second * 1)
          }()
          return nil
      }
        
      func (p *program) Stop() error {
          fmt.Println("stop")
          return nil
      }
    

    注意事项

    1. Start方法中不能只直接阻塞,需要在Start方法中新开goroutine去写需要阻塞的代码
    2. svc.Run()方法的第二个参数可以指定需要程序监听的信号,默认情况下不指定的话,默认会监听 SIGINT和 SIGTERM两个

    实现原理

      // Run runs your Service.
      //
      // Run will block until one of the signals specified in sig is received.
      // If sig is empty syscall.SIGINT and syscall.SIGTERM are used by default.
      func Run(service Service, sig ...os.Signal) error {
          env := environment{}
          if err := service.Init(env); err != nil {
              return err
          }
        
          if err := service.Start(); err != nil {
              return err
          }
        
          if len(sig) == 0 {
              sig = []os.Signal{syscall.SIGINT, syscall.SIGTERM}
          }
        
          signalChan := make(chan os.Signal, 1)
          signalNotify(signalChan, sig...)
          //signalNotify方法其实就是 signal.Notify 方法
          //var signalNotify = signal.Notify
          <-signalChan
        
          return service.Stop()
      }
    
  • golang atomic包的Value类型

    atomic.Value是goroutine间共享的变量,Load()方法取最后一次被Store()写入的数据

      package main
        
      import (
          "fmt"
          "sync"
          "sync/atomic"
          "time"
      )
        
      type myMap map[int]int
        
      var mutex sync.Mutex
        
      func main() {
          var m atomic.Value
          m.Store(make(myMap))
          for i := 0; i < 10; i++ {
              go func(j int) {
                  mutex.Lock()
                  defer mutex.Unlock()
        
                  m1 := m.Load().(myMap)
                  m2 := make(myMap)
                  for k, v := range m1 {
                      m2[k] = v
                  }
        
                  m2[j] = j
                  m.Store(m2)
                  time.Sleep(time.Second * 2)
              }(i)
          }
          for {
              m1 := m.Load().(myMap)
              fmt.Println(m1)
              time.Sleep(time.Second * 1)
          }
      }