关闭

go学习

1252人阅读 评论(1) 收藏 举报

        对于go的学习,需要注意的地方。


        1、特别注意,go中可以使用go env命令查看我们可以在环境变量中设置的变量。

        2、一般的输出使用内建函数print或者println就可以了,fmt.printXXX函数有更多的作用,比如格式化输出。同时注意,内建的println并不是很好用,一般都是会在函数运行完之后才会被调用。

        3、注意,在go中,同时存在函数和方法两种概念,这点和java是不一样的。在go中,类的函数叫做方法,而不是类的函数就是函数。

        4、系统输出数组喜欢用[]并使用空格作为元素分隔,所以不要以为[]和空格是数据本来就又的,这点需要注意!

        5、java以文件学习,因为java中国一个文件对应一个类,而go以模块学习,因为go中都是直接调用模块的函数,而模块的函数都分布在模块下的文件里面。

        6、代码块中的:=短式声明在代码块外不起作用,另外,多使用go tool vet -shadow your_fil.go检查隐藏变量。

        7、GOBIN环境变量可以让go install的exe文件放置在GOBIN下。

        8、函数有多少个返回值,在给变量赋值的时候就需要有多少变量需要来获取这些返回值,但是如果使用range就可以只获取第一个返回值,这点在range可以操作channel就可以看出来了。

        9、使用go get url之后,使用import "url"就可以使用这个包了。

        10、可以再var()和const()中声明重复的变量,可以再函数中使用:=短式声明多个变量,其中有变量是与上面的变量重复的,这样也是允许的。但是需要注意,在匿名函数中重复声明匿名函数外的变量,并不会影响匿名函数外的变量,这点需要注意。

        11、一般使用组合来实现继承关系,所以可以很容易查找到继承的关系,也就可以更好的给函数传值了,一般不会要求通过看函数实现来定位继承关系。

        12、实例化可以分为有序初始化和无序初始化。无序初始化:数组{1:“aaa”,3:“bbb”},类xxx{name1:“ddd”,name2:234}

        13、变量不可以使用iota,因为常量如果没有被赋值,他会跟上一个常量一个值,而变量不会。

        14、需要特别注意,只有append()函数可以给slice增加容量,不要以为使用超出slice的index的XXX[index]就可以给slice增加容量,这是错误的,需要注意。另外,append函数给slice增加元素时,如果元素少于1000时,容量按照2倍增加,多于1000时1.5倍增加。

        15、使用os.OpenFile设置打开的时候清空目标文件的数据,或者使用File.Truncate函数清除数据都可以用于清楚数据,这里需要注意的是go的清除数据函数使用truncate命名而不是clean或者clear,这是因为truncate可以清除部分数据,这样更加简洁了。

        16、go中正则使用regexp模块,使用regexp.complie函数返回一个Regexp对象,使用Regexp.FindXXX函数查找,最后使用FindSubmatch函数可以查找完整匹配和子项匹配,这点跟java的group(n)一样。另外,使用fmt.Printf("%q", )可以格式化输出,特别是像regexp.Split函数返回的[][]byte数组,更加应该通过fmt.Printf("%q", )输出,不然输出的是数字。

        17、注意,go中是根据属性来放置容器类的,所以container模块中没有多少文件,而io模块中有pipe文件

        18、io.WriteString和ioutil.WriteFile形成的文件是文本格式的,而文本格式会添加东西,所以又出错的可能,而io.Copy函数则不会,因为是根据字节复制的。

        19、go的编码包中,一般字符串的编码解码使用Marshal和Unmarshal函数,对于流式读写则使用NewEncoder和NewDecoder函数生成需要的编码解码类,比如对xml,json,gob操作。最后需要注意,go的编码都是utf-8的,所以需要查看操作的编码格式,如果在网页中的话,可以在header的meta中查看charset就能知道编码格式了。

        20、windres.exe可以给go的exe程序打包图标,windres.exe在MinGW,tdm-gcc中都可以找到。

        21、特别注意,switch后面的表达式可以是任意类型,不限于常量,同时可以先使用初始化语句然后使用“;”分隔后面的表达式。case后面的表达式可以是参数列表例如1,2,3,甚至是值,例如arr[1]。另外,别忘了还可以使用default。

        22、在go中,使用T.(X)这种方式实现类型断言,其中T必须是interface{},而X可以是任意类型,甚至实际上type关键字也是可以的,只是如果是type关键字的话就只能在switch中使用了。最后注意,使用断言还可以实现类型转换,就是将interface{}转换为需要的类型,就是X,Ok := T.(X),其中T是interface{},X是需要转化的类型。如果类型断言使用type关键字,那么需要配合switch使用,同时返回单一参数,因为返回的是类型,如果是系统内置类型就不用,同时可以返回两个参数,第二个是查看是否符合。

        23、可以将函数名作为参数传递给函数,这点跟python一样。例如定义一个函数X(),然后定义一个函数XX(src func()){src()},然后调用XX(X)。

        24、go中应该注意,对于变量,应该分为声明和初始化,生命在左边,而初始化在右边。完整的变量声明与初始化时左右两边都有声明的,左边声明变量类型,而右边声明初始化的对象类型,=号用于赋值。也就是说完整的表达式是左右两边都有声明。但是基本数据类型可以只写左边声明,或者只写右边声明,甚至使用:=短式,完全不写声明。但是如果是复合类型,那么就只能是使用:=短式和右边声明。也就是说初始化的复合类型对象的类型是必须要写的,例如var xx = []string{}可以,但是var xx []string = {"d", 'ss"}却不行。简单点说其实就是复合类型已经是类了,系统无法自行辨认。

        25、特别注意,在使用数组声明并初始化时,需要写明数组的个数,声明和初始化都要。如果少写了,虽然在编码时不会报错,但是编译会出错!

        26、特别注意,在go中是以模块作为import的,所以不能出现模块环引用,这会报错,避免的方法是传值:传递变量,传递函数,传递类对象。特别是传递类对象这点,是常见的方法,就是使用接口代替原本需要import的类提供给函数作为参数,而这个接口在不同的模块,这样就不会造成环引用了。总的来说,就是使用函数传值。

        27、在go中,其实也是可以实现abstract抽象的,例如:type Test struct{New func() interface{}},这里声明一个New函数,这个函数返回一个interface{}对象。其实这种方式就是将函数当成变量。

        28、注意,在go中给变量生命并赋值之后,变量的地址就已经是一定的不能更改的,所以尽管可以给这个变量重新赋值,但是这个地址仍旧不会改变,所以如果需要改变地址,那么就需要使用指针!

        29、类的方法是针对类实现的功能封装,而一般的函数会对类的方法进行进一步的封装。所以往往函数更加成熟,而类方法更加底层。

        30、数组的实例化,例如[]string的实例化必须是[]string{“aa”, “bb”},而string{“aa”, “bb”}是错误的。其实这点根据go的类型转换必须是显式转换的就可以知道。

        31、像java的对象,其实底层都是使用指针的,而go的源代码可以看出,其实go绝大多数的对象也是使用指针进行的操作。所以以后注意,多使用指针进行操作,尽管go允许使用非指针对象。

        32、go源代码中multiple slash是//号,而separator是\符号。









        在go中调用C/C++是非常容易的,可以分为两种:1、可以直接嵌入。在import “C”上使用//或者/**/然后再里面添加需要的例如#include xxx或者代码。这里还可以使用#cgo操作cgo命令,例如#cgo LDFLAGS : -L ./ -lfoo告诉编译器,链接当前目录下lfoo共享库,也就是so文件,如果在编译中使用so库,使用命令go build -ldflags="-r DIR",其中DIR是so库目录。2、在src下创建一个专门存放c代码的文件夹,然后在里面添加h,c,go文件,go文件用于将c,h包装给外面使用。例如下面的例子:


fun.c:

#include <stdio.h>
#include <stdlib.h>

char* MySecret() {
  return "my secret not tell anybody";
}


fun.h:

extern char* MySecret();


fun.go


package misc


//注意,这里可以使用#include dir来引进某个c库,实际上include就是为了引进文件
//C:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0\ucrt下有我们需要
//的如stdlib.h等文件


// #include "fun.h"
import "C"

func MySecret() string {
    return (C.GoString(C.MySecret()));
}



        网络编程大致上分为b/s,c/s两种。而go中,net实现底层网络功能,net.http实现高级的网络功能。net模块一般用于c/s,而net.http则是c/s,b/s都可以。但是无论是哪种实现方式,其实都是客户端请求数据,而服务端监听并给予数据。

        在go中使用net.Dial函数就可以实现客户端的socket功能了,在服务端有net.Listen函数作为服务端的socket功能。这是go的net底层的功能,也是实现c/s的基础方式。

        对于常见的方式,应该是使用net.http模块的功能。一个url对应一个网络资源,常见的Get,Post,Put,Delete分别对应增删查改,get查询,post修改,put增加,delete删除。网页的获取方式是get,所以一般get不会被拒绝。这里客户端一般使用net.http.Get()或者net.http.Post()进行数据获取,而服务端使用net.http.ListenAndServe()监听并操作数据,如果是浏览器,其实也是通过net.http.Get()获取数据的,net.http.Post()交互数据的。



        对于go的包,不要命名为main,all,std等,不要再src下创建main包,因为在src下创建一个文件并添加package main和main函数就可以作为程序的入口文件了。在引用自己项目的东西的时候,需要使用import "./xxx"而不能是直接import  "xxx",不然会在编译的时候出错。这样做其实是为了不和go系统下的文件混淆,当然,如果直接将项目放到GOPATH下的第一个路径,那么可以直接使用import  "xxx",因为这个时候,项目被当成系统文件了。

        另外,自己的项目模块的命名最好不要跟系统的模块名字一样,因为系统分辨不出来。

        最后需要注意,在go项目下点号"."代表的是当前项目路径。

        使用go get命令下载并编译的文件会放在GOPATH的第一个路径下,另外,我们的项目也是需要添加到GOPATH下的。最后注意,项目定位到项目名,而不是工作空间名。另外注意,对于需要翻墙的,如果使用cmd,putty,一般网上的代理配置完之后都没用,可以使用git clone直接下载需要翻墙的部分,当然git需要配置代理。然后使用go get下载不用翻墙的部分。其实go get就是使用git clone之后编译下载的项目而已。

        当然我们可以使用gopm这个包管理工具,这个工具可以用于安装,搜索,更新和分享go包,所以对于go的第三方包的管理,使用gopm是个不错的选择。



        在go中只有不可修复的错误才使用defer,panic(),recover(),其他的用error,一般使用errors.New,fmt.Errorf函数都可以创建实现error接口的错误对象。这里注意,errors本身就是用于操作error的,而fmt则是格式化错误输出。

        应该说defer只是延迟操作而已,真正的错误操作是panic和recover函数。其中,panic是保存一个异常信息,因此多次调用panic,当前异常会覆盖上一个异常。其实recover可以在任何地方调用,只是如果没有panic的错误,那么返回的是nil,所以recover只有在defer中调用才能得到异常信息了,而且panic之后程序就返回了。最后需要特别注意,recover需要在defer中直接调用,或者defer后的函数或者匿名函数里面直接调用才可以。不然返回nil,这点可以在进程工作空间看出。



        go中有errors,bytes,strings三个模块分别对应error,byte,string的操作封装,应该说go很喜欢使用XXXs来封装对XXX的操作这种命名模式。当然了,XXutil这种也很常见,比如ioutil模块用于辅助io操作。另外,io模块仅仅是常规的io类创建和操作而已,而bufio模块则封装了更多的io操作,特别是返回数据方面,io模块都是字节数组操作的,很不方便,而bufio有更多的操作给我们使用。最后,bufio模块中的scan模块是用于切分的,关键是里面的Scaner类,传参数给Scaner.Split函数,然后调用Scaner.Scan函数产生一次读取扫描,然后可以进行更多的操作。


        go中,string和[]byte其实是一样的,只是被定义成了不同的类型,并且string拥有了函数。所以bytes,strings两个模块的函数很多很相似,比如两个模块都有去空格函数。应该特别注意,java中String封装的很多函数,在go中就在strings模块中。同样的bytes封装了[]byte的操作。同时注意,封装了很多很好用的正则函数。最后注意,bytes,strings都封装了相应的Reader,Writer类,和NewReader,NewWriter函数,这也是为了读写操作封装的。

        Reader里面的数据读取一次之后就没有了,所以我们可以使用ioutil.FindAll函数得到字节数组,然后将字节数组传递给bytes.NewReader生成Reader,这样就可以再次操作Reader了。

        需要注意,bytes.NewBufferString跟java中的StringBuffer一个道理,当然可以使用bytes的Buffer类进行读写。另外,使用fmt.Printf跟strings的操作速度一样,而+的操作更快,最快的是使用bytes.Buffer,bytes.NewBufferString的操作。



        go中,最应该掌握的是builtin模块,这个模块里面的都是内建函数,这个模块里面的函数都是用于操作内建数据类型的。比如len函数就是用于得到内建数据类型的数据的长度的。另外,nil的==,!=比较只能是在内置函数中进行,这是因为在go中nil被定义了类型。最后注意,除了指针类型,其他的内置数据类型不能修改指针地址。

        go的内建函数copy用于数组间的赋值,可以将string编程[]byte,然后copy之后,将[]byte转化为string,这同时也是strings.Join函数的原理。

        说到go的类型,就需要注意type,type用于定义而不是用于别名,type不仅仅可以定义struct,interface,func,还可以用在基本数据类型上。另外,struct用{}初始化,func用()初始化,而interface自身是函数集合。另外,可以在type一个func之后给这个func添加成员func。最后注意,可以type一个func赋值给type的interface,只需要两者有共同的函数,这个过程其实就是type一个func作为函数集合,简单点说就是直接实现函数集合,这比创建一个struct去实例化interface要简单得多。例子如下:

type TestInterface interface {Do()}
type FuncDo func()
func (self FuncDo) Do() {self()}
func main() {
 var t TestInterface = FuncDo(func() {println("hello world")})
t.Do()
 }



        对于go的第三方包goquery,提供了方便的操作DOM的API,在分析html中非常有用。goquery借鉴了jquery。注意:jquery可以给网页插入元素,添加效果,所以可以不用熟悉js,而通过jquery的脚本编程给网页添加代码,效果。



        需要特别注意区分cmd.Args和os.Args,其中,其中cmd.Args是我们去调用命令的时候使用的,而os.Args则是系统调用我们编码的命令程序的时候使用的,所以os.Args[0]可以看到调用的路径,而cmd.Args就是命令的名字,但其实只是os.Args[0]看到的是完整的命令的路径而已,其实如果是我们调用程序,那么os.Args[0]只是输入的第一个字符串而已,系统调用才是完整路径。所以如果是go run就会与路径。os命令行比较麻烦,可以使用flag模块,flag可以直接使用类似go run XX.go -x -xx模式,其中x,xx是自定义参数,同时注意可以使用可以使单横杠“-”,也可以是双横杠“--”。



        在go中对于数据类型是需要非常注意的地方,在java中数据类型转换很方便,但是在go中,数据转换不支持隐式转换,所有的转换都需要显式转换。

        注意,xxx()可以用于基本数据类型的转换,但是跟string的转换就不行了,当然由于go中string的本质是int的,所以其实string(int)还是可以的。所以关于string跟其他类型的转换需要使用strconv模块进行。总的来说就是,字符串的转换需要使用strconv模块,其他的基本数据类型之间的转换使用XXX()就可以了。

        另外注意,查看数据的类型有两种方式:

1、

func t(i interface{}) {    //函数t 有一个参数i
    switch i.(type) {      //多选语句switch
    case string:
        //是字符时做的事情
    case int:
        //是整数时做的事情
    }
    return
}
i.(type)只能在switch中使用,所以这种方式局限性比较大。

2、使用Reflect.TypeOf()函数进行判断




        在go中,最关键的就是并发部分的知识点,并发部分可以简单的概括为:go,channel,sync,select,range,runtime。其中sync模块中是辅助并发的函数和类,例如Once,Mutex,RWMutext,WaitGroup,和原子操作atomic模块。并发和网络一般一起使用,因为网络交互如果没有并发,那么不但耗时,而且阻塞主进程,如果是在gui中,那么会造成无响应的情况。最后需要特别注意,对于并发,如果需要明确goroutine之间的运行顺序,应该查看代码的运行顺序,这点特别重要!

        需要注意,在任何系统中,一个程序的运行就只有一个进程,这个进程就是程序的运行实例,除非绕到底层才能创建多个进程。进程是操作线程的,线程是系统可以调度的最小的单位。线程时间片的获取和让出都是由内核决定的,同时内核态的时间片切换时比较耗性能的。


        go中,Locker锁概念的Lock()函数原理其实就是运行这个函数的时候需要得到返回的数据,不然会一直等待。


        可以在goroutine里goroutine,goroutine无论在哪里实现的,都跟实现所在的goroutine没有关系,没有内部外部之分,他们都是独立的,这点需要特别注意!但是注意,主进程goroutine是所有goroutine赖以存在的,所以退出之后其他的goroutine就检测不到了,但是实际上其他的goroutine并没有推出,而是执行完。这点使用goroutine里面在生成goroutine,然后goroutine退出之后,生成的goroutine还可以运行可以看出来。另外,可以使用runtime.NumGoroutine()查看运行的goroutine的数量;使用runtime.Goexit()可以让当前goroutine退出,但是goroutine也会确保defer的运行;使用runtime.Gosched()函数可以让出当前goroutine的时间片,相当于java的yield()方法。


        go关键字开启一个协程goroutine,可以通过runtime.GOMAXPROCS或者环境变量设置,让调度器调用多个线程实现多核并行。使用runtime.Goexit()可以终止当前goroutine,最后需要特别注意,使用runtime.Gosched()可以让出底层线程,暂停协程,放回队列等待再次被调度。这点其实就是java的Thread.yield()功能。


        go的goroutine机制有点像线程池,go里面有三种对象:processor代表上下文或者cpu,work thread工作线程,goroutine。正常情况下,一个processor产生一个工作线程,线程检查并执行goroutine,碰到goroutine阻塞,会启动一个新的工作线程,以充分使用cpu资源,所以有时候工作线程会比cpu对象多很多。同时注意,工作线程控制goroutine去获取processor上下文,所以工作线程是一个中间控制层的作用。所有goroutine都是使用同一个processor上下文的,只是以时间片的方式来轮流使用而已,所以goroutine是排队使用processor,而不是排队使用工作线程的,存在一个runqueue。processor会定期检查这个global runqueue的。

        processor运行遇到goroutine阻塞的时候,会创建一个新的工作线程,然后转到新的工作线程去运行。


        go默认一个processor对应一个工作线程,默认情况下,使用一个cpu,所以processor产生多个工作线程。但是如果设置了runtime.GOMAXPROCS,那么工作线程遇到goroutine阻塞之后,需要创建工作线程的话,会优先另一个processor创建一个工作线程,以保持默认的一个processor对应一个工作线程的默认设置。

        传统的线程是可运行的操作系统能够调度的最小单位,是进程的执行流。无法决定线程什么时候获得时间片,什么时候让出时间片,这些由内核决定。而协程可以有可控的切换时机和较小的切换代价,所以协程不需要内核态的切换。

        需要明确概念,进程是程序的运行实例;并行parallelism是系统多核或多处理器才能实现的,而并发concurrency是逻辑结构,goroutine是并行的;goroutine并不是传统的协程,并不是网上所说的轻量级的线程,它的概念是:goroutine是一个与其他goroutine并发运行在地址空间的函数或方法,一个运行的程序有一个或多个goroutine组成,goroutine与进程,线程,协程等是不一样的,goroutine是运行库功能,而不是操作系统提供的功能。其实,从goroutine使用go func()来实现就可以知道这是实现地址空间执行运行库函数。

        在使用中,如果使用goroutine操作其他goroutine的函数或资源,那么这个函数或资源是可以被修改的,但是注意,如果是使用了Mutex.Lock(),那么由于生成了static函数资源块返回给Mutex.Lock(),所以Mutex.Lock()队列操作的其实是函数资源的复制体,所以在Mutex.Lock(),Mutex.Unlock()之外可以发现被操作的函数资源其实根本就没有变化,这点是goroutine和锁概念需要区分的点。

        最后注意,设置runtime.GOMAXPROCS可以充分利用cpu,适合的场景是cpu密集,并行度比较高的情况,如果是IO密集的话,那么cpu的切换会导致性能损耗。而且go默认使用一个cpu,所以注意设置runtime.GOMAXPROCS。


        使用channel<-xx添加数据,使用<-channel导出数据,一般使用value:=<-channel获取导出的数据,但是需要特别注意的是,使用value,ok :=<-channel获取数据的情况,如果channel被关闭,那么ok就返回false。注意close()函数可以关闭所有的<-channel,取消这个channel的阻塞状态。同时特别注意,如果一个未channel被关闭了,那么使用value:=<-channel得到的值是类型默认值,而如果向关闭的channel进行channel<-value,那么会panic。如果一个缓冲channel被关闭了,那么使用value:=<-channel是可以得到缓冲的值的,直到缓冲数字读完,再读取则得到的值是类型默认值,读取也会panic。

        没有初始化的channel是nil channel,网络上说会阻塞,但是实际上会报deadlock死锁错误。chan XXX则是一个XXX类型的channel,一般使用make(chan XXX)或者make(chan XXX, int)初始化,前者是无缓冲channel,而后者是缓冲channel。最后有一点需要特别注意,channel中有一种特别的情况:单想channel,例如c := make(chan int, 10);var send chan<- =c;var receive <-chan = c;这里单向channel分为只读channel和只写channel,只读channel如果写操作就会报错,而只写channel读操作也会报错,并且单向channel不能转化为一般channel。另外,一般使用缓冲channel进行转化。

        内建函数len(),cap()使用在无缓冲channel只能得到0,而使用在缓冲channel可以得到相应的个数和容量。

        注意,channel有同步模式和异步模式两种模式。而且需要特别注意的是,channel发送会需要对应的goroutine,而goroutine不知道什么时候会有,所以只能报deadlock,这点需要特别注意!这也是为什么当发送时如果没有对应的接收goroutine的话会报deadlock死锁的原因。所以从这点看可以明白,其实当给channel发送数据时,其实channel会去查找接收channel数据的goroutine。

        同步模式使用无缓冲channel。在同步模式下需要发送和接收一一对应,不然的话,如果发送多于接收,那么会导致deadlock死锁错误,但是如果是接收多于发送的话,那么就只是阻塞而已。当然,接收多于发送的情况中,接收需要在其他goroutine才能阻塞,如果在主进程那么还是会deadlock报错。

        异步模式使用缓冲channel。在异步模式下,在缓冲区满之前发送不会报错,直到发送的数量大于缓冲时才会报错。

        总的来说,其实就是<-channel相当于一个goroutine在请求,而channel<-value相当于当前goroutine在查找可以发送数据的goroutine。所以直接实现一个无缓冲的channel,然后就只是发送数据的话,会直接报错。而实现一个缓冲channel,然后发送的数据在缓冲范围内的话,是不会报错的,直到缓冲溢出才会报错。最后接收数据肯定是不会报错的,因为已经是goroutine了,或者说必须在其他goroutine中才不会报错,在主进程还是会deadlock报错,所以channel的接收看成goroutine。

        channel的接收有两种方式,一种是value:=<-channel,这种方式是直接得到值;第二种是value,ok:=<-channel,这种模式中ok是用于判断channel是否已经关闭。在channel的使用中,使用range只能返回一个值,所以range不能使用第二种接收方式。

        如果channel只发送一个数据而被多个goroutine接收数据,那么就随机一个goroutine能接收到数据。

        channel必须使用在不同goroutine之间,不然deadlock死锁报错。

        对于channel的deadlock错误,其实是没有需要的goroutine导致的。channel<-value所在的goroutine需要另外一个可以发送的goroutine,而<-channel需要存在于一个goroutine中,只要满足这两个条件就不会触发deadlock错误。


        channel使用for range获取数据的话,只有在close之后才会退出for循环,不然的话不是在获取数据就是阻塞。同时<-channel可以有多于channel<-value,而channel<-value不可以。



        需要特别注意,在go中实现Timer和心跳是非常简单的,使用time.After()函数和time.Tick()函数就可以实现,这两个函数都是返回channel的,所以配合select就可以实现定时器和心跳了。

        主线程使用for{}进行循环,而并发中,使用select进行阻塞。另外注意,对于select的default,以及解除阻塞之后还想继续实现阻塞,可以使用for{}配合select{},应该说在并发中使用for{}配合select是最常用的方式。注意,select{}会一直循环而不停止。

        sync模块是辅助模块,原理基本上使用的是go,channel,但是使用sync模块更便于并发的实现。

1、使用sync.Once.Do(func())可以实现全局只能调用一次函数

2、sync.WaitGroup是一个计数器方式的阻塞器,通过wait(0函数阻塞,通过Add()和Done()增加和较少计数。注意,WaitGroup.Done()不可以让WaitGroup减少到变成负数,不然会报错。

3、使用sync.mutex.Lock()和sync.mutex.Unlock()函数可以锁定和解锁函数所在的协程,其实就是阻塞和解除阻塞协程。同时注意,使用Lock()函数可以批量锁定协程,然后使用Unlock()同时解锁这些协程,从而实现同时运行的协程。最后需要特别注意,使用sync.mutex.Lock()函数之后必须配合defer sync.mutex.Unlock()函数解除锁定。注意,如果Unlock之前没有Lock,那么会引发错误,而且Lock并不会与特定goroutine关联,所以可以对特定goroutine锁定,使用另外的goroutine解锁。一个goroutine被Lock之后,在Unlock之前不能再次Lock,不然会死锁。

        Lock()可以重复调用,但是Unlock之前如果没有Lock()那么是会报错的,这点注意!


        应该说,Mutex是互斥锁,或者说全局锁,而RWMutex是读写锁。Mutex锁定之后当前goroutine内可以运行,而其他的goroutine则不能调用当前goroutine占据的函数等资源,直到解锁。RWMutex适用于读多写少的场景。简单点说,Mutex相当于java的synchronized关键字,而RWMutex相当于java的ReadWriteLock。

        需要注意的一点是,使用Mutex.Lock()排队加锁的函数资源,会像java的static对象一样,排队的函数资源操作过的全局变量是static的。Mutex.Lock()其实相当于等待获取这个static的函数资源返回。同时需要注意的是,这个static的函数资源会在所有的Unlock()完成后被销毁掉,所以如果是Lock()一个函数和全局变量,那么这个全局变量其实操作的是一个复制体的全局变量,所以在所有的Unlock()之后,在Lock()和Unlock()代码之外会发现全局变量其实没有被修改,哪怕这个全局变量是指针型的。


4、sync.mutex.RWMutex.Lock()之前如果已经有读锁和写锁,那么就会阻塞直到该锁可用,这种情况其实从读写锁这个名字就可以知道为什么不会像Mutex那样已经锁定,再锁定就报错了。

5、sync.Cond类是一个条件并发类,使用一个实现了Lock类(一般为Mutex,RWMutex)的对象作为初始化参数,然后这个对象其实就是后面的Cond.L这个类变量了,使用这个变量实现锁定和解锁,然后使用Wait()函数进行goroutine阻塞,使用Signal()函数解除阻塞一个goroutine,使用Broadcast()解除阻塞所有的goroutine。最后注意,Signal()是按照Wait()的顺序进行解除阻塞的。总的来说,从功能上来说,就是按照顺序阻塞和解除阻塞goroutine。

        但是对于Cond的使用需要注意,需要先使用Cond.L.Lock()然后才能使用Cond.L.Wait(),最后才是Cond.L.Unlock(),这一点可以从Wait(0的源代码看出来。所以,其实Cond的设计就是为了多goroutine竞争资源的时候,不会因为一个goroutine运行完了之后将资源释放了。总的来说就是,goroutine锁住函数等资源,然后进行操作和wait,然后解锁,期间其他goroutine也可以锁住函数等资源,这样就可以顺序竞争函数资源了。

        这里有一点需要特别注意,Cond.Wait()函数可以重复调用,并且不会出错和重复阻塞,所以可以在条件不成立的情况下使用for循环调用Cond.Wait()函数。应该说这里Cond.Wait()就是需要lock和unlock的阻塞。

        Cond适合的场景是顺序操作函数资源,或者同时进行操作函数资源的场景。



6、Pool类是一个用于缓冲的类,只有三个重要的地方:Get()函数,Put()函数,和New函数变量。放入Pool的类会被系统回收。这一点Pool和java的Phantom类一样。


        Cond相当于Lock锁和阻塞的组合,而Pool则是垃圾回收和缓冲的组合。


7、sync的atomic模块是原子操作。原子操作分为:增减,比较,交换,载入,存储,交换,但是总的来说其实就是实现值替换操作。特别注意,其实原子操作就是最简单的锁,可以不形成临界值和创建互斥锁的情况下实现并发安全的值替换操作。这可以大大减少并发对于性能的损耗。但是原子操作如果太频繁,会不那么容易成功,这是因为原子操作是连续进行,不会交出时间片的。最后,直接查看atomic模块就可以知道原子操作分为哪些了。

        总的来说,原子操作就是不用我们关心锁的实现值替换的操作。








0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:100572次
    • 积分:1893
    • 等级:
    • 排名:千里之外
    • 原创:105篇
    • 转载:0篇
    • 译文:0篇
    • 评论:18条
    博客专栏
    最新评论