文章目录
函数
- 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语言中的多态是类似的,看个例子:
空接口
- 类似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判断,而这里有转换的功能
- 总结一下接口的使用:
- 类似于结构体的复用,接口也可以复用,由多个小接口组成
- 定义新接口时,尽量满足最小接口定义(只包含一个方法),方便复用,因为接口中定义的方法都要实现,这和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报错,还可以用
panic
和os.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最具特点的就是协程机制了
-
线程和协程之间
-
操作系统中分:用户态和内核态,内核暴露了系统调用,作为接口给用户使用
- 一般是一个用户态线程对应一个内核态实体(entity),在竞争CPU资源时,如果竞争后需要在内核态执行操作,就要切换,也就要保存context(跨态);这样做开销是比较大的
- Groutine和内核实体的对应关系是多对一,很多协程任务在一个线程内搞(用户态,完全用户自己控制),go自身的调度器完成,一定程度上避免了context切换,节省开销
- 一个协程所需的栈空间2K起,最大到1G;Java1M起,大了几百倍
- 如图所示:M:system thread P:processor G:goroutine
- 从图中可以看出,一个进程挂了多个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了!
- 取消任务需要一并取消其子任务,这就要创建context(背景上下文)
- 必须看个例子:本质还是使用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根节点其子节点都会取消
- 取消任务需要一并取消其子任务,这就要创建context(背景上下文)
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()
阻塞等待了
- 其实最大的不同就是这里不用WaitGroup的
- 类似的:所有任务完成,得到结果集,再返回
- 可以用
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 }
- 可以用
对象池
- 数据库连接、网络连接一般都要池化
- 实现池的两个核心方法:获取 释放
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
- 注意,这不是上面的池化技术,而是对象缓存
- 获取对象
- 私有对象是协程安全的,共享池是协程不安全的(需要加锁)
- 使用方法
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无法人为干预