https://studygolang.com/pkgdoc
https://tour.golang.org/welcome/1
go语言高级编程:https://books.studygolang.com/advanced-go-programming-book/
hello world
1 | package main |
Go语言开发环境搭建
安装
参考 https://golang.google.cn/dl/
正常安装就好,然后在cmd下输入go version试试,能不能得到结果。
windows下GOPATH配置
GOPATH是一个环境变量,用来表明你写的go项目的存放路径,gopath最好只设置一个,所有的项目代码都放置到GOPATH的src目录下。
- 在D盘下新建一个目录比如,
D:\documents\go,这个路径作为系统环境变量GOPATH的值。可能用户环境变量也有个GOPATH,是C:\Users\zhou2,把这个删了。 - 在
D:\documents\go下新建三个文件夹:src,pkg,bin。 - 将
D:\documents\go\bin加入到系统环境变量PATH中。 - 在cmd中输入
go env查看GOPATH是否正确。
go get 包可以使用了, 最后包会安装到 D:\documents\go目录的pkg和src文件夹里, pkg里是*.a格式的, 暂且理解为库吧, src目录里是*.go格式的源码。
项目结构
适合个人开发者

流行的项目结构

适合企业开发者

VS code
使用vs code来编辑go代码,首先在vs code里面安装goz扩展插件。在侧边栏的extensions选项中搜索go,然后安装就好了。
使用vs code打开D:\documents\go\src文件夹,在里面新建go文件写代码。
这时,问题来了:由于网络的原因,vs code内部不自己下载很多插件,所以必须我们自己上VPN从github上下载,然后在vs code中安装:
- 进入项目地址
D:\documents\go\src\github.com\zhouhuahui\helloWorld - 然后打开git,执行
git clone https://github.com/golang/tools.git tools - 返回到vs code中的项目
- 使用快捷键
ctrl+shift+p,打开了vs code的命令行,执行Go:Install/Update Tools,然后在下面出现的很多选项中全部选中,点击OK,然后等待vs code安装所有的插件,并全部显示succeed。
在项目内写代码时,比如helloWorld项目,首先在helloWorld内部,打开cmd(我想在vs code内的terminal内也可以,注意terminal定位到helloWorld目录下),执行go mod init helloWorld。之后才能运行go build和helloWorld.exe执行代码。还有,terminal要是cmd形式的,而不是powershell。

go install错误
1 | A connection attempt failed because the connected party did not prope |
其实这个错误不是网络原因,可以使用下述命令使用go模块代理,这样go install或者go get就不会报错了。参考:https://goproxy.cn/
1 | go env -w GO111MODULE=on |
然后创建$GOPATH/src/golang.org/x文件夹,cd到此文件夹,执行
1 | git clone https://github.com/golang/tools.git |
然后重启VSCode就好了。
参考:
https://www.jianshu.com/p/4f2c476e189a
https://blog.csdn.net/qq_36546907/article/details/83958134
最后安装的包都在$GOPATH/pkg/mod里面:

