8小时速成Golang+IM实战
引言
学习前置要求
- 具备1种后端编程语言开发经验(C/C++/Java/Python/PHP等)
- 具备基本的网络编程能力和并发思想
- 了解计算机基本体系结构
- 了解Linux基础知识
学习提纲
Golang开发环境
Go官网下载地址:https://golang.org/dl/
Go官方镜像站(推荐):https://golang.google.cn/dl/
根据自己系统,自行选择安装。
记得配置GOPROXY~
Go1.14版本之后,都推荐使用go mod
模式来管理依赖了,也不再强制我们把代码必须写在GOPATH
下面的src
目录了,你可以在你电脑的任意位置编写go代码。
默认GoPROXY配置是:GOPROXY=https://proxy.golang.org,direct
,由于国内访问不到 https://proxy.golang.org
所以我们需要换一个PROXY
,这里推荐使用https://goproxy.io
或 https://goproxy.cn
。
可以执行下面的命令修改GOPROXY
:
1 | go env -w GOPROXY=https://goproxy.cn,direct |
Golang语言特性
Golang的优势
- 极简单的部署方式
- 可直接编译成机器码
- 不依赖其他库
- 直接运行即可部署
- 静态类型语言:编译的时候检查出来隐藏的大多数问题
- 语言层面的并发
- 天生的基因支持
- 充分的利用多核
- 强大的标准库
- Runtime系统调度机制
- 高效的GC垃圾回收
- 丰富的标准库
- 简单易学
- 25个关键字
- C语言简洁基因,内嵌C语法支持
- 面向对象特征(继承、多态、封装)
- 跨平台
- “大厂”领军
Golang适合做什么
- 云计算基础设施领域
代表项目:docker、kubernetes、etcd、consul、cloudflare CDN、七牛云存储等。
- 基础后端软件
代表项目:tidb、influxdb、cockroachdb等。
- 微服务
代表项目:go-kit、micro、monzo bank的typhon、bilibili等。
- 互联网基础设施
代表项目:以太坊、hyperledger等。
Golang的不足
包管理,大部分包都在 github 上
所有 Excepiton 都用 Error 来处理(比较有争议)
对 C 的降级处理,并非无缝,没有 C 降级到 asm 那么完美(序列化问题)
Golang语法新奇
从main函数初见golang语法
1 | package main |
- 第一行代码 package main 定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
- 下一行 import “fmt” 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。
- 下一行 func main() 是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。
变量的声明
1 | package main |
常量
常量是一个简单值的标识符,在程序运行时,不会被修改的量。
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
1 | package main |
函数
函数返回多个值
1 | package main |
init函数与import
- init函数
init 函数可在package main中,可在其他package中,可在同一个package中出现多次。
- main函数
main 函数只能在package main中
- 执行过程
golang里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。
虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数。
go程序会自动调用init()
和main()
,所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。
程序的初始化和执行都起始于main包。
如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。
当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。
等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。
下图详细地解释了整个执行过程:
1 | package lib1 |
1 | package lib2 |
1 | package main |
如果导入了一个包,但没使用,会报错,所以可以声明匿名包,匿名包不能调用方法,但是会执行init()
方法
针对每个包也可以起别名。
1 | package main |
函数参数
函数如果使用参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。
调用函数,可以通过两种方式来传递参数:
值传递
值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
引用传递(指针传递)
Go 语言中指针是很容易学习的,Go 语言中使用指针可以更简单的执行一些任务。
我们都知道,变量是一种使用方便的占位符,用于引用计算机内存地址。Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。
引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。引用传递指针参数传递到函数内,以下是交换函数 swap() 使用了引用传递:
1 | package main |
defer
defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数。
defer作用:
- 释放占用的资源
- 捕捉处理异常
- 输出日志
如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行。(压栈)
defer和return,return先执行,defer后执行。
recover错误拦截
运行时panic异常一旦被引发就会导致程序崩溃。
Go语言提供了专用于“拦截”运行时panic的内建函数“recover”。它可以是当前的程序从运行时panic的状态中恢复并重新获得流程控制权。
注意:recover只有在defer调用的函数中有效。
1 | package main |
slice和map
Go 语言切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片(“动态数组“),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
定义切片
你可以声明一个未指定大小的数组来定义切片:
1 | myArray := []int |
切片不需要说明长度。或使用make()函数来创建切片:
1 | var slice1 []int = make([]int, len) |
也可以指定容量,其中capacity为可选参数。
1 | make([]T, length, capacity) |
这里 length 是数组的长度并且也是切片的初始长度。
切片初始化
1 | slice1 := []int{1,2,3} //声明并初始化,默认值为1,2,3,长度len是3 |
len()
和cap()
函数
切片是可索引的,并且可以由 len()
方法获取长度。
切片提供了计算容量的方法 cap()
可以测量切片最长可以达到多少。
切片的追加与截取
切片的扩容机制:append的时候,如果长度增加后超过最大容量,则会自动将容量增加2倍
切片的截取:
1 | s := []int{1,2,3,4} |
map
map和slice类似,只不过是数据结构不同,下面是map的一些声明方式。
1 | // 第一种声明方式 |
struct
1 | type Book struct { |
面向对象特征
类的封装与表示
如果类名首字母大写,表示其他包也能够访问
如果说类的属性首字母大写, 表示该属性是对外能够访问的,否则的话只能够类的内部访问
1 | package main |
1 | Name = xiaoxin |
方法的接收者是值类型,只能读,不能改~
1 | func (this *Hero) GetName() { |
类的继承
1 | package main |
类的多态
示例
1 | package main |
基本要素
- 有一个父类接口
- 有子类,实现了全部接口方法
- 父类类型的变量(指针)指向(引用)子类的具体数据变量
interface与类型断言
Golang的语言中提供了断言的功能。Golang中的所有程序都实现了interface{}的接口,这意味着,所有的类型如string,int,int64甚至是自定义的struct类型都就此拥有了interface{}的接口,这种做法和java中的Object类型比较类似。那么在一个数据通过func funcName(interface{})的方式传进来的时候,也就意味着这个参数被自动的转为interface{}的类型。
1 | package main |
反射reflect
编程语言中反射的概念
在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。
每种语言的反射模型都不同,并且有些语言根本不支持反射。Golang语言实现了反射,反射机制就是在运行时动态的调用对象的方法和属性,官方自带的reflect包就是反射相关的,只要包含这个包就可以使用。
多插一句,Golang的gRPC也是通过反射实现的。
interface和反射
学习反射之前,先来看看Golang关于类型设计的一些原则:
- 变量包括(type, value)两部分
- 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】。
interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。
reflect.TypeOf()
reflect.ValueOf()
1 | package main |
结构体标签
基本使用
1 | package main |
在json中的应用
1 | package main |
1 | jsonStr = {"title":"喜剧之王","year":2000,"rmb":10,"actors":["星爷","张柏芝"]} |
Golang高阶
goroutine
协程并发
协程:coroutine。也叫轻量级线程。
与传统的系统级线程和进程相比,协程最大的优势在于“轻量级”。可以轻松创建上万个而不会导致系统资源衰竭。而线程和进程通常很难超过1万个。这也是协程别称“轻量级线程”的原因。
一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。
多数语言在语法层面并不直接支持协程,而是通过库的方式支持,但用库的方式支持的功能也并不完整,比如仅仅提供协程的创建、销毁与切换等能力。如果在这样的轻量级线程中调用一个同步 IO 操作,比如网络通信、本地文件读写,都会阻塞其他的并发执行轻量级线程,从而无法真正达到轻量级线程本身期望达到的目标。
在协程中,调用一个任务就像调用一个函数一样,消耗的系统资源最少!但能达到进程、线程并发相同的效果。
在一次并发任务中,进程、线程、协程均可以实现。从系统资源消耗的角度出发来看,进程相当多,线程次之,协程最少。
Go并发
Go 在语言级别支持协程,叫goroutine。Go 语言标准库提供的所有系统调用操作(包括所有同步IO操作),都会出让CPU给其他goroutine。这让轻量级线程的切换管理不依赖于系统的线程和进程,也不需要依赖于CPU的核心数量。
有人把Go比作21世纪的C语言。第一是因为Go语言设计简单,第二,21世纪最重要的就是并行程序设计,而Go从语言层面就支持并发。同时,并发程序的内存管理有时候是非常复杂的,而Go语言提供了自动垃圾回收机制。
Go语言为并发编程而内置的上层API基于顺序通信进程模型CSP(communicating sequential processes)。这就意味着显式锁都是可以避免的,因为Go通过相对安全的通道发送和接受数据以实现同步,这大大地简化了并发程序的编写。
Go语言中的并发程序主要使用两种手段来实现:
- goroutine
- channel
什么是Goroutine
goroutine是Go语言并行设计的核心,有人称之为go程。 Goroutine从量级上看很像协程,它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。
一般情况下,一个普通计算机跑几十个线程就有点负载过大了,但是同样的机器却可以轻松地让成百上千个goroutine进行资源竞争。
创建Goroutine
只需在函数调⽤语句前添加 go 关键字,就可创建并发执⾏单元。开发⼈员无需了解任何执⾏细节,调度器会自动将其安排到合适的系统线程上执行。
在并发编程中,我们通常想将一个过程切分成几块,然后让每个goroutine各自负责一块工作,当一个程序启动时,主函数在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。而go语言的并发设计,让我们很轻松就可以达成这一目的。
1 | package main |
Goroutine特性
主goroutine退出后,其它的工作goroutine也会自动退出:
1 | package main |
Goexit函数
调用 runtime.Goexit()
将立即终止当前 goroutine 执⾏,调度器确保所有已注册 defer 延迟调用被执行。
1 | package main |
1 | B.defer |
channel
简介
channel是Go语言中的一个核心类型,可以把它看成管道。并发核心单元通过它就可以发送或者接收数据进行通讯,这在一定程度上又进一步降低了编程的难度。
channel是一个数据类型,主要用来解决go程的同步问题以及go程之间数据共享(数据传递)的问题。
goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine 奉行通过通信来共享内存,而不是共享内存来通信。
引⽤类型 channel 可用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。
定义channel变量
和map类似,channel也一个对应make创建的底层数据结构的引用。
当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。
定义一个channel时,也需要定义发送到channel的值的类型。channel可以使用内置的make()函数来创建:
chan 是创建channel所需使用的关键字。Type 代表指定channel收发数据的类型。
1 | make(chan Type) //等价于make(chan Type, 0) |
当参数capacity= 0 时,channel 是无缓冲阻塞读写的;当capacity > 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity个元素才阻塞写入。
channel非常像生活中的管道,一边可以存放东西,另一边可以取出东西。channel通过操作符 <- 来接收和发送数据,发送和接收数据语法:
1 | package main |
默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得goroutine同步变的更加的简单,而不需要显式的lock。
无缓冲的channel
无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何数据值的通道。这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。否则,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。
这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。
阻塞:由于某种原因数据没有到达,当前go程(线程)持续处于等待状态,直到条件满足,才解除阻塞。
同步:在两个或多个go程(线程)间,保持数据内容一致性的机制。
下图展示两个 goroutine 如何利用无缓冲的通道来共享一个值:
有缓冲的channel
有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个数据值的通道。这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也不同。
只有通道中没有要接收的值时,接收动作才会阻塞。
只有通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。
这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。
示例图如下:
有缓冲的 channel 创建格式:
1 | make(chan Type, capacity) |
如果给定了一个缓冲区容量,通道就是异步的。只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞地进行。
借助函数 len(ch)
求取缓冲区中剩余元素个数,cap(ch)
求取缓冲区元素容量大小。
1 | package main |
1 | len(c) = 0 , cap(c) = 3 |
关闭channel
如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现。
1 | package main |
1 | 0 |
如果子go程不关闭,会产生死锁。
注意:
- channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;
- 关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
- 关闭channel后,可以继续从channel接收数据;
- 对于nil channel,无论收发都会被阻塞。
range
可以使用 range 来迭代不断操作channel:
1 | for data := range c { |
单向channel
默认情况下,通道channel是双向的,也就是,既可以往里面发送数据也可以同里面接收数据。但是,我们经常见一个通道作为参数进行传递而只希望对方是单向使用的,要么只让它发送数据,要么只让它接收数据,这时候我们可以指定通道的方向。
单向 channel 变量的声明非常简单,如下:
1 | var ch1 chan int // ch1是一个正常的channel,是双向的 |
- chan <- 表示数据进入管道,要把数据写进管道,对于调用者就是输出。
- <- chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入。
可以将 channel 隐式转换为单向队列,只收或只发,不能将单向 channel 转换为普通 channel。
select
Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。
有时候我们希望能够借助channel发送或接收数据,并避免因为发送或者接收导致的阻塞,尤其是当channel没有准备好写或者读时。select语句就可以实现这样的功能。
select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。
与switch语句相比,select有比较多的限制,其中最大的一条限制就是每个 case 语句里必须是一个IO操作,大致的结构如下:
1 | select { |
在一个select语句中,Go语言会按顺序从头至尾评估每一个发送和接收的语句。
如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。
如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:
- 如果给出了default语句,那么就会执行default语句,同时程序的执行会从select语句后的语句中恢复。
- 如果没有default语句,那么select语句将被阻塞,直到至少有一个通信可以进行下去。
1 | package main |
Go Modules
什么是 Go Modules
Go modules 是 Go 语言的依赖解决方案,发布于 Go1.11,成长于 Go1.12,丰富于Go1.13,正式于 Go1.14 推荐在生产上使用。
Go moudles 目前集成在 Go 的工具链中,只要安装了 Go,自然而然也就可以使用 Go moudles 了,而 Go modules 的出现也解决了在 Go1.11 前的几个常见争议问题:
- Go 语言长久以来的依赖管理问题。
- “淘汰”现有的 GOPATH 的使用模式。
- 统一社区中的其它的依赖管理工具(提供迁移功能)。
GOPATH 工作模式
Go Modoules 的目的之一就是淘汰GOPATH, 那么GOPATH是个什么?为什么在 Go1.11 前就使用 GOPATH,而 Go1.11 后就开始逐步建议使用 Go modules,不再推荐 GOPATH 的模式了呢
What is GOPATH
1 | $ go env |
我们输入go env
命令行后可以查看到 GOPATH 变量的结果,我们进入到该目录下进行查看,如下:
1 | go |
GOPATH目录下一共包含了三个子目录,分别是:
- bin:存储所编译生成的二进制文件。
- pkg:存储预编译的目标文件,以加快程序的后续编译速度。
- src:存储所有
.go
文件或源代码。在编写 Go 应用程序,程序包和库时,一般会以$GOPATH/src/github.com/foo/bar
的路径进行存放。
因此在使用 GOPATH 模式下,我们需要将应用代码存放在固定的$GOPATH/src
目录下,并且如果执行go get
来拉取外部依赖会自动下载并安装到$GOPATH
目录下。
GOPATH 模式的弊端
在 GOPATH 的 $GOPATH/src
下进行 .go
文件或源代码的存储,我们可以称其为 GOPATH 的模式,这个模式拥有一些弊端:
无版本控制概念 :在执行
go get
的时候,你无法传达任何的版本信息的期望,也就是说你也无法知道自己当前更新的是哪一个版本,也无法通过指定来拉取自己所期望的具体版本。无法同步一致第三方版本号:在运行 Go 应用程序的时候,你无法保证其它人与你所期望依赖的第三方库是相同的版本,也就是说在项目依赖库的管理上,你无法保证所有人的依赖版本都一致。
无法指定当前项目引用的第三方版本号:你没办法处理 v1、v2、v3 等等不同版本的引用问题,因为 GOPATH 模式下的导入路径都是一样的,都是
github.com/foo/bar
。
Go Modules 模式
接下来用 Go Modules 的方式创建一个项目, 建议为了与 GOPATH 分开,不要将项目创建在GOPATH/src
下。
go mod 命令
命令 | 作用 |
---|---|
go mod init | 生成 go.mod 文件 |
go mod download | 下载 go.mod 文件中指明的所有依赖 |
go mod tidy | 整理现有的依赖 |
go mod graph | 查看现有的依赖结构 |
go mod edit | 编辑 go.mod 文件 |
go mod vendor | 导出项目所有的依赖到vendor目录 |
go mod verify | 校验一个模块是否被篡改过 |
go mod why | 查看为什么需要依赖某模块 |
go mod 环境变量
可以通过 go env
命令来进行查看:
1 | go env |
GO111MODULE
Go语言提供了GO111MODULE
这个环境变量来作为 Go modules 的开关,其允许设置以下参数:
- auto:只要项目包含了 go.mod 文件的话启用 Go modules,目前在 Go1.11 至 Go1.14 中仍然是默认值。
- on:启用 Go modules,推荐设置,将会是未来版本中的默认值。
- off:禁用 Go modules,不推荐设置。
GOPROXY
这个环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时直接通过镜像站点来快速拉取。
GOPROXY 的默认值是:https://proxy.golang.org,direct
proxy.golang.org
国内访问不了,需要设置国内的代理。
GOPROXY 的值是一个以英文逗号 “,” 分割的 Go 模块代理列表,允许设置多个模块代理。
1 | go env -w GOPROXY=https://goproxy.cn,https://mirrors.aliyun.com/goproxy/,direct |
而在刚刚设置的值中,我们可以发现值列表中有 “direct” 标识,它又有什么作用呢?
实际上 “direct” 是一个特殊指示符,用于指示 Go 回源到模块版本的源地址去抓取(比如 GitHub 等),场景如下:当值列表中上一个 Go 模块代理返回 404 或 410 错误时,Go 自动尝试列表中的下一个,遇见 “direct” 时回源,也就是回到源地址去抓取,而遇见 EOF 时终止并抛出类似 “invalid version: unknown revision…” 的错误。
GOSUMDB
它的值是一个 Go checksum database,用于在拉取模块版本时(无论是从源站拉取还是通过 Go module proxy 拉取)保证拉取到的模块版本数据未经过篡改,若发现不一致,也就是可能存在篡改,将会立即中止。
GOSUMDB 的默认值为:sum.golang.org
,在国内也是无法访问的,但是 GOSUMDB 可以被 Go 模块代理所代理(详见:Proxying a Checksum Database)。
因此我们可以通过设置 GOPROXY 来解决,而先前我们所设置的模块代理 goproxy.cn
就能支持代理 sum.golang.org
,所以这一个问题在设置 GOPROXY 后,你可以不需要过度关心。
另外若对 GOSUMDB 的值有自定义需求,其支持如下格式:
- 格式 1:
<SUMDB_NAME>+<PUBLIC_KEY>
。 - 格式 2:
<SUMDB_NAME>+<PUBLIC_KEY> <SUMDB_URL>
。
也可以将其设置为“off”,也就是禁止 Go 在后续操作中校验模块版本。
GONOPROXY/GONOSUMDB/GOPRIVATE
这三个环境变量都是用在当前项目依赖了私有模块,例如像是你公司的私有 git 仓库,又或是 github 中的私有库,都是属于私有模块,都是要进行设置的,否则会拉取失败。
更细致来讲,就是依赖了由 GOPROXY 指定的 Go 模块代理或由 GOSUMDB 指定 Go checksum database 都无法访问到的模块时的场景。
而一般建议直接设置 GOPRIVATE,它的值将作为 GONOPROXY 和 GONOSUMDB 的默认值,所以建议是直接使用 GOPRIVATE。并且它们的值都是一个以英文逗号 “,” 分割的模块路径前缀,也就是可以设置多个,例如:
1 | go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote" |
设置后,前缀为 git.xxx.com 和 github.com/eddycjy/mquote 的模块都会被认为是私有模块。如果不想每次都重新设置,我们也可以利用通配符,例如:
1 | go env -w GOPRIVATE="*.example.com" |
这样子设置的话,所有模块路径为 example.com 的子域名(例如:git.example.com)都将不经过 Go module proxy 和 Go checksum database,需要注意的是不包括 example.com 本身。
使用Go Modules初始化项目
开启Go Modules
1 | go env -w GO111MODULE=on |
初始化项目
创建项目目录
1 | mkdir -p /Users/liuwq/Documents/GoProjects/modules_test |
执行 Go modules 初始化
1 | go mod init github.com/liuwq/modules_test |
在执行 go mod init
命令时,我们指定了模块导入路径为 github.com/aceld/modules_test
。接下来我们在该项目根目录下创建 main.go
文件,如下:
1 | package main |
OK,我们先不要关注代码本身,我们看当前的 main.go 也就是我们的liuwq/modules_test
项目,是依赖一个叫github.com/aceld/zinx
库的。 znet
和ziface
只是zinx
的两个模块。
接下来我们在本项目的根目录执行:
1 | go get github.com/aceld/zinx/znet |
我们会看到 我们的go.mod
被修改,同时多了一个go.sum
文件。
查看go.mod文件
module
:用于定义当前项目的模块路径go
:标识当前Go版本,即初始化版本require
:当前项目依赖的一个特定的必须版本// indirect
: 示该模块为间接依赖,也就是在当前应用程序中的 import 语句中,并没有发现这个模块的明确引用,有可能是你先手动go get
拉取下来的,也有可能是你所依赖的模块所依赖的。我们的代码很明显是依赖的"github.com/aceld/zinx/znet"
和"github.com/aceld/zinx/ziface"
,所以就间接的依赖了`github.com/aceld/zinx
查看go.sum文件
在第一次拉取模块依赖后,会发现多出了一个 go.sum 文件,其详细罗列了当前项目直接或间接依赖的所有模块版本,并写明了那些模块版本的 SHA-256 哈希值,以备 Go 在今后的操作中保证项目所依赖的那些模块版本不会被篡改。
我们可以看到一个模块路径可能有如下两种:
- h1:hash情况
1 | github.com/aceld/zinx v1.1.21 h1:8zoZ+hcEAd7gDsl8xOKPaWPEs9vZDRQOvhjG3vuvAnQ= |
- go.mod hash 情况
1 | github.com/aceld/zinx v1.1.21/go.mod h1:nITkdASGtkLSwNKZ5yj88IpcCHTCFCP6cL12JWms1Fo= |
h1 hash 是 Go modules 将目标模块版本的 zip 文件开包后,针对所有包内文件依次进行 hash,然后再把它们的 hash 结果按照固定格式和算法组成总的 hash 值。
而 h1 hash 和 go.mod hash 两者,要不就是同时存在,要不就是只存在 go.mod hash。那什么情况下会不存在 h1 hash 呢,就是当 Go 认为肯定用不到某个模块版本的时候就会省略它的 h1 hash,就会出现不存在 h1 hash,只存在 go.mod hash 的情况。
修改模块的版本依赖关系
1 | go mod edit -replace=zinx@v0.0.0-20200306023939-bc416543ae24=zinx@v0.0.0-20200221135252-8a8954e75100 |
实战-即时通信系统
构建基础Server
main.go
1 | package main |
server.go
server类型
1 | type Server struct { |
方法
创建一个server对象
1 | // 创建一个Server接口 |
启动Server服务
1 | // 启动服务器接口 |
处理链接业务
1 | func (this *Server) Handler(conn net.Conn) { |
测试
- 编译
1 | go build -o server main.go server.go |
- 启动服务器
1 | ./server |
- 模拟
1 | nc 127.0.0.1 8888 |
- 结果
用户上线功能
架构
user.go
user类型
1 | type User struct { |
方法
创建一个user对象
1 | // 创建一个用户的API |
监听user对应的channel消息
1 | // 监听当前User channel的方法,一旦有消息,就直接发送给客户端 |
server.go
server类型
新增OnlineMap和Message属性
1 | type Server struct { |
1 | // 创建一个Server接口 |
在处理客户端上线的Handler创建并添加用户
1 | func (this *Server) Handler(conn net.Conn) { |
新增广播消息方法
1 | // 广播消息的方法 |
新增监听广播消息channel方法
1 | // 监听Message广播消息channel的goroutine,一旦有消息,就发送给在线的所有User |
⽤一个goroutine单独监听Message
1 | //启动监听message的goroutine |
测试
- 编译并运行
1 | go build -o server main.go server.go user.go |
- 启动多个客户端
1 | nc 127.0.0.1 8888 |
- 结果
用户消息广播机制
server.go
完善handler处理业务方法,启动一个针对当前客户端的读goroutine。
1 | // 接受客户端发送的消息 |
测试
启动服务后,开启多个客户端进行测试
用户业务层封装
user.go
user类型新增server关联
1 | type User struct { |
1 | // 创建一个用户的API |
新增Online方法
1 | // 用户的上线业务 |
新增Offline方法
1 | // 用户的下线业务 |
新增DoMessage方法
1 | // 用户处理消息的业务 |
server.go
将之前user的业务进行替换
在线用户查询
消息格式“who”
user.go
- 提供SendMsg向对象客户端发送消息API
1 | func (this *User) SendMsg(msg string) { |
- 在
DoMessage()
方法中,加上对“who”指令的处理,返回在线用户信息
1 | // 用户处理消息的业务 |
修改用户名
消息格式“rename|小新”
user.go
在DoMessage()
方法中,加上对“rename|小新”指令的处理,返回在线用户信息
1 | // 用户处理消息的业务 |
测试
超时强踢功能
用户的任意消息表示为用户活跃,长时间不发消息则认为超时,就要强制关闭用户连接。
server.go
- 在用户的
Hander()
goroutine 中,添加用户活跃channel,一旦有消息,就向该channel发送数据
- 在用户的
Hander()
goroutine 中,添加定时器功能,超时则强踢
1 | // 当前handler阻塞 |
测试
私聊功能
消息格式“to|小新|你好…”
user.go
在DoMessage()
方法中,加上对“to|小新|你好…”指令的处理,返回在线用户信息
1 | } else if len(msg) > 5 && msg[:3] == "to|" { |
测试
客户端实现
客户端类型定义与链接
1 | type Client struct { |
解析命令行
init函数初始化命令行参数
1 | var serverIp string |
main函数解析命令行
1 | //解析命令行 |
测试
菜单显示
client新增flag属性
新增menu()方法,获取用户输入的模式
1 | func (client *Client) menu() bool { |
新增Run()主业务循环
1 | func (client *Client) Run() { |
main()中调用Run()
1 | //启动客户端的业务 |
更新用户名
新增UpdateName()更新用户名
1 | func (client *Client) UpdateName() bool { |
加入到Run业务分支中
1 | case 3: |
添加处理server回执消息方法DealResponse()
1 | // 处理server回应的消息,直接显示输出即可 |
开启一个go程,去承载DealResponse()
1 | //单独开启一个goroutine去处理server的回执消息 |
测试
公聊模式
新增PublicChat()公聊模式业务
1 | func (client *Client) PublicChat() { |
加入到Run的分支中
1 | case 1: |
测试
私聊模式
查询当前都有哪些用户在线
1 | func (client *Client) SelectUsers() { |
新增私聊模式业务
1 | func (client *Client) PrivateChat() { |
添加到Run业务分支
1 | case 2: |