0
点赞
收藏
分享

微信扫一扫

Go——基础

最不爱吃鱼 2022-01-24 阅读 60
golanggo

文章目录

函数

  • go语言中函数的地位举足轻重,使用函数式编程的方式,方法名首字母大写
  • 先体会一下函数式编程,格式:函数名、参数名、再声明参数类型;函数可能还要声明返回值类型(名称–>类型
  • 以一个装饰者模式统计函数运行时间的例子演示
    // 被装饰函数作为参数,
    func timeSpent(inner func(op int) int) func(op int) int {	// 函数类型就是要写 func(op int) int
    	return func(n int) int{	// 内部函数接收被装饰函数参数
    		start:=time.Now()
    		ret:=inner(n)
    		fmt.Println("time spent: ", time.Since(start).Seconds())
    		return ret
    	}
    }
    
    func slowFunc(op int) int {
    	time.Sleep(time.Second*1)
    	return op	// 传入什么返回什么
    }
    
    // 测试
    func TestFn(t *testing.T) {
    	sf:=timeSpent(slowFunc)
    	t.Log(sf(10))
    }
    
    • 注:一个文件夹下的各文件包名必须一致,相当于一个域,不能有重复定义
    • 测试时同一文件夹下的所有测试函数都会执行
  • 可变长参数
    func Sum(ops ...int) int {
    	ret:=0
    	for _, op := range ops {
    		ret += op
    	}
    	return ret
    }
    
    func TestSum(t *testing.T) {
    	t.Log(Sum(1,2,3,4,5))
    	t.Log(Sum(6,7,8,9))
    }
    
  • defer延迟执行,类似Java的finally
    func Clear() {
    	fmt.Println("release resource")
    }
    
    func TestDefer(t *testing.T) {
    	defer Clear()	// 即使报错了也会执行
    	fmt.Println("start here=============")
    	panic("error occur!")	// 报错
    }
    

面向对象

  • go不支持继承

封装

  • 但面向对象的一大特性在于封装数据和行为(属性和方法),先来看看
    package myfunc
    
    import "testing"
    
    type Employee struct {	// 结构体,可以理解为类
    	Id string
    	Name string
    	Age int
    }
    
    func TestEmployee(t *testing.T) {
    	e1 := Employee{"0", "Roy", 18}	// 花括号
    	e2 := Employee{Id: "1", Name: "Allen"}	// 类似构造方法创建对象
    	e3 := new(Employee)	// 得到实例对象的指针
    	e3.Id = "2"
    	e3.Name = "Eric"
    	e3.Age = 22
    	t.Log(e1)
    	t.Log(e2)
    	t.Log(&e2)	// 变成指针类型
    	t.Log(e3)
    	t.Log(e3.Age)
    }
    
  • 上面封装了数据,现在定义行为(函数),有两种方式
    // 方式1
    func (e Employee) String() string {	// 无参
    	return fmt.Sprintf("ID:%s /Name:%s /Age:%d", e.Id, e.Name, e.Age)
    }
    
    // 方式2
    func (e *Employee) String() string {	// 多了个*
    	return fmt.Sprintf("ID:%s /Name:%s /Age:%d", e.Id, e.Name, e.Age)
    }
    
    // 测试
    func TestEmployeeBehav(t *testing.T) {
    	e := Employee{"3", "Atom", 24}	// e := &Employee{"3", "Atom", 24}
    	t.Log(e.String())
    }
    
    • 这两种方式好像没什么区别?改造一下
    // 方式1
    func (e Employee) String() string {	// 无参
      	fmt.Printf("Address is %x", unsafe.Pointer(&e.Name))  // 和调用时传入的结构体位置不一致
    	return fmt.Sprintf("ID:%s /Name:%s /Age:%d", e.Id, e.Name, e.Age)
    }
    
    // 方式2
    func (e *Employee) String() string {	// 多了个*
      	fmt.Printf("Address is %x", unsafe.Pointer(&e.Name))	// 和调用处传参结构体的位置一致
    	return fmt.Sprintf("ID:%s /Name:%s /Age:%d", e.Id, e.Name, e.Age)
    }
    
    // 测试
    func TestEmployeeBehav(t *testing.T) {
    	e := Employee{"3", "Atom", 24}  // e := &Employee{"3", "Atom", 24}
      	fmt.Printf("Address is %x \n", unsafe.Pointer(&e.Name))
      	t.Log(e.String())
    }
    
    • 方式1会复制一份传入的参数,方式2没有数据的复制,节省空间

接口

  • go语言的接口为非入侵性:不依赖接口定义
    • Java中先定义接口—>实现—>再使用这个实现的类,顺序是固定的,依赖是大大滴
  • 接口定义可以包含在接口使用(实现)者包内
  • 看个例子:
    // interface_test.go
    package myinterface
    
    import "testing"
    // import "fmt"
    
    
    type Hello interface{
      SayHello() string
    }
    
    type GoHello struct { // 结构体不显式继承接口
    }
    
    func (s *GoHello) SayHello() string {
      return "Hello, I am Go"
    }
    
    func TestSayHello(t *testing.T) {
      var p Hello       // 接口变量
      p = new(GoHello)  // 接口实现, 必须传指针
      // p = GoHello{} // GoHello does not implement Hello (SayHello method has pointer receiver)
      t.Log(p.SayHello())
      fmt.Printf("%T", &GoHello{})  // *myinterface.GoHello
      fmt.Printf("%T", new(GoHello))  // *myinterface.GoHello 都是指针类型
      fmt.Printf("%T", GoHello{})   // myinterface.GoHello 直接得到对象地址
    }
    
    • 在测试中,先定义了一个接口变量p,类型是接口,值是实现接口的结构体的对象(实例),类似Java的List->ArrayList
    • 也就是从这里才开始和接口搭上关系!接口中定义的方法都要实现
    • 但这里interface的定义可以后续根据需要再补上然后使用(var p Hello 如果这个方法在多处定义),并不强制依赖,或者说:是ducktype?
  • 自定义类型,改写之前的一个函数为例
    type IntConv func(op int) int	// 自定义类型别名,简化代码编写
    func timeSpent(inner IntConv) IntConv {
    	return func(n int) int {
    		start:=time.Now()
    		ret:=inner(n)
    		fmt.Println("time spent: ", time.Since(start).Seconds())
    		return ret
    	}
    }
    
    • 说明一下:用var声明变量要指定类型,:=不用

继承

  • 面向对象封装了数据和行为,但扩展(继承)也非常重要
  • go语言不支持继承,用例子证明
    package myextend
    
    import "testing"
    import "fmt"
    
    type Pet struct {
    
    }
    
    func (p *Pet) Speak() {
      fmt.Println("...pet ")
    }
    
    func (p *Pet) SpeakTo(name string) {
      p.Speak()
      fmt.Println(name)
    }
    
    type Cat struct {
      // p *Pet
      Pet // 匿名嵌套类型
    }
    
    // func (c *Cat) Speak() {
    //   fmt.Println("...cat miao~ ")
    // }
    //
    // func (c *Cat) SpeakTo(name string) {
    //   c.Speak()
    //   fmt.Println(name)
    // }
    
    func TestExtend(t *testing.T) {
      cat := new(Cat)
      cat.SpeakTo("Roy!")
    }
    
    • 上面使用匿名嵌套类型(只有Pet而不是p *Pet),“子类”中不“重写”父类的方法时,可以调用父类的方法;如果重写了父类的方法,会调用重写的
    • 这有点像继承了“父类”的数据和行为,果真如此?测试:
    func TestExtend(t *testing.T) {
      var cat Cat= new(Cat)  // cannot use new(Cat) (type *Cat) as type Cat in assignment
      // cat := new(Cat)
      cat.SpeakTo("Roy!")
    }
    
    • 如果是继承,使用上述类型声明的方式定义,应该是没问题的,但并不能assign type
    • 也就是说:go不支持继承(也就不能叫重写),只支持复用(匿名嵌套调用)父类的方法
    • 或者说:不支持LSP

多态

  • 一般多态的概念是指:父类指针指向子类对象的能力,翻译成人话就是在主函数中传入父类作为参数,却可以调用子类的方法
  • go语言中的多态是类似的,看个例子:
    1

空接口

  • 类似Java的Object类型,可以表示任何类型p interface{},先名称,后类型
  • 一般通过断言将空接口转换为指定类型
    package myvoid
    
    import "testing"
    import "fmt"
    
    func VoidConvertOther(p interface{}) {  // 这就是一个空接口
      // go语言很多函数都会返回执行状况(true/false),我们也要学习这种风格
      // if语句中调用函数,获取返回值
      if i, ok := p.(int); ok {  // 断言的使用: p.(xxx) 即p是xxx类型
        fmt.Println("to integer", i)
        return
      }
    
      if i,ok := p.(string); ok { //
        fmt.Println("to string", i)
        return
      }
    
      fmt.Println("nothing!")
    }
    
    func VoidInterface(p interface{}) {
      switch v := p.(type) {
      case int :
        fmt.Println("to integer", v)
      case string :
        fmt.Println("to string", v)
      default:
        fmt.Println("nothing!")
      }
    }
    
    func TestAssert(t *testing.T) {
      VoidConvertOther(10)
      VoidInterface("roy")
    }
    
    • 这里不是强制类型转换,空接口可以转换为任何类型
    • 一般用assert判断,而这里有转换的功能
  • 总结一下接口的使用:
    2
    • 类似于结构体的复用,接口也可以复用,由多个小接口组成
    • 定义新接口时,尽量满足最小接口定义(只包含一个方法),方便复用,因为接口中定义的方法都要实现,这和Java等其他语言是一致的!

错误

  • go语言没有异常机制,创始者认为任意的定义异常对程序不友好
  • 利用go支持多返回值得特性,使用errors.New()生成并错误信息,而且一般在外部定义错误变量
    package myfunc
    
    import "testing"
    import "errors"
    
    var ParamOutofRange = errors.New("n should be in [2,100]")  // 一般会定义错误变量
    
    func Fibonacci(n int) ([]int, error) {   // 定义错误发生时的返回值
      if n<2 || n>100 { // 参数检查
        return nil, ParamOutofRange
      }
    
      fibList := []int{1, 1}
    
      for i:=2; i<n; i++ {
        fibList = append(fibList, fibList[i-1]+fibList[i-2])
      }
    
      return fibList, true // 注意返回值
    }
    
    func TestFibonacci(t *testing.T) {
      // t.Log(Fibonacci(6))  // [1 1 2 3 5 8]
      if v, err := Fibonacci(1); err != nil {
        t.Error(err)
      } else {
        t.Log(v)
      }
    }
    
    • 因为多返回值,在函数正常执时也要定义错误的返回
    • 一般将判断校验写在前面:快速失败,避免嵌套

panic

  • 除了使用errors报错,还可以用panicos.Exit(-1)退出程序,提示错误
    package myfunc
    
    import "testing"
    import "errors"
    import "fmt"
    import "os"
    
    func TestPanic(t *testing.T) {
      defer func() {
        fmt.Println("defer finally!")
      }() // 会执行
      panic(errors.New("wrong occur! exit"))  // wrong occur! exit 输出错误
    }
    
    func TestOSExit(t *testing.T) {
      defer func() {
        fmt.Println("defer finally!")
      }() // 不会执行
      fmt.Println("start================!")
      os.Exit(-1)
      fmt.Println("end!") // 不执行
    }
    

recover

  • recover可以让进入宕机流程中的 goroutine 恢复过来,仅在延迟函数 defer 中有效
  • 调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行
    func TestRecover(t *testing.T) {
      defer func() {
        if err:=recover(); err!=nil {
          fmt.Println("recover from panic!",err)  // recover from panic! Wrong occur! exit
        }
      }() // 会执行
      panic(errors.New("Wrong occur! exit"))
    }
    
    • 注:通常不应该对进入 panic 宕机的程序做任何处理,但有时在程序崩溃前,需要做一些操作
    • 例如:web崩溃,应该将所有的连接关闭,如果不做任何处理,会使得客户端一直处于等待状态
    • 例如:崩溃前服务器可以将异常信息反馈到客户端,帮助调试
    • 但!recover有可能产生僵尸进程,啥活不干了资源还不释放,我们上面的例子只做个输出就很有可能这样;而且还会避开health check!
    • 所以:一般情况下最好直接干掉进程并重启,不要恢复;需要权衡

  • 最基本的可复用模块:package
  • 前面说过,同一目录里的package名要一致
  • 系统到指定位置寻找包,需要配置:GOPATH,声明包的存放位置;Windows直接在环境变量增加此路径
    • 默认应该会在C:\Users\Windows10\go存储包,分号隔开添加即可
    • Linux中在配置文件export即可
    • 重启Atom
  • 在GOPATH下,会得到bin和src文件夹,定位包从src下开始即可
    // src/pack/fib/fib.go
    package series
    
    import "errors"
    
    var ParamOutofRange = errors.New("n should be in [2,100]")  // 一般会定义错误变量
    
    func Fibonacci(n int) ([]int, error) {   // 定义错误发生时的返回值
      if n<2 || n>100 { // 参数检查
        return nil, ParamOutofRange
      }
    
      fibList := []int{1, 1}
    
      for i:=2; i<n; i++ {
        fibList = append(fibList, fibList[i-1]+fibList[i-2])
      }
    
      return fibList, nil // 注意返回值
    }
    
    // src/pack/use_fib/fib_test.go
    package usefib
    
    import "testing"
    import "pack/fib" // 导入包所在的文件夹即可,因为同一文件夹下包名相同
    
    func TestFibonacci(t *testing.T) {
      // t.Log(Fibonacci(6))  // [1 1 2 3 5 8]
      if v, err := series.Fibonacci(10); err != nil { // 包名. 调用
        t.Error(err)
      } else {
        t.Log(v)
      }
    }
    
    • 所以说,以后写go从指定的src下开始,不然定义的包也找不到

init

  • 在main被执行前,所有的依赖包的init方法会先执行
  • 每个源文件也可以有多个init函数
    func init() {
      fmt.Println("init1")
    }
    
    func init() {
      fmt.Println("init2")
    }
    

远程包

  • 使用go ger获取远程依赖,以concurrent_map为例
  • clone代码:go get -u github.com/easierway/concurrent_map 不带.git,-u表示强制从网络更新
    package cmap
    
    import "testing"
    import cm "github.com/easierway/concurrent_map"
    
    func TestConMap(t *testing.T) {
      m := cm.CreateConcurrentMap(99)
      m.Set(cm.StrKey("key"), 10)
      t.Log(m.Get(cm.StrKey("key")))
    }
    
    • 注意代码在GitHub的形式,从src下开始,不包括src,以适应go get(它要把包都放到src下)
    • 下载的包默认放到GOPATH的第一个参数所指位置

协程

  • 并发编程中,Go最具特点的就是协程机制了

  • 线程和协程之间
    1

  • 操作系统中分:用户态和内核态,内核暴露了系统调用,作为接口给用户使用

    • 一般是一个用户态线程对应一个内核态实体(entity),在竞争CPU资源时,如果竞争后需要在内核态执行操作,就要切换,也就要保存context(跨态);这样做开销是比较大的
    • Groutine和内核实体的对应关系是多对一,很多协程任务在一个线程内搞(用户态,完全用户自己控制),go自身的调度器完成,一定程度上避免了context切换,节省开销
    • 一个协程所需的栈空间2K起,最大到1G;Java1M起,大了几百倍
    • 如图所示:M:system thread P:processor G:goroutine
      2
    • 从图中可以看出,一个进程挂了多个go协程
    • 使用类似队列的形式处理协程任务,并用一个守护线程监听,避免某个任务响应时间过长
      • 当某一个协程要IO(阻塞了),其他协程会挂到另外的线程上继续执行
      • 更多go调度器原理后面会讲(核心),
      • 以函数为单位,一个func一个协程;如何定义一个func,让并发性能更好?
      • 一个线程内调度和多个线程间调度协程都好办,因为都依赖同一个进程的资源
    • 很多说法是协程相当于微线程,这是体量上;从这里也可以看出,多个协程挂在一个进程上,进程分配线程来处理队列(相当于一个线程挂多个协程,也叫主线程)
      • 当一个协程被中断,其寄存器状态也会保存到协程对象,恢复后加到队列末尾继续执行(go的context很简单)
    • go协程并发性能好的基本原理:更小量级的切换、更细分的任务调度(类似流水线,更充分的资源使用;也方便监控,监控到了才能触发调度)
      • 资源是一定的,就看如何更充分的使用
      • 理解有待深化,继续学习!
  • 使用协程:go func函数为基础

    package gr
    
    import "testing"
    import "fmt"
    
    func TestGoR(t *testing.T) {
      for i:=0; i<10; i++ {
        go func(i int) {
          fmt.Println(i)  // 0 2 4 5 ...
        }(i)
      }
    }
    
  • 了解了协程的基本原理,针对协程兄弟们,还有一些机制,用来协调竞争、通知消息等;下面这部分属于go调度器啦(都是并发机制),熟悉这些概念和用法:

共享内存

  • 协程依赖线程,多个线程也是共用进程的资源,协程也就涉及到资源共享
  • 多个协程操作同一变量就要共享内存
  • 看看两种方式
    package share
    
    import "testing"
    import "sync"
    import "time"
    
    func TestShareSafe(t *testing.T) {
      var mut sync.Mutex
      counter := 0
      for i:=0; i<1000; i++ {
        go func() {
          defer func() {
            mut.Unlock()
          }()
          mut.Lock()
          counter++
        }()
      }
      time.Sleep(1 * time.Second) // 别让主线程先退出了!
      t.Logf("counter=%d", counter) // 1000
    }
    
    func TestShareSafeWait(t *testing.T) {
      var mut sync.Mutex	// 多协程,必须加锁,wg只是保证主线程不退出的另一种方式
      var wg sync.WaitGroup
      counter := 0
      for i:=0; i<1000; i++ {
        wg.Add(1) // 计数器 +1
        go func() {
          defer func() {
            mut.Unlock()
          }()
          mut.Lock()
          counter++
          wg.Done() // 计数器 -1
        }()
      }
      wg.Wait() // 阻塞,等待所有任务完成, 类似join
      t.Logf("counter=%d", counter) // 1000
    
    • 注意一下线程和协程的区别,类似进程和线程;主线程不能先退出,否则依托于线程的协程会被强制结束!
    • 这里的两种方式是读写锁(可并发读,串行写)和互斥锁(串行读写)的区别==???==

CSP

  • 上面介绍了共享内存的并发机制,这里介绍go语言特有的并发机制:communicating sequential processes
    • 并发机制主要指处理竞争的方式,包括加锁、通信协调等
    • CSP+chan是go语言最常用的通信协调机制
    • 后续的所有机制或者说任务场景都是基于CSP+chan
  • 使用channel完成通信实体之间的协调:chan
    package mycsp
    
    import "testing"
    import "fmt"
    import "time"
    
    
    func Service() string {
      time.Sleep(time.Millisecond * 50)
      return "Done"
    }
    
    func FuckTask() {
      fmt.Println("this is a fuck task! nothing")
      time.Sleep(time.Millisecond * 50)
      fmt.Println("fucking task done!")
    }
    
    func AsyncService() chan string {
      // retCh := make(chan string)  // 通道里只能放 string
      retCh := make(chan string, 1)   // buffer channel 只能放一个值
      go func () {
        ret := Service()
        fmt.Println("get service return!")
        retCh <- ret  // 放到channel
        fmt.Println("push into channel!")
      }()
      return retCh
    }
    
    func TestAsyncService(t *testing.T) {
      retCh := AsyncService()
      FuckTask()
      fmt.Println(<-retCh)  // 从channel取值
    }
    
    • 直接使用channel的结果
    this is a fuck task! nothing
    get service return!
    fucking task done!
    Done
    push into channel!	// 没有限定buffer值,协程阻塞,等待取值完才继续执行
    
    • 使用channel buffer的结果
    this is a fuck task! nothing
    get service return!
    push into channel!	// 我们不需要阻塞等待从channel取值,这样比较好!
    fucking task done!
    Done
    
    • 这种异步的通信协调方式很常用!chan是go语言通信的基础
  • CSP也和Actor model不同
    • Actor直接通信,mailbox容量无限
    • csp使用channel(中间人)通信,松耦合,容量限制

多路选择

  • 处理协程当然不止竞争关系,还要控制一下协程兄弟们,比如时间上:
  • 使用多路选择实现协程的超时控制,必须用chan
  • 形式上类似switch case语句,只要有任何一个case在timeout之前完成程序就可以正常执行
  • 如果多个case都没有超时那就都能执行,因为是协程,case只是等待后面代码的结果
    • 只是形式上类似switch,并不等待某个变量的结果选择case分支,每个case后都会执行
    • time.After在这里是过滤的作用
    package myselect
    
    import "testing"
    import "fmt"
    import "time"
    
    func TimeSleep() string{
      time.Sleep(time.Millisecond *50)
      return "fuck over!"
    }
    
    func AsyncTimeSleep() chan string{
      retCh := make(chan string, 1)
      go func() {
        ret := TimeSleep()
        retCh <- ret
        fmt.Println("fuck really over!")
      }()
      return retCh
    }
    
    func Service() string {
      time.Sleep(time.Millisecond * 50)
      return "Done"
    }
    
    func AsyncService() chan string {
      retCh := make(chan string, 1)   // buffer channel
      go func () {
        ret := Service()
        fmt.Println("get service return!")
        retCh <- ret
        fmt.Println("push into channel!")
      }()
      return retCh
    }
    
    func TestAsyncService(t *testing.T) {
      select {
      case retCh := <-AsyncService():	// 必须是个chan,传递信号
        t.Log(retCh)
      case fuckDone := <-AsyncTimeSleep():
        t.Log(fuckDone)
      case <-time.After(time.Millisecond * 100):  // 程序运行超过100ms即报错
        t.Error("time out") // time out
      }
    }
    

广播

  • 广播机制,为了通知协程兄弟们某件事情,比如通道关闭
  • 以生产者-消费者模型为例,了解channel关闭的一些细节
    package mychan
    
    import "testing"
    import "fmt"
    import "sync"
    
    
    func DataProducer(ch chan int, wg *sync.WaitGroup) {
      go func() {
        for i:=0; i<10; i++ {
          ch <- i
        }
        close(ch) // 关闭通道
        wg.Done()
      }()
    }
    
    func DataConsumer(ch chan int, wg *sync.WaitGroup) {
      go func() {
        for { // while
          if data, ok := <-ch; ok {
            fmt.Println(data) // 正常取值
          } else {
            break
          }
        }
        wg.Done()
      }()
    }
    
    func TestPC(t *testing.T) {
      var wg sync.WaitGroup
      ch := make(chan int)
      wg.Add(1)
      DataProducer(ch, &wg)
      wg.Add(1)
      DataConsumer(ch, &wg)
      wg.Add(1)
      DataConsumer(ch, &wg)
      wg.Wait()
    }
    
    • 向关闭的channel发送数据会导致panic,从关闭的channel取值?
    • 取值时data, ok中如果OK为false表示通道关闭;这会向多个channel接收者同时发送退出信号,这就是传说中的广播机制

cancel

  • 任务的取消,可以通过传值后做判断break/down掉,或者使用广播close(chan)
  • 同样的,还是用到channel
    package mycancel
    
    import "testing"
    import "fmt"
    import "time"
    
    func IsCancelled(canChan chan struct{}) bool {
      select {
      case <- canChan:  // 如果通道有值就返回true,break任务(注意这是从通道取,不是放)
        return true
      default:
        return false
      }
    }
    
    func Cancel_1(canChan chan struct{}) {
      canChan <- struct{}{} // 传入个值,用来cancel
    }
    
    func Cancel_2(canChan chan struct{}) {
      close(canChan)    // 广播的方式,全部关闭
    }
    
    func TestCancel(t *testing.T) {
      canCh := make(chan struct{}, 0)
      for i:=0; i<5; i++ {  // 开5个协程
        go func(i int, canCh chan struct{}) {
          for { // 死循环住,一直判断
            if IsCancelled(canCh) {
              break
            }
            time.Sleep(time.Millisecond * 5)
          }
          fmt.Println(i, "Cancelled")
        }(i, canCh) // 
      }
      // Cancel_1(canCh) // 4 Cancelled  只能关闭最后一个;因为我们只传入了一个(值)信号,没取到结束信号就继续判断
      // Cancel_1(canCh)
      // Cancel_1(canCh)
      // Cancel_1(canCh)
      // Cancel_1(canCh)  // 如果想全部关闭只能调用 5 次
      Cancel_2(canCh) // 4 Cancelled  3 Cancelled 2 Cancelled 0 Cancelled 1 Cancelled
      time.Sleep(time.Second * 1)
    }
    
  • context 与子任务取消;这里就不用channel了!
    1
    • 取消任务需要一并取消其子任务,这就要创建context(背景上下文)
      2
    • 必须看个例子:本质还是使用channel(了吧)
      package mycancel
      
      import "testing"
      import "fmt"
      import "time"
      import "context"
      
      func IsCancelled(ctx context.Context) bool { // 这里就不用通道传递信号了
        select {
        case <- ctx.Done():  // 接受消息通知,取到值就取消任务
          return true
        default:
          return false
        }
      }
      
      func TestCancel(t *testing.T) {
        ctx, cancel_root := context.WithCancel(context.Background()) // 根节点上下文
        for i:=0; i<5; i++ {  // 5个平行的根节点
          go func(i int, ctx context.Context) {
            for {
              if IsCancelled(ctx) {
                break	// 任务取消
              }
              time.Sleep(time.Millisecond * 5)
            }
            fmt.Println(i, "Cancelled")
          }(i, ctx) //
        }
        // 重点:
        cancel_root()  // 就会给ctx传入消息,本质还是channel,.Done()方法就能取到值
        time.Sleep(time.Second * 1)
        /*
        4 Cancelled
        0 Cancelled
        1 Cancelled
        2 Cancelled
        3 Cancelled
        */
      }
      
    • cancel根节点其子节点都会取消

once

  • 类似Java的单例模式:保证代码只执行一次,看个例子:Once
    package singleton
    
    import "testing"
    import "fmt"
    import "sync"
    import "unsafe"
    
    type Singleton struct { // 类
    
    }
    
    var singleton *Singleton  // 声明类型定义变量
    var once sync.Once  // 使用单例
    
    func GetSingleObj() *Singleton{
      once.Do(func() {	// 函数形式
        fmt.Println("create obj via singleton")
        singleton = new(Singleton)  // var声明了,直接 =
      })
      return singleton
    }
    
    func TestSingle(t *testing.T) {
      var wg sync.WaitGroup
      for i:=0; i<5; i++ {
        wg.Add(1)
        go func() {
          // 具体什么原理呢?
          obj := GetSingleObj()	// 虽然五个协程,但这个函数只会调用一次,后面都是赋值!!!
          fmt.Printf("%x\n", unsafe.Pointer(obj))  // 打印地址
          wg.Done() // 协程结束, 计数器 -1
        }()
      }
      wg.Wait()
    }
    /*
    create obj via singleton
    685378
    685378
    685378
    685378
    685378	// 地址都是一样的,只创建了一次
    */
    

有完成即返回

  • 有时候启了多个任务,但只需要有一个任务完成并返回值就可以,其他任务不需要再执行
  • 其实就是形式上,channel一有值就return
  • 注意要使用buffer channel,避免协程泄露!
    package any
    
    import "testing"
    import "fmt"
    import "runtime"
    import "time"
    
    func MyTask(id int) string {
      time.Sleep(time.Millisecond * 60)
      return fmt.Sprintf("this result is from %d", id)
    }
    
    func FirstResponse() string {
      numRunner := 6
      // ch := make(chan string)
      ch := make(chan string, numRunner)  // 设置了buffer,就不用等待接受者把消息拿走! 避免协程泄露!
      for i:=0; i<numRunner; i++ {
        go func(id int) {
          ret := MyTask(id)
          ch <- ret // 放入通道返回
        }(i)
      }
      return <-ch   // 等待第一次响应,就取值,没有值就阻塞;就形式上实现了所谓的 “只需要有一个任务完成就返回值”
      // 其他的协程也运行了,但已经不需要了!
    }
    
    func TestFirst(t *testing.T) {
      t.Log("before", runtime.NumGoroutine())  // 协程数 before 2
      t.Log(FirstResponse())
      time.Sleep(time.Second *2)
      t.Log("after", runtime.NumGoroutine())  // after 7 协程泄露(没用还没人管)!  采用buffer channel就能避免 after 2(不是这个任务里的,系统默认协程)
    }
    
    • 其实最大的不同就是这里不用WaitGroup的Wait()阻塞等待了
  • 类似的:所有任务完成,得到结果集,再返回
    • 可以用Wait(),但这里直接拼接所有结果返回;也就是借助CSP+chan
    package any
    
    import "testing"
    import "fmt"
    import "runtime"
    import "time"
    
    func Task(id int) string {
      time.Sleep(time.Millisecond * 60)
      return fmt.Sprintf("this result is from %d", id)
    }
    
    func AllResponse() string {
      numRunner := 6
      // ch := make(chan string)
      ch := make(chan string, numRunner)
      for i:=0; i<numRunner; i++ {
        go func(id int) {
          ret := Task(id)
          ch <- ret
        }(i)
      }
      allSet := ""
      for j:=0; j<numRunner; j++ {
        allSet += <-ch + "\n" // 拼接结果,不推荐使用 +=
      }
      return allSet
    }
    
    func TestAll(t *testing.T) {
      t.Log("before", runtime.NumGoroutine())  // 协程数 before 2
      t.Log(AllResponse())
      time.Sleep(time.Second *2)
      t.Log("after", runtime.NumGoroutine())  // after 7 协程泄露!  采用buffer channel就能避免 after 2
    }
    

对象池

  • 数据库连接、网络连接一般都要池化
    1
  • 实现池的两个核心方法:获取 释放
    package mypool
    
    import "testing"
    import "fmt"
    import "time"
    import "errors"
    
    type ReusableObj struct { // 连接类,它的实例代表一个已连接的对象,要放入池
    
    }
    
    type ObjPool struct { // 连接池类
       bufChan chan *ReusableObj
    }
    
    // 连接池类方法,得到池内对象 (得到连接)
    func (p *ObjPool) GetObj(timeout time.Duration) (*ReusableObj, error) {
      select {
      case ret := <-p.bufChan:
        return ret, nil
      case <-time.After(timeout):
        return nil, errors.New("time out error!")
      }
    }
    
    // 连接池类方法,释放连接
    func (p *ObjPool) ReleaseObj(obj *ReusableObj) error {
      select {
      case p.bufChan <- obj:  // 还给池对象
        return nil  // 没error
      default:
        return errors.New("release obj fail!")
      }
    }
    
    func NewPool(numOfPool int) *ObjPool {  // 初始化池对象
      objPool := ObjPool{}  // 实例化类
      objPool.bufChan = make(chan *ReusableObj, numOfPool)  // 属性赋值
      for i:=0; i<numOfPool; i++ {
        objPool.bufChan <- &ReusableObj{} // 存已创建好的连接对象
      }
      return &objPool
    }
    
    func TestObjPool(t *testing.T) {
      pool := NewPool(4)  // 得到连接池
      for i:=0; i<5; i++ {  // 如果不释放,只有4个连接,timeout
        if v, err := pool.GetObj(time.Second * 1); err != nil {
          t.Error(err)
        }else {
          fmt.Printf("%T\n", v)
          // if err := pool.ReleaseObj(v); err != nil {
          //   t.Error(err)
          // }
        }
      }
    }
    
    • 使用池对象是否一定会带来性能的提高呢?不一定
    • 这里的池使用buffer channel实现,为了协程安全会有锁机制,有时会影响性能
    • 对于一些易创建的对象可以考虑不使用池化技术

sync.Pool

  • 注意,这不是上面的池化技术,而是对象缓存
    • 获取对象
      1
    • 私有对象是协程安全的,共享池是协程不安全的(需要加锁)
      2
    • 使用方法
    package mysync
    
    import "testing"
    import "fmt"
    import "sync"
    import "runtime"
    
    func TestSyncPool(t *testing.T) {
      pool := &sync.Pool{
        New : func() interface{} {  // 空接口,可返回任意类型
          fmt.Println("create a new sync pool")
          return 100
        },
      }
      v := pool.Get().(int) // assert int型
      fmt.Println(v)
      pool.Put(666)
      // 手动触发一次GC,一般不使用
      runtime.GC()
      v1, _ := pool.Get().(int)
      fmt.Println(v1) // create a new sync pool  100
      v2, _ := pool.Get().(int)
      fmt.Println(v2) // create a new sync pool  100   已经被拿走了,所以会再次调用New方法
    }
    
    • 可以使用WaitGroup测试,sync.Pool是协程安全的
    • 这也就意味着要加锁,使用它缓存就要权衡创建对象快还是加锁缓存快
  • 为什么不能做池
    • 每一次GC都会清除sync.Pool缓存的对象
    • 对象的缓存有效期为下一次GC前,而GC无法人为干预
举报

相关推荐

0 条评论