go语言基础
变量和常量
go语言中值得注意的关键字。
1 | func func interface select |
标识符
用来表示变量名,函数名,类型名,方法名。在go语言中,标识符有些特殊,如果标识符的首字母是大写的,那么就表示对外部可见,否则对外部不可见。
变量
值得注意的保留字。
1 | Constants: true false iota nil |
变量的声明:
1 | var s1 string |
1 | // 批量声明 |
常量
如何声明常量
1 | const pi = 3.14 // 不需要指明类型 |
类型
基础类型:bool, string和其他数字类型
聚合类型:数组和结构体类型
引用类型:通道chan, 函数func, 指针, 切片slice, map
变量声明
在全局声明的变量是可以不使用的,但是在函数中声明的变量是要必须使用的,否则会编译不过去。
声明变量同时赋值
1 | var s1 string = "zhou" |
类型推导(根据值来判断变量是什么类型)
1 | var s2 = "20" |
在函数内部,可以使用更简短的:=方式声明并初始化变量
1 | func main(){ |
匿名变量
1 | func foo() (int, string){ |
字符串
go语言字符串内部支持UTF-8编码。
字符串用双引号包裹的,字符是用单引号包裹的,和C++语言类似。字符串也可以通过`符号来包裹多行的语句,类似于js语言。
1 | import ( |
1 | import ( |
go语言没有自动类型转换,只能进行强制类型转换
数组
数组的初始化
1 | func main(){ |
数组的遍历
1 | func main(){ |
二维数组:
1 | func main(){ |
一个数组可以被其他数组初始化,但是不能被其他数组赋值
1 | func main(){ |
切片Slice
切片Slice是一个拥有相同类型元素的可变长度的序列,它是基于数组类型做的一层封装,非常灵活,支持自动扩容。切片是一个引用类型,它的内部结构包含地址,长度和容量。切片一般用于快速地操作一块数据集合。
切片的定义
1 | func main(){ |
通过内置的len()和cap()函数可以得到切片的长度和容量。
由数组得到切片
1 | func main(){ |
由make()函数创建一个切片。前面的切片创建方式都不是动态的,没法指定切片的长度和容量。
1 | func main(){ |
如果make函数只有两个参数,那么第二个参数表示切片的长度。
要判断一个切片是否是空的,要用len()函数来判断。
切片只是保存了一个数组的元数据,因此切片之间的拷贝,仅仅复制了元数据,而没有复制数组本身。
append()
调用append()函数来向切片中新增元素
1 | func main(){ |
copy()
copy()函数实现将一个切片的数据复制到另一个切片空间中
1 | func main(){ |
判断切片是否相等
使用bytes包
1 | a := []byte("ab") |
map
例子
1 | package bayes |
1 | package bayes |
循环
for循环
Go 语言可以使用 for range 遍历数组、切片、字符串、map 及通道(channel)。通过 for range 遍历的返回值有一定的规律:
- 数组、切片、字符串返回索引和值。
- map 返回键和值。
- 通道(channel)只返回通道内的值。
遍历数组,切片获得索引和元素
1 | for key, value := range []int{1, 2, 3}{ |
遍历map, 获得map的键和值
1 | m := map[string]int{ |
函数
1 | // ret是返回值 |
defer
在golang当中,defer代码块会在函数调用链表中增加一个函数调用。这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之时添加一个函数调用。因此,defer通常用来释放函数内部变量。
多个defer语句按照先进后出的顺序延迟执行。
go语言的return语句不是原子操作,这个和defer有关。
1 | func f1() int{ |
结构体
结构体定义
1 | type person struct{ |
方法
1 | type dog struct{ |
值接收者和指针接收者
指针接收者可以改变值。
1 | type dog struct{ |
import
1 | import( |
使用规范
- 不要使用相对路径引入包(以GOPATH/src作为根目录)
- 引入包顺序遵循 标准库,项目包,第三方包
- 第三方包按命名顺序
比如:
1 | import( |
第三方包应该会放在go/bin/pkg/里面。
接口
接口是一种类型;
1 | type speaker interface{ |
接口的实现
1 | type 接口名 interface{ |
如果一个变量中实现了接口规定的所有方法,这个变量就实现了这个接口。
值接收者和指针接收者实现接口:
1 | type speaker interface{ |
1 | type speaker interface{ |
空接口
1 | interface{} // 空接口 |
如果一个函数的参数可以是任何类型,那么就设置为空接口类型。
如果我们想要map的键是string类型,但是值是不一定的类型,就可以用空接口了。
1 | func main(){ |
通过断言判断空接口的动态类型
1 | func assign(a interface{}){ |
如果我们知道了一个空接口类型变量的动态类型是int,则可以使用类型断言,来转换为对应的类型。
1 | func main(){ |
文件操作
创造一个文件:
1 | import "os" |
打开文件:
1 | import "os" |
读取文件:
1 | import "io/ioutil" |
strconv包
如果使用string方法来给一个int32类型的变量转换为字符串,那么就会产生这个int类型底层二进制表示在UTF-8编码中对应的字符。
1 | func main(){ |
1 | ऌ |
使用fmt.Sprintf()函数可以将数字转换为字符串
1 | func main(){ |
1 | 2316 |
使用strconv包进行类型转换
1 | import ( |
time包
1 | import ( |
reflect包
it’s all relative
I started learning golang a couple of days ago and found reflect.Valueof() and Value.Elem() quite confusing. What is the difference between this two function/methods and how to use them correctly?
Both function/methods return a Value, and according to the go doc
ValueOf returns a new Value initialized to the concrete value stored in the interface i. ValueOf(nil) returns the zero Value.Elem returns the value that the interface v contains or that the pointer v points to. It panics if v’s Kind is not Interface or Ptr. It returns the zero Value if v is nil.
interface 和 反射
在讲反射之前,先来看看Golang关于类型设计的一些原则
- 变量包括(type, value)两部分
- 理解这一点就知道为什么nil != nil了
- type 包括 static type和concrete type. 简单来说 static type是你在编码是看见的类型(如int、string),concrete type是runtime系统看见的类型
- 类型断言能否成功,取决于变量的concrete type,而不是static type. 因此,一个 reader变量如果它的concrete type也实现了write方法的话,它也可以被类型断言为writer.
接下来要讲的反射,就是建立在类型之上的,Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static type),在创建变量的时候就已经确定,反射主要与Golang的interface类型相关(它的type是concrete type),只有interface类型才有反射一说。
在Golang的实现中,每个interface变量都有一个对应pair,pair中记录了实际变量的值和类型:
1 | (value, type) |
value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型【对应concrete type】,另外一个指针指向实际的值【对应value】。
例如,创建类型为*os.File的变量,然后将其赋给一个接口变量r:
1 | tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) |
接口变量r的pair中将记录如下信息:(tty, *os.File),这个pair在接口变量的连续赋值过程中是不变的,将接口变量r赋给另一个接口变量w:
1 | var w io.Writer |
接口变量w的pair与r的pair相同,都是:(tty, *os.File),即使w是空接口类型,pair也是不变的。
interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。
Golang的反射reflect
reflect的基本功能TypeOf和ValueOf
既然反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。那么在Golang的reflect反射包中有什么样的方式可以让我们直接获取到变量内部的信息呢? 它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf() 和 reflect.TypeOf(),看看官方的解释
1 | // ValueOf returns a new Value initialized to the concrete value |
reflect.TypeOf()是获取pair中的type,reflect.ValueOf()获取pair中的value,示例如下:
1 | package main |
- reflect.TypeOf: 直接给到了我们想要的type类型,如float64、int、各种pointer、struct 等等真实的类型
- reflect.ValueOf:直接给到了我们想要的具体的值,如1.2345这个具体数值,或者类似&{1 “Allen.Wu” 25} 这样的结构体struct的值
- 也就是说明反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Type和reflect.Value这两种
从relfect.Value中获取接口interface的信息
当执行reflect.ValueOf(interface)之后,就得到了一个类型为”relfect.Value”变量,可以通过它本身的Interface()方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。不过,我们可能是已知原有类型,也有可能是未知原有类型,因此,下面分两种情况进行说明。
已知原有类型【进行“强制转换”】
已知类型后转换为其对应的类型的做法如下,直接通过Interface方法然后强制转换,如下:
1 | realValue := value.Interface().(已知的类型) |
示例如下:
1 | package main |
- 转换的时候,如果转换的类型不完全符合,则直接panic,类型要求非常严格!
- 转换的时候,要区分是指针还是指
- 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
未知原有类型【遍历探测其Filed】
很多情况下,我们可能并不知道其具体类型,那么这个时候,该如何做呢?需要我们进行遍历探测其Filed来得知,示例如下:
1 | package main |
通过运行结果可以得知获取未知类型的interface的具体变量及其类型的步骤为:
- 先获取interface的reflect.Type,然后通过NumField进行遍历
- 再通过reflect.Type的Field获取其Field
- 最后通过Field的Interface()得到对应的value
通过运行结果可以得知获取未知类型的interface的所属方法(函数)的步骤为:
- 先获取interface的reflect.Type,然后通过NumMethod进行遍历
- 再分别通过reflect.Type的Method获取对应的真实的方法(函数)
- 最后对结果取其Name和Type得知具体的方法名
- 也就是说反射可以将“反射类型对象”再重新转换为“接口类型变量”
- struct 或者 struct 的嵌套都是一样的判断处理方式
通过reflect.Value设置实际变量的值
reflect.Value是通过reflect.ValueOf(X)获得的,只有当X是指针的时候,才可以通过reflec.Value修改实际变量X的值,即:要修改反射类型的对象就一定要保证其值是“addressable”的。
示例如下:
1 | package main |
- 需要传入的参数是 float64这个指针,然后可以通过pointer.Elem()去获取所指向的Value,*注意一定要是指针。
- 如果传入的参数不是指针,而是变量,那么
- 通过Elem获取原始值对应的对象则直接panic
- 通过CanSet方法查询是否可以设置返回false
- newValue.CantSet()表示是否可以重新设置其值,如果输出的是true则可修改,否则不能修改,修改完之后再进行打印发现真的已经修改了。
- reflect.Value.Elem() 表示获取原始值对应的反射对象,只有原始对象才能修改,当前反射对象是不能修改的
- 也就是说如果要修改反射类型对象,其值必须是“addressable”【对应的要传入的是指针,同时要通过Elem方法获取原始值对应的反射对象】
- struct 或者 struct 的嵌套都是一样的判断处理方式
通过reflect.ValueOf来进行方法的调用
这算是一个高级用法了,前面我们只说到对类型、变量的几种反射的用法,包括如何获取其值、其类型、如果重新设置新值。但是在工程应用中,另外一个常用并且属于高级的用法,就是通过reflect来进行方法【函数】的调用。比如我们要做框架工程的时候,需要可以随意扩展方法,或者说用户可以自定义方法,那么我们通过什么手段来扩展让用户能够自定义呢?关键点在于用户的自定义方法是未可知的,因此我们可以通过reflect来搞定
示例如下:
1 | package main |
- 要通过反射来调用起对应的方法,必须要先通过reflect.ValueOf(interface)来获取到reflect.Value,得到“反射类型对象”后才能做下一步处理
- reflect.Value.MethodByName这.MethodByName,需要指定准确真实的方法名字,如果错误将直接panic,MethodByName返回一个函数值对应的reflect.Value方法的名字。
- []reflect.Value,这个是最终需要调用的方法的参数,可以没有或者一个或者多个,根据实际参数来定。
- reflect.Value的 Call 这个方法,这个方法将最终调用真实的方法,参数务必保持一致,如果reflect.Value’Kind不是一个方法,那么将直接panic。
- 本来可以用u.ReflectCallFuncXXX直接调用的,但是如果要通过反射,那么首先要将方法注册,也就是MethodByName,然后通过反射调用methodValue.Call
Golang的反射reflect性能
Golang的反射很慢,这个和它的API设计有关。在 java 里面,我们一般使用反射都是这样来弄的。
1 | Field field = clazz.getField("hello"); |
这个取得的反射对象类型是 java.lang.reflect.Field。它是可以复用的。只要传入不同的obj,就可以取得这个obj上对应的 field。
但是Golang的反射不是这样设计的:
1 | type_ := reflect.TypeOf(obj) |
这里取出来的 field 对象是 reflect.StructField 类型,但是它没有办法用来取得对应对象上的值。如果要取值,得用另外一套对object,而不是type的反射
1 | type_ := reflect.ValueOf(obj) |
这里取出来的 fieldValue 类型是 reflect.Value,它是一个具体的值,而不是一个可复用的反射对象了,每次反射都需要malloc这个reflect.Value结构体,并且还涉及到GC。
Golang reflect慢主要有两个原因
- 涉及到内存分配以及后续的GC;
- reflect实现里面有大量的枚举,也就是for循环,比如类型之类的。
总结
上述详细说明了Golang的反射reflect的各种功能和用法,都附带有相应的示例,相信能够在工程应用中进行相应实践,总结一下就是:
- 反射可以大大提高程序的灵活性,使得interface{}有更大的发挥余地
- 反射必须结合interface才玩得转
- 变量的type要是concrete type的(也就是interface变量)才有反射一说
- 反射可以将“接口类型变量”转换为“反射类型对象”
- 反射使用 TypeOf 和 ValueOf 函数从接口中获取目标对象信息
- 反射可以将“反射类型对象”转换为“接口类型变量
- reflect.value.Interface().(已知的类型)
- 遍历reflect.Type的Field获取其Field
- 反射可以修改反射类型对象,但是其值必须是“addressable”
- 想要利用反射修改对象状态,前提是 interface.data 是 settable,即 pointer-interface
- 通过反射可以“动态”调用方法
- 因为Golang本身不支持模板,因此在以往需要使用模板的场景下往往就需要使用反射(reflect)来实现
参考链接
- The Go Blog : 其实看官方说明就足以了!
- 官方reflect-Kind
- Go语言的反射三定律
- Go基础学习五之接口interface、反射reflection
- 提高 golang 的反射性能
encoding/base64包
导入方式:
1 | import "encoding/base64" |
base64实现了RFC 4648规定的base64编码。Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符(即6Bits)来表示二进制数据(即8Bits)的方法。Base64编码是从二进制到字符的过程,可用于在HTTP环境下传递较长的标识信息,这样更适合放在URL中进行传递。此时,采用Base64编码具有不可读性,需要解码后才能阅读。
该编码的规则就是:
- 把3个字符变成4个字符:方法就是先将3个8Bits(38=24Bits)的值分成4个6Bits的值(46=24Bits),然后在这四个6Bits值前添加两个0将其变为8Bits的值,这样就能够保证生成的4个值表示的值大小为(0~63),即可以将其转成对应的ASCII码(64个可打印字符)举例:
1 | 转换前: 01110011 00110001 00110011 |
- 每76个字符加一个换行符
- 最后的结束符也要处理
然而,标准的Base64并不适合直接放在URL里传输,因为URL编码器会把标准Base64中的“/”和“+”字符变为形如“%XX”的形式,而这些“%”号在存入数据库时还需要再进行转换,因为ANSI SQL中已将“%”号用作通配符。
为解决此问题,可采用一种用于URL的改进Base64编码,它不仅在末尾去掉填充的’=’号,并将标准Base64中的“+”和“/”分别改成了“-”和“_”,这样就免去了在URL编解码和数据库存储时所要作的转换,避免了编码信息长度在此过程中的增加,并统一了数据库、表单等处对象标识符的格式。
type Encoding
1 | type Encoding struct { |
双向的编码/解码协议,根据一个64字符的字符集定义,RFC 4648标准化了两种字符集。默认字符集用于MIME(RFC 2045)和PEM(RFC 1421)编码;另一种用于URL和文件名,用’-‘和’_’替换了’+’和’/‘。
下面的两个函数分别就是编码和解码的过程:
func (*Encoding) EncodeToString
1 | func (enc *Encoding) EncodeToString(src []byte) string |
返回将src编码后的字符串。
func (*Encoding) DecodeString
1 | func (enc *Encoding) DecodeString(s string) ([]byte, error) |
返回base64编码的字符串s代表的数据。
举例:
1 | package main |
下面的方法不是将他们编码成字符串:
解码字符:
func (*Encoding) DecodedLen
1 | func (enc *Encoding) DecodedLen(n int) int |
返回n字节base64编码的数据解码后的最大长度。
func (*Encoding) Decode
1 | func (enc *Encoding) Decode(dst, src []byte) (n int, err error) |
将src的数据解码后存入dst,最多写DecodedLen(len(src))字节数据到dst,并返回写入的字节数。 如果src包含非法字符,将返回成功写入的字符数和CorruptInputError。换行符(\r、\n)会被忽略。
举例:
1 | package main |
下面两个代码组合进行编码:
func (*Encoding) EncodedLen
1 | func (enc *Encoding) EncodedLen(n int) int |
返回n字节数据进行base64编码后的最大长度。
func (*Encoding) Encode
1 | func (enc *Encoding) Encode(dst, src []byte) |
将src的数据编码后存入dst,最多写EncodedLen(len(src))字节数据到dst,并返回写入的字节数。
函数会把输出设置为4的倍数,因此不建议对大数据流的独立数据块执行此方法,使用NewEncoder()代替。
举例:
1 | package main |
下面就是生成编码器和解码器的方法,其实和上面的Encode和Decode实现的效果一样,在编码的是大数据流时使用下面的方法:
func NewEncoder(可写Write)
1 | func NewEncoder(enc *Encoding, w io.Writer) io.WriteCloser |
创建一个新的base64流编码器。写入的数据会在编码后再写入w,base32编码每3字节执行一次编码操作;写入完毕后,使用者必须调用Close方法以便将未写入的数据从缓存中刷新到w中。
举例:
1 | package main |
func NewDecoder(可读-Read)
1 | func NewDecoder(enc *Encoding, r io.Reader) io.Reader |
创建一个新的base64流解码器。
举例:
1 | package main |
bytes包
go test的使用
go test真的太难理解了,我一开始也不要知道Test开头的函数里的fmt输出语句是没有显示在屏幕上的,导致我看不到自己的输出语句,也就没法测试。
1 | func TestF1(t *testing.T){ |
这篇文章写的不错:【1】
1 | // main.go |
1 | // main_test.go |
以上两个文件是在同一个文件夹的,然后运行下面的代码:
1 | go test -run TestF1 |
输出是:
1 | PASS |
如果将main_test.go中的第8行改了,会输出
1 | --- FAIL: TestF1 (0.00s) |
这里有一些go test的使用知识:
- go的test一般以xxx_test.go为文件名,xxx并没有特别要求是要实测的文件名
- tes文件里要测试的函数要大写Testxxx(t testing.T)或者Testxxx(b testing.B) \
- t.Errorf(“”)为打印错误信息,并且当前test case会被跳过
- t.SkipNow()跳过当前test,并且直接按PASS处理继续下一个test
- 使用t.Run来执行subtests可以做到控制test输出以及test的顺序
main_test.go第14行可以改为
1 | t.Run("testF1",testF1) |
并发编程
go语言通过goroutine实现并发。goroutine属于用户态线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成。
go语言还提供channel在多个goroutine间进行通信,goroutine和channel是go语言秉承的CSP(Communicating Sequential Process)并发模式的重要实现基础。
goroutine
只需要在调用函数时在前面加上go关键字就可以创建一个goroutine。
1 | func hello(){ |
如何让hello打印出来?
1 | func hello(i int){ |
使用匿名函数的形式来创建goroutine
1 | // main函数启动后会自动创建一个main goroutine |
上面这个函数会有问题,最后会输出很多个100。原因是当for循环结束时,i的结果是100,而这100个goroutine都用的是这一个i。
改成下面的代码就好了。
1 | // main函数启动后会自动创建一个main goroutine |
sync.WaitGroup
使用sync.WaitGroup来实现goroutine的同步,不用使用time.Sleep来傻等了。
1 | var wg sync.WaitGroup |
开启一个线程,来输出随机数。
1 | import ( |
channel
虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞争问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法会造成性能问题。
go语言提倡使用通信共享内存。
go语言的channel是一种特殊的数据类型,通道像一个传送带或者队列,总是遵循先进先出的规则,保证收发数据的顺序,每一个通道是一个具体类型的管道,也就是声明channel的时候需要为其指定元素类型。
1 | var 变量 chan 元素类型 |
例如:
1 | var ch1 chan int // 传递int类型的通道 |
通道必须使用make来初始化。
1 | var b chan int |
发送:
1 | ch <- 10 // 把10发送到ch中 |
接收:
1 | <- ch |
关闭:
1 | close(ch) |
select
go语言可以尝试随机地从多个通道中取出数据或发送数据,是用select来实现的。
1 | select{ |
举个例子:
1 | func main() { |
输出是:
1 | 0 |
sync.Cond
Cond实现了一个条件变量,一个线程集合地,供线程等待或者宣布某事件的发生。比如,调用了Cond.Wait()函数的线程就会进入线程集合,而其他线程判断满足某个事件了,就会调用Cond.Broadcast()或者Cond.Signal()进行唤醒线程集合中的被阻塞的线程的操作。
Cond
1 | type Cond struct { |
每个Cond实例都有一个相关的锁(一般是Mutex或RWMutex类型的值),它必须在改变条件时或者调用Wait方法前保持锁定。
func (*Cond)Broadcast
Broadcast唤醒所有等待c的线程。调用者在调用本方法时,建议(但并非必须)保持c.L的锁定。
func (*Cond)Signal
Signal唤醒等待c的一个线程(如果存在)。调用者在调用本方法时,建议(但并非必须)保持c.L的锁定。
func (*Cond)Wait
Wait自行解锁c.L并阻塞当前线程,在之后线程恢复执行时,Wait方法会在返回前锁定c.L。和其他系统不同,Wait除非被Broadcast或者Signal唤醒,不会主动返回。
注意:Wait方法会在返回前锁定c.L,因此执行Signal或者Broadcast的线程应该不要忘了在合适的时机执行Cond.L.Unlock()。
go语言使用rpc
用通俗易懂的语言描述就是:RPC允许跨机器、跨语言调用计算机程序方法。打个比方,我用go语言写了个获取用户信息的方法getUserInfo,并把go程序部署在阿里云服务器上面,现在我有一个部署在腾讯云上面的php项目,需要调用golang的getUserInfo方法获取用户信息,php跨机器调用go方法的过程就是RPC调用。
在golang中实现RPC非常简单,有封装好的官方库和一些第三方库提供支持。Go RPC可以利用tcp或http来传递数据,可以对要传递的数据使用多种类型的编解码方式。golang官方的net/rpc库使用encoding/gob进行编解码,支持tcp或http数据传输方式,由于其他语言不支持gob编解码方式,所以使用net/rpc库实现的RPC方法没办法进行跨语言调用。
golang官方还提供了net/rpc/jsonrpc库实现RPC方法,JSON RPC采用JSON进行数据编解码,因而支持跨语言调用。但目前的jsonrpc库是基于tcp协议实现的,暂时不支持使用http进行数据传输。
除了golang官方提供的rpc库,还有许多第三方库为在golang中实现RPC提供支持,大部分第三方rpc库的实现都是使用protobuf进行数据编解码,根据protobuf声明文件自动生成rpc方法定义与服务注册代码,在golang中可以很方便的进行rpc服务调用。
net/rpc库实现rpc的例子
1 | //master.go |
1 | //worker.go |
1 | //rpc.go |
1 | //master_main.go |
1 | //worker_main.go |
上面的每份代码对应一个文件,属于同一个package的文件放在对应名字的文件夹中。然后转到main文件夹中,先运行go run master_main.go 即可打开服务器进程,然后运行go run worker_main.go 运行客户端程序,即可得到结果100。
上面是个简单的利用go语言的net/rpc包来实现rpc的例子。
package
如果一个package内有很多文件,一个文件利用到了另一个文件定义的变量或函数,则在go run时要把这个包内的所有文件都加到后面,顺序不重要,这时,所有文件相当于组合成一个go文件了。
引用
go语言标准网:https://studygolang.com/pkgdoc
一个go语言教程:https://docs.hacknode.org/gopl-zh/ch12/ch12-01.html





