Golang8小时基础入门
1 Golang安装和环境变量
首先下载安装包,Golang镜像网站,Golang中文镜像网站。Windows直接安装,Linux解压到要安装的文件夹下即可。
设置环境变量
GOROOT,设置为安装路径即可,例如:D:\Program Files\go
GOPATH,即我们写go语言的工作路径,可以自定义,例如:D:\Program Files\go\GoWorks
然后在path中加上%GOROOT\bin
验证是否安装和配置成功:
go version
IDE如果是免费的选择VSCode,收费的选择Goland。也可以Vim+go插件。
2 Golang语言特性
2.1 优势
1、极简单的部署方式
可直接编译成机器码,不依赖其他库,可以直接运行部署。
2、静态类型语言
编译的时候可以检查出隐藏的大多数问题(一般静态语言的优势)。
3、语言层面的并发
go语言是基因支持的并发,很多语言其实是“美容”的并发,一层包一层实现高并发。go的语法使得能够充分地利用多核,切换成本低,尽量提高CPU并发效率。
4、强大的标准库支撑
有runtime的系统调度机制,高效的垃圾回收,丰富的标准库。
5、简单易学
仅有25个关键字,语法从c语言过渡,内嵌c语法支持,具有面向对象特征(继承、封装、多态),跨平台。
6、大厂领军
国内外大公司有在使用go语言,例如Google,Facebook,腾讯,百度,字节跳动,京东,小米,阿里巴巴,哔哩哔哩等等。
例子:斐波那契亚数列算法下不同语言效率对比
就这个例子而言,可以看到不管是编译还是运行Go都是比较快的。
2.2 适合做什么
1、云计算基础设施领域
2、基础后端软件
3、微服务
4、互联网基础设施
2.3 明星作品
Docker和Kubernetes
2.4 缺点
1、包管理,大部分第三方库都在Github上,代码稳定性有风险
2、无泛化类型,目前在Go1.18已经加上了泛型
3、全部Exception都采用Error处理
Java是极端地把全部Error都用Exception处理,python是取了中间两种都可以,而Golang是极端地把全部Exception都用Error处理,没有谁对谁错之分
4、对C语言的降级处理并不是无缝的,没有降级到汇编那么完美,但目前只有go能够这样做
c语言是唯一能够和操作系统交流的语言,
3 Golang语法新奇
3.1 从main函数初见go的语法
package main
import "fmt"
func main() {
fmt.Println("Hello Go!")
}
然后使用go run hello.go
既编译又运行,或者先go build hello.go
会生成可执行程序,再.\hello
执行。
1、有没有分号都可以,对编译影响不大,建议不加
2、导包方式有两种,导入多个包建议后者
import "fmt"
import "time"
或者
import (
"fmt"
"time"
)
3、方法的左花括号必须要和函数名同一行
3.2 变量
3.2.1 单变量声明
1、声明一个变量,不初始化,默认值是0
var a int
2、声明一个变量,并初始化
var b int = 100
3、初始化时省去类型声明,通过值的类型自动匹配(不推荐)
var c = 100
fmt.Printf("%T", c)
4、(常用)省去var关键字,直接匹配
d := 100
区别:声明全局变量(方法外)可以用前三种,不能用第四种
3.2.2 多变量声明
1、数据类型相同
var x, y int = 100, 200
2、数据类型不同
var k, l = 100, "hello"
3、多行写法,类型可不声明
var (
i (int) = 100
j (bool) = true
)
3.3 常量与iota
常量命名
const length int = 10
const定义枚举
const (
BEIJING = 0
SHANGHAI = 1
SHENZHEN = 2
)
或者使用iota,只要第一个赋值iota,它默认是0,每行依次加1
const (
BEIJING = iota
SHANGHAI
SHENZHEN
)
如果写成10*iota
,则依次是0,10,20。相当于后面都是符合前面的iota表达式,如果中间改变表达式,后面也会改变,但是iota累加的值保持。
const (
a, b = 1 + iota, 2 + iota
c, d
e, f
g, h = 2 * iota, 3 * iota
i, j
)
fmt.Println(a, b, c, d, e, f, g, h, i, j)
3.4 函数
函数声明,括号内是形参,右边是返回值
func f1(a string, b int) int {
fmt.Println(a)
fmt.Println(b)
c := 100
return c
}
多返回值,用括号括起来
1、匿名返回值
func f2(a string, b int) (int, int) {
fmt.Println(a)
fmt.Println(b)
return 111, 222
}
2、给返回值命名
func f3(a string, b int) (r1 int, r2 int) {
fmt.Println(a)
fmt.Println(b)
r1 = 111
r2 = 222
return
}
3、如果返回值类型一样,可以只保留一个返回值类型
func f4(a string, b int) (r1, r2 int) {
fmt.Println(a)
fmt.Println(b)
r1 = 111
r2 = 222
return
}
3.5 init函数与导包
- init函数
从图中可以看出,程序先从main包进入,再递归导包,然后执行包中的常量,变量,init函数,并依次返回,最后执行main方法。
对外开放的方法首字母大写。
- import导包
go语言的包导入如果不调用会报错。
1、如果不想使用某一个包的API,但是要使用这个包的init函数,可以匿名导包
import _ "lib1"
2、可以给包起别名,并且可以调用它的方法
import mylib2 "lib2"
3、将包的全部方法导入本包,调用方法时可以不带包名(少使用,防止函数名冲突)
import . lib2
3.6 指针
默认情况下方法是值传递
package main
import "fmt"
func main() {
a := 1
changeValue(a)
fmt.Println(a)//输出为20,说明a的值未改变
}
func changeValue(p int) {
p = 10
}
可以指针传递,*int表示是指向int类型的指针,p处存储的就是a的地址值,*p表示找到存的地址值对应的地址,然后改变值为10,&表示传入地址
package main
import "fmt"
func main() {
a := 1
changeValue(&a)
fmt.Println(a)//输出10,说明a的值被改变
}
func changeValue(p *int) {
*p = 10
}
经典例子:交换数据
如果这样交换,并不能交换成功,因为是值传递
package main
import "fmt"
func main() {
a := 10
b := 20
swap(a, b)
fmt.Println(a, b)
}
func swap(a int, b int) {
tmp := a
a = b
b = tmp
}
需要使用指针
package main
import "fmt"
func main() {
a := 10
b := 20
swap(&a, &b)
fmt.Println(a, b)
}
func swap(a *int, b *int) {
tmp := *a
*a = *b
*b = tmp
}
二级指针:指针的指针
3.7 defer关键字
有点像c++的析构函数或者Java中的finally关键字
defer语句放在return之前,在当前函数结束,return返回后执行,defer可以有多个,但是按照栈的顺序,先写的后执行。
package main
import "fmt"
func main() {
returnAndDefer()
}
func returnFun() int {
fmt.Println("return...")
return 0
}
func returnAndDefer() int {
defer fmt.Println("defer...")
return returnFun()
}
3.8 数组和动态数组slice
声明数组
var myArray1 [10]int
myArray2 := [10]int{}
数组如果作为形参,要声明长度,而且是值拷贝,方法内不改变数组的值
func printArray(myArray [10]int) {
for i, value := range myArray {
fmt.Println(i,value)
}
}
动态数组声明,相比之下不指定长度
var myArray1 []int
myArray1 := []int{1,2,3,4}
方法如下,动态数组是指针传递,因此方法内会改变原有的值
func printArray(myArray []int) {
for i, value := range myArray {
fmt.Println(i, value)
}
}
另外,再使用range遍历时,如果索引不想使用,可以使用匿名的方式for _, value := range myArray
3.8.1 slice声明方式
四种声明slice方式如下:
slice1 := []int{1, 2, 3}
var slice2 []int
var slice3 []int = make([]int,3)//通过make分配空间
slice4 := make([]int,3)
if slice2 == nil {
fmt.Println("空切片")
} else {
fmt.Println("有空间")
}
输出空切片,要注意else要和上一个右花括号放在同一行。
3.8.2 slice使用方式
1、切片容量的追加
长度小于等于容量
append方法追加一个元素并赋值,如果cap不够会追加cap容量
var slice = make([]int, 3, 5)
fmt.Println(len(slice), cap(slice), slice)
slice = append(slice, 1)
fmt.Println(len(slice), cap(slice), slice)
slice = append(slice, 2)
fmt.Println(len(slice), cap(slice), slice)
slice = append(slice, 3)
fmt.Println(len(slice), cap(slice), slice)
扩容机制:根据cap增加二倍,即每次翻一倍
2、切片的截取
这里和python有些类似,但是这里是指针传递,改变slice2会改变slice。
slice1 := slice[0:2]//取头不取尾
slice2 := slice[:5]
slice3 := slice[3:]
slice4 := slice[:]
copy函数可以深拷贝,前一个参数是destination,后一个参数是source
slice := []int{0, 1, 2, 3, 4, 5, 6}
slice5 := make([]int,7)
copy(slice5,slice)
3.9 map
三种声明方式:
var myMap1 map[string]int
myMap1 = make(map[string]string, 10)
myMap1["one"] = "java"
myMap1["two"] = "c"
myMap1["three"] = "python"
myMap2 := make(map[string]string)
myMap3 := map[string]string{
"one": "c++",
"two": "java",
"three": "python",
}
使用方式:
//遍历
for key, value := range cityMap {
fmt.Println(key)
fmt.Println(value)
}
//删除
delete(cityMap, "Japan")
//修改
cityMap["USA"] = "Washington"
3.10 面向对象特征
type声明一种新的数据类型,可以定义结构体,即把多种基本数据类型组合形成复杂的数据类型。%v可以格式化各种类型的输出。
type Book struct {
title string
autu string
}
var book1 Book
book1.title = "Golang"
book1.autu = "zhangsan"
fmt.Printf("%v\n", book1)
如果需要函数中改变值,需要传指针
changeBook(&book1)
func changeBook(book *Book) {
book.auth = "666"
}
go语言中的类其实就是结构体绑定方法,
type Hero struct {
Name string
Ad int
Level string
}
func (this Hero) GetName() {
fmt.Println("Name = " + this.Name)
}
func (this Hero) SetName(newName string) {
this.Name = newName
}
func (this Hero) Show() {
fmt.Println("Name = ", this.Name)
fmt.Println("Ad = ", this.Ad)
fmt.Println("Level = ", this.Level)
}
//创建并初始化对象
hero := Hero{Name: "zhangsan", Ad: 100, Level: 1}
要注意,this Hero是调用这个方法的对象的拷贝,因此SetName不会修改原来的属性值,要实现修改还是需要使用传指针
func (this *Hero) Show() {
fmt.Println("Name = ", this.Name)
fmt.Println("Ad = ", this.Ad)
fmt.Println("Level = ", this.Level)
}
func (this *Hero) GetName() string {
return this.Name
}
func (this *Hero) SetName(newName string) {
this.Name = newName
}
3.10.1 封装
前面已经提到了,方法名首字母如果大写,可以被其他包访问。结构体名字,属性首字母如果大写可以被其他包访问,如果小写只有包内部可以访问。这就是go语言的封装。
3.10.2 继承
在SuperMan中Human就表示继承了Human这个类。可以重写方法,添加新方法。
type Human struct {
name string
sex string
}
type SuperMan struct {
Human
level int
}
创建对象:
human := Human{"zhangsan", "female"}
superMan := SuperMan{Human{"lisi", "female"}, 100}
//或者
var s SuperMan
s.name = "zhangsan"
s.sex = "male"
s.level = 3
3.10.3 多态
interface本质是一个指针,在实现类只要重写三个方法就实现了接口。如果重写不完全,接口就不能指向这个实现类。
type AnimalIF interface {
Sleep()
GetColor() string
GetType() string
}
type Cat struct {
color string
}
func (this *Cat) Sleep() {
fmt.Println("Cat is sleeping")
}
func (this *Cat) Getolor() string {
return this.color
}
func (this *Cat) GetType() string {
return "Cat"
}
创建接口指向实现类,需要把对象的地址传过去。
var animal AnimalIF
animal = &Cat{"green"}
animal.Sleep()
animal = &Dog{"blue"}
animal.Sleep()
多态的方法
func showAnimal(animal AnimalIF) {
animal.Sleep()
fmt.Println("color = ", animal.GetColor())
fmt.Println("type = ", animal.GetType())
}
cat := Cat{"black"}
dog := Dog{"yellow"}
showAnimal(&cat)
showAnimal(&dog)
3.10.4 万能类型
- 基本数据类型都实现了
interface{}
,可以用interface{}
引用任意类型。
func myFunc(arg interface{}) {
fmt.Println("myFunc is called...")
fmt.Println(arg)
}
book := Book{"golang", "zhangsan"}
myFunc(book)
myFunc(100)
myFunc("abc")
myFunc(3.14)
- go语言提供了“类型断言”机制,判断是否是某种类型
func myFunc(arg interface{}) {
value, ok := arg.(string)
fmt.Println(value, ok)
}
- 变量的内置pair
一个变量包含类型type和值value,类型要么是静态类型,要么是具体类型。type和value组成pair
示例,赋值的时候pair不会改变
var a string
a = "abc"
var allType interface{}
allType = a
str, ok := allType.(string)
fmt.Println(str, ok)
或者
type Reader interface {
ReadBook()
}
type Writer interface {
WriteBook()
}
type Book struct {
}
func (this Book) ReadBook() {
fmt.Println("Read a Book")
}
func (this Book) WriteBook() {
fmt.Println("Write a Book")
}
func main() {
b := Book{}
var r Reader
r = b
r.ReadBook()
var w Writer
w = r.(Writer)
w.WriteBook()
}
上述例子之所以成立是因为“赋值的时候pair不会改变”,复制过去的pair是<type:Book,value:&Book{}>
。
3.11 反射
在reflect包,两个重要API:TypeOf和ValueOf
func main() {
var num float64 = 1.2345
refelctNum(num)
}
func DoFileAndMethod(input interface{}) {
//获取类型
inputType := reflect.TypeOf(input)
fmt.Println("input type is:", inputType.Name())
//获取值
inputValue := reflect.ValueOf(input)
fmt.Println("input value is:", inputValue)
//获取字段Field
for i := 0; i < inputType.NumField(); i++ {
field := inputType.Field(i)
value := inputValue.Field(i).Interface()
fmt.Println(field.Name, field.Type, value)
}
//获取方法并调用
for i := 0; i < inputType.NumMethod(); i++ {
m := inputType.Method(i)
fmt.Println(m.Name, m.Type)
}
}
3.12 结构体标签Tag
注意要用反斜杠,里面是键值对,中间用空格隔开,主要的作用是根据这个标签,判断这个属性在不同包中怎么用。
type resume struct {
Name string `info:"name" doc:"我的名字"`
Sex string `info:"sex"`
}
func findTag(str interface{}) {
t := reflect.TypeOf(str).Elem()
for i := 0; i < t.NumField(); i++ {
tagString := t.Field(i).Tag.Get("info")
fmt.Println("info:", tagString)
}
}
func main() {
var re resume
findTag(&re)
}
3.12.1 结构体标签的应用
- 在json中的应用,编解码
在转换为json时,会先检查是否在标签中有json键值对,有则将值取出来组成json字符串。
type Movie struct {
Title string `json:"title"`
Year int `json:"year"`
Price int `json:"price"`
Actors []string `json:"actors"`
}
func main() {
movie := Movie{"喜剧之王", 2000, 10, []string{"周星驰", "张柏芝"}}
//编码的过程 结构体-->json
jsonStr, err := json.Marshal(movie)
if err != nil {
fmt.Println("json marshal error", err)
return
}
fmt.Printf("jsonStr=%s\n", jsonStr)
//解码的过程 json-->结构体
myMovie := Movie{}
err = json.Unmarshal(jsonStr, &myMovie)
if err != nil {
fmt.Println("json unmarshal error", err)
return
}
fmt.Println(myMovie)
}
- orm映射关系
4 Golang高阶
4.1 协程
4.1.1 co-routine
进程或线程数量越多,切换成本越高,CPU资源越浪费,此外还有高内存占用的弊端。一个线程分为用户态和内核态,划分之后,内核线程称为线程thread,用户线程称为协程co-routine。通过一个内核线程和协程调度器,绑定多个协程。内核线程和协程之间的关系不适合一对多或者一对一,适合N对M。从图中可以看出主要需要优化的就是协程调度器。
4.1.2 Golang的协程——Goroutine
Golang的协程内存小,几KB,灵活调度。G表示goroutine协程,P表示处理器,M表示内核线程。
调度器的设计策略:
- 服用线程
- 利用并行
- 抢占
- 全局G队列
go语言创建协程非常方便,使用go关键字加上函数即可。
第一种调用,go + 方法
//子Goroutine
func newTask() {
i := 0
for {
i++
fmt.Printf("new Goroutine : i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
//主Goroutine
func main() {
go newTask()
}
第二种调用,匿名的go协程,匿名方法也可以有形参和返回值,但是返回值不能直接用参数去接,如果要接,其实就是要解决协程之间的通信,这里就要用到channel了
func main() {
//用go创建承载一个形参为空,返回值为空的函数
go func() {
defer fmt.Println("A.defer")
//匿名函数
func() {
defer fmt.Println("B.defer")
runtime.Goexit()//退出当前协程,而不仅是匿名函数
fmt.Println("B")
}()
fmt.Println("A")
}()
for {
time.Sleep(1 * time.Second)
}
}
4.2 协程的通信——channel
先定义channel类型变量,然后在协程中通过<-
将值赋给channel变量,最后在主协程中获取channel变量的值。channel有同步两个协程的能力,
func main() {
//定义一个channel
c := make(chan int)
go func() {
defer fmt.Println("goroutine结束")
fmt.Println("goroutine正在运行...")
c <- 666
}()
num := <-c
fmt.Println("num = ", num)
fmt.Println("main goroutine结束...")
}
示意图如下,如果main协程更快执行到了num := <-c
,会被阻塞,等待channel中有值,如果sub协程更快执行,那么main协程能顺利取到管道的值。
4.2.1 无缓存的channel
对于无缓存的channel,传递消息的一方如果提前到达了要传递channel的指令,但此时接收协程还没有执行到接收channel,那么发送方就需要一直等待,直到接收方来接收。
4.2.2 有缓存的channel
对于有缓存的channel,发送方将数据发送到channel中便继续执行程序,如果管道中有数据,接收方直接取走数据;发送方发现channel中已经存满了数据时才会被阻塞,接收方直到channel中被取空了才会被阻塞。类似生产者消费者模式。
代码实例:
func main() {
c := make(chan int, 3)
fmt.Println(len(c), cap(c))
go func() {
defer fmt.Println("子goroutine结束...")
for i := 0; i < 3; i++ {
c <- i
fmt.Println("子goroutine正在运行:len(c)=", len(c), "cap(c)=", cap(c))
}
}()
for i := 0; i < 3; i++ {
num := <-c
fmt.Println("num=", num)
}
fmt.Println("main goroutine结束")
}
4.2.3 关闭channel
- channel不像文件一样需要经常去关闭,除非确定不会再向channel中发送数据,或者想显式地结束range循环,才会去关闭channel。
- 关闭channel后无法向它发送数据
- 关闭channel后可以继续接收数据
- 对于nil channel,无论收发都会被阻塞
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
close(c)
}()
for {
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("Main Finished..")
}
这里if后面的语法意思是这样的:先执行data, ok := <-c
,这样data和ok都是if里面的局部变量,然后把ok
作为条件判断是否执行。
4.2.4 channel和range
类似的,可以用range来获取channel中的数据,把上面的例子修改,代码如下:
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
close(c)
}()
for data := range c {
fmt.Println(data)
}
fmt.Println("Main Finished..")
4.2.5 channel和select
单个goroutine下只能监控一个channel的状态,select能够实现监控多个channel的状态。
func fibonacii(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x = y
y = x + y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacii(c, quit)
}
注意:方法传channel传的是指针。
5 Go modules 模块管理
Go modules是Go语言的依赖解决方案,正式于go1.14推荐在生产上使用,目前只要安装了go就安装了go modules。
5.1 淘汰的Go path
由于go modules淘汰的是go path,因此需要知道go path工作模式。
Go path弊端:
1、没有版本控制概念
使用go get -u ...
时不能指定版本
2、无法同步一致第三方版本号
别人编写的代码使用的库和我们使用的库版本可能不一致
3、无法指定当前项目引用的第三方版本号
5.2 Go modules模式
5.2.1 go mod命令
命令 | 功能 |
---|---|
go mod init | 生成go mod文件 |
go mod download | 下载go mod文件中指明的所有依赖 |
go mod vendor | 导出项目所有依赖到vendor目录 |
5.2.2 go mod环境变量
go mod环境变量通过go env
来查看,重要的环境变量如下:
- GO111MODULE
推荐为on,现在高版本go默认都是on,如果不是可以使用命令go env -w GO111MODULE=on
- GOPROXY
自动导包的时候从哪个站点下载,默认是https://proxy.golang.org,direct
,国内上不去,因此需要更换站点:
阿里云:https://mirrors.aliyun.com/goproxy/
七牛云·:https://goproxy.cn,direct
设置命令为:go env -w GOPROXY=https://goproxy.cn,direct
这里解释一下direct表示回源到模块的源地址去拉取,如果找不到会重定向到源地址拉取,最后找不到就会报错,因此加上就行。 - GOSUMDB
保证拉取到的模块版本数据是没有篡改过的,如果不一致将会立即终止。默认是sum.golang.org
,国内访问不了。但只要GOPROXY设置过了,就可以不用设置。 - GONOPROXY/GONOSUMDB/GOPRIVATE
这三个环境变量表示哪些是不需要代理的,不需要校验的,哪些是私有的。只要配置GOPRIVATE一个变量,其余两个变量就被覆盖了。命令:go env -w GOPRIVATE="..."
5.3 使用Go modules初始化项目
1、保证GO111MODULE是on,具体解释参考上面
go env -w GO111MODULE=on
2、初始化项目
- 任意文件夹创建一个项目
mkdir modules_test
cd modules_test
- 初始化go modules模块,创建go.mod文件,注意要跟上当前模块名称
go mod init github.com/kevin-zkp/modules_test
dir //windows下查看当前目录文件
打开如下:
- 引用代码
import (
"fmt"
"github.com/aceld/zinx/ziface"
"github.com/aceld/zinx/znet"
)
//ping test 自定义路由
type PingRouter struct {
znet.BaseRouter
}
//Ping Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
//先读取客户端的数据
fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))
//再回写ping...ping...ping
err := request.GetConnection().SendBuffMsg(0, []byte("ping...ping...ping"))
if err != nil {
fmt.Println(err)
}
}
func main() {
//1 创建一个server句柄
s := znet.NewServer()
//2 配置路由
s.AddRouter(0, &PingRouter{})
//3 开启服务
s.Serve()
}
- 下载包
//手动download
go get github.com/aceld/zinx/znet
go get github.com/aceld/zinx/ziface
//可以指定版本号,如下
go get github.com/aceld/zinx@v1.0.0
//自动download
执行go run会自动下载
go.mod文件会多一行如下,如果指定版本,在go.mod中指定
再打开go.sum文件
h1加哈希
5.4 修改项目模块的版本依赖关系
使用命令:
go mod edit -replace=版本号1=版本号2
//例如
go mod edit -replace=zinx@v1.0.1=zinx@v1.0.0
如果报错:go: -replace=zinx@v1: need old[@v]=new[@w] (missing =)
,
可以尝试改成:go mod edit -replace='zinx@v1.0.1'='zinx@v1.0.0'
执行完会在go.mod文件下产生如下一行:
6 Golang案例——即时通信系统
项目源码:
目的是覆盖大部分go语法特性,特别是网络,系统基础结构如下,使用了读写分离模型,使用九个版本迭代:
6.1 版本一:构建基础Server
这里创建main.go和server.go,都是在package main中。在main.go中创建Server并运行。在Server.go中首先创建结构体Server,构造函数,启动函数Start,以及处理业务Handler,每个连接都创建一个Handler协程。
编写之后先运行main.go,再模拟tcp连接,linux下可以nc 127.0.0.1 8888
,windows下telnet 127.0.0.1 8888
,显示如下:
6.2 用户上线功能
OnlineMap记录上线的用户,使用channel进行广播。
如果使用windows自带的telnet会中文乱码,可以下载putty,选择Other,telnet,输入Ip地址和端口号,点击open,可以看到成功上线。
6.3 用户消息广播机制
用户输入一段话,也进行广播。回车换行"\r\n"
。这里如果是windows下,由于telnet一次只能传递一个字符,因此对函数要进行一些改动,需要判断接收的字符是否是回车换行,如果不是则拼接字符串,如果是则进行广播。这里使用了读写分离模式,每个用户都有一个协程接收客户端消息,一个协程广播消息。效果如下:
6.4 用户业务层封装
把能够合并的代码封装成函数,sever.go中的用户上线,下线,广播的代码需要提取到user.go中。需要为User添加属性,表示属于哪个Server。
6.5 在线用户查询
当客户端输入who
时,发送所有当前登录用户给客户端。
6.6 修改用户名
消息格式rename|张三
,修改效果如下:
6.7 超时强踢功能
只要执行time.After就会重置,同时它其实是管道,只要监听管道中能否取到数据即可。把isLive写到上面,这样只要isLive触发,其他case会执行条件,但是不会执行括号内的代码,重置定时器。
6.8 私聊功能
消息格式:to|张三|你好呀,我是...
。
6.9 客户端实现
这里还是用终端形式,也可以用UI形式。
1、实现连接
windows下编译成可执行文件,linux下去除.exe
即可执行。
go build -o server.exe server.go main.go user.go
go build -o client.exe client.go
2、让客户端指定IP和端口
解析命令行,借助flag库,这样指定.\client.exe -ip 127.0.0.1 -port 8888
func init() {
flag.StringVar(&serverIp, "ip", "127.0.0.1", "设置服务器IP地址(默认是127.0.0.1)")
flag.IntVar(&serverPort, "port", 8888, "设置服务器端口号(默认是8888)")
}
3、菜单显示
4、更新用户名
5、公聊模式
6、私聊模式
首先查询当前有哪些用户在线,提示选择一个用户
7 Golang生态拓展介绍
1、Web框架
beego:国内的框架,文档比较全
gin:国外的轻量级框架,性能较高,比较主流
echo:国外的,轻量级
Iris:国外的,更加重量级,性能较高
2、微服务框架
go kit:包含很多工具,比较灵活
Istio:包括熔断,安全审核等,适合繁琐的大型微服务
3、容器编排
Kubernetes:市场占有率高,谷歌出来的
Swarm:相对不那么高
4、服务发现
类似Java中的zookeeper
consul:服务发现,服务注册
5、存储引擎
k/v存储——etcd:类似Redis,且支持分布式,一致性比较好
分布式存储——tidb:类似MySQL
6、静态建站
hugo
7、中间件
消息队列——nsg
TCP长链接框架(轻量级服务器)——zinx
Leaf(游戏服务器)——Leaf
RPC框架——gRPC
redis集群——codis
8、爬虫框架
go query:效率比python高,但是目前爬虫生态还是python更好