引言
学习前置要求
- 具备1种后端编程语言开发经验(C/C++/Java/Python/PHP等)
- 具备基本的网络编程能力和并发思想
- 了解计算机基本体系结构
- 了解Linux基础知识
学习提纲
Golang开发环境
Go官网下载地址:golang.org/dl/
Go官方镜像站(推荐):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:
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语法
package main
import (
"fmt"
"time"
)
// main函数
func main() {
fmt.Println("Hello,Golang~")
time.Sleep(1 * time.Second)
}
- 第一行代码 package main 定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
- 下一行 import "fmt" 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。
- 下一行 func main() 是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。
变量的声明
package main
import "fmt"
/*
四种变量的声明方式
*/
// 声明全局变量,方法一、二、三是可以的
var gA int = 100
var gB = 200
//用方法四声明全局变量,会报错
//gC := 300
func main() {
//方法一:声明一个变量,默认的值是0
var a int
fmt.Println("a = ", a)
fmt.Printf("type of a = %T\n", a)
//方法二:初始化一个值
var b int = 100
fmt.Println("b = ", b)
fmt.Printf("type of b = %T\n", b)
var bb string = "abcd"
fmt.Println("bb = ", bb)
fmt.Printf("type of bb = %T\n", bb)
//方法三:在初始化时,可以省去数据类型,通过值自动匹配
var c = 100
fmt.Println("c = ", c)
fmt.Printf("type of c = %T\n", c)
//方法四:(常用)省去var关键字,直接自动匹配
d := 100
fmt.Println("d = ", d)
fmt.Printf("type of d = %T\n", d)
//打印全局变量
fmt.Println("gA = ", gA)
fmt.Println("gB = ", gB)
//fmt.Println("gC = ", gC)
//声明多个变量
var xx, yy int = 1, 2
fmt.Println("xx = ", xx, "yy = ", yy)
var kk, qq = 3, "qwer"
fmt.Println("kk = ", kk, "qq = ", qq)
var (
vv int = 100
jj string = "qazx"
)
fmt.Println("vv = ", vv, "jj = ", jj)
}
常量
常量是一个简单值的标识符,在程序运行时,不会被修改的量。
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
package main
import "fmt"
// const 来定义枚举类型
const (
// 可以在const() 添加一个关键字iota,每行的iota都会累积啊1,第一行的iota默认为0
BEIJING = iota
SHANGHAI
SHENZHEN
NANJING
)
const (
a, b = iota + 1, iota + 2 // iota = 0, a = 1, b = 2
c, d // iota = 1, c = 2, d = 3
e, f // iota = 2, e = 3, f = 4
g, h = iota * 2, iota * 3 // iota = 3, g = 6, h = 9
i, k // iota = 4, i = 8, k = 12
)
func main() {
//常量(只读属性)
const length = 10
fmt.Println("length = ", length)
fmt.Println("BEIJING = ", BEIJING)
fmt.Println("a = ", a, "b = ", b)
fmt.Println("c = ", c, "d = ", d)
fmt.Println("e = ", e, "f = ", f)
fmt.Println("g = ", g, "h = ", h)
fmt.Println("i = ", i, "k = ", k)
// iota 只能够配合const() 一起使用, iota只有在const进行累加效果
}
函数
函数返回多个值
package main
import "fmt"
func foo1(a string, b int) int {
fmt.Println("a = ", a)
fmt.Println("b = ", b)
c := 100
return c
}
// 返回多个返回值,匿名的
func foo2(a string, b int) (int, int) {
fmt.Println("a = ", a)
fmt.Println("b = ", b)
return 666, 777
}
// 返回多个返回值, 有形参名称的
func foo3(a string, b int) (r1 int, r2 int) {
fmt.Println("---- foo3 ----")
fmt.Println("a = ", a)
fmt.Println("b = ", b)
//r1 r2 属于foo3的形参, 初始化默认的值是0
//r1 r2 作用域空间 是foo3 整个函数体的{}空间
fmt.Println("r1 = ", r1)
fmt.Println("r2 = ", r2)
//给有名称的返回值变量赋值
r1 = 1000
r2 = 2000
return
}
func main() {
c := foo1("ab", 200)
fmt.Println("c = ", c)
ret1, ret2 := foo2("haha", 999)
fmt.Println("ret1 = ", ret1, " ret2 = ", ret2)
ret1, ret2 = foo3("foo3", 333)
fmt.Println("ret1 = ", ret1, " ret2 = ", ret2)
}
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函数。
下图详细地解释了整个执行过程:
package lib1
import "fmt"
// 当前lib1包提供的API
func Lib1Test() {
fmt.Println("lib1Test()...")
}
func init() {
fmt.Println("lib1.init() ... ")
}
package lib2
import "fmt"
// 当前lib2包提供的API
func Lib2Test() {
fmt.Println("lib2Test()...")
}
func init() {
fmt.Println("lib2.init() ... ")
}
package main
import (
"GolangStudy/init/lib1"
"GolangStudy/init/lib2"
)
func main() {
lib1.Lib1Test()
lib2.Lib2Test()
}
如果导入了一个包,但没使用,会报错,所以可以声明匿名包,匿名包不能调用方法,但是会执行init()方法
针对每个包也可以起别名。
package main
import (
_ "GolangStudy/init/lib1"
mylib2 "GolangStudy/init/lib2"
)
func main() {
//lib1.Lib1Test()
mylib2.Lib2Test()
}
函数参数
函数如果使用参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。
调用函数,可以通过两种方式来传递参数:
值传递
值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
引用传递(指针传递)
Go 语言中指针是很容易学习的,Go 语言中使用指针可以更简单的执行一些任务。
我们都知道,变量是一种使用方便的占位符,用于引用计算机内存地址。Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。
引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。引用传递指针参数传递到函数内,以下是交换函数 swap() 使用了引用传递:
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 100
var b int= 200
fmt.Printf("交换前,a 的值 : %d\n", a )
fmt.Printf("交换前,b 的值 : %d\n", b )
/* 调用 swap() 函数
* &a 指向 a 指针,a 变量的地址
* &b 指向 b 指针,b 变量的地址
*/
swap(&a, &b)
fmt.Printf("交换后,a 的值 : %d\n", a )
fmt.Printf("交换后,b 的值 : %d\n", b )
}
func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}
defer
defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数。
defer作用:
- 释放占用的资源
- 捕捉处理异常
- 输出日志
如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行。 (压栈)
defer和return,return先执行,defer后执行。
recover错误拦截
运行时panic异常一旦被引发就会导致程序崩溃。
Go语言提供了专用于“拦截”运行时panic的内建函数“recover”。它可以是当前的程序从运行时panic的状态中恢复并重新获得流程控制权。
注意:recover只有在defer调用的函数中有效。
package main
import "fmt"
func Demo(i int) {
//定义10个元素的数组
var arr [10]int
//错误拦截要在产生错误前设置
defer func() {
//设置recover拦截错误信息
err := recover()
//产生panic异常 打印错误信息
if err != nil {
fmt.Println(err)
}
}()
//根据函数参数为数组元素赋值
//如果i的值超过数组下标 会报错误:数组下标越界
arr[i] = 10
}
func main() {
Demo(10)
//产生错误后 程序继续
fmt.Println("程序继续执行...")
}
slice和map
Go 语言切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
定义切片
你可以声明一个未指定大小的数组来定义切片:
myArray := []int
切片不需要说明长度。或使用make()函数来创建切片:
var slice1 []int = make([]int, len)
// 也可以简写为
slice1 := make([]type, len)
也可以指定容量,其中capacity为可选参数。
make([]T, length, capacity)
这里 length 是数组的长度并且也是切片的初始长度。
切片初始化
slice1 := []int{1,2,3} //声明并初始化,默认值为1,2,3,长度len是3
var slice2 []int //未分配空间
var slice4 []int = make([]int, 3) //通过make开辟空间,初始化值为0
slice5 := make([]int, 3)
len()和cap()函数
切片是可索引的,并且可以由 len() 方法获取长度。
切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
切片的追加与截取
切片的扩容机制:append的时候,如果长度增加后超过最大容量,则会自动将容量增加2倍
切片的截取:
s := []int{1,2,3,4}
s1 := s[0:2] // [0,2)
map
map和slice类似,只不过是数据结构不同,下面是map的一些声明方式。
// 第一种声明方式
var myMap1 map[string]string
myMap1 = make(map[string]string, 10)
myMap1["one"] = "php"
myMap1["two"] = "golang"
myMap1["three"] = "java"
// 第二种声明方式
myMap2 := make(map[int]string)
myMap2[1] = "php"
myMap2[2] = "golang"
myMap2[3] = "java"
//第三种声明方式
myMap3 := map[string]string{
"one" : "php",
"two" : "golang",
"three" : "java"
}
struct
type Book struct {
title string
auth string
}
func main() {
var book Book
book.title = "Golang"
book.auth = "zhangsan"
}
面向对象特征
类的封装与表示
- 如果类名首字母大写,表示其他包也能够访问
- 如果说类的属性首字母大写, 表示该属性是对外能够访问的,否则的话只能够类的内部访问
package main
import "fmt"
// 如果类名首字母大写,表示其他包也能够访问
type Hero struct {
//如果说类的属性首字母大写, 表示该属性是对外能够访问的,否则的话只能够类的内部访问
Name string
Ad int
level int
}
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)
}
func main() {
//创建一个对象
hero := Hero{Name: "xiaoxin", Ad: 101, level: 1}
hero.show()
hero.SetName("zhangsan")
hero.show()
}
Name = xiaoxin
Ad = 101
level = 1
Name = xiaoxin
Ad = 101
level = 1
方法的接收者是值类型,只能读,不能改~
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)
}
类的继承
package main
import "fmt"
type Human struct {
name string
age int
}
func (this *Human) Eat() {
fmt.Println("Human.Eat()...")
}
func (this *Human) Walk() {
fmt.Println("Human.Walk()...")
}
// ======================================
type SuperMan struct {
Human //SuperMan类继承了Human类的方法
level int
}
// 重定义父类方法
func (this *SuperMan) Eat() {
fmt.Println("SuperMan.Eat()...")
}
func (this *SuperMan) Walk() {
fmt.Println("SuperMan.Walk()...")
}
func (this *SuperMan) show() {
fmt.Println("name = ", this.name)
fmt.Println("age = ", this.age)
fmt.Println("level = ", this.level)
}
func main() {
human := Human{"小新", 24}
human.Eat()
human.Walk()
//定义一个子类对象
superMan := SuperMan{Human{"小新不吃蔬菜", 24}, 1}
//superMan.name = "abcd"
//superMan.age = 22
superMan.Eat()
superMan.Walk()
superMan.show()
}
类的多态
示例
package main
import "fmt"
// 本质是一个指针
type Animal interface {
Sleep()
GetColor() string //获取动物的颜色
GetType() string //获取动物的种类
}
// 具体的类
type Cat struct {
color string
}
func (this *Cat) Sleep() {
fmt.Println("Cat is sleeping")
}
func (this *Cat) GetColor() string {
return this.color
}
func (this *Cat) GetType() string {
return "Cat"
}
type Dog struct {
color string
}
func (this *Dog) Sleep() {
fmt.Println("Dog is sleeping")
}
func (this *Dog) GetColor() string {
return this.color
}
func (this *Dog) GetType() string {
return "Dog"
}
func showAnimal(animal Animal) {
animal.Sleep()
fmt.Println("color = ", animal.GetColor())
fmt.Println("type = ", animal.GetType())
}
func main() {
var animal Animal
animal = &Cat{"white"}
animal.Sleep()
animal = &Dog{"black"}
animal.Sleep()
cat := Cat{"Green"}
dog := Dog{"Yellow"}
showAnimal(&cat)
showAnimal(&dog)
}
基本要素
- 有一个父类接口
- 有子类,实现了全部接口方法
- 父类类型的变量(指针)指向(引用)子类的具体数据变量
interface与类型断言
Golang的语言中提供了断言的功能。Golang中的所有程序都实现了interface{}的接口,这意味着,所有的类型如string,int,int64甚至是自定义的struct类型都就此拥有了interface{}的接口,这种做法和java中的Object类型比较类似。那么在一个数据通过func funcName(interface{})的方式传进来的时候,也就意味着这个参数被自动的转为interface{}的类型。
package main
import "fmt"
// interface{}是万能数据类型
func myFunc(arg interface{}) {
fmt.Println("myFunc is called ...")
fmt.Println(arg)
//如何区分arg类型
value, ok := arg.(string)
if !ok {
fmt.Println("arg is not string type")
} else {
fmt.Println("arg is string type, value = ", value)
fmt.Printf("value type is %T\n", value)
}
}
type Book struct {
name string
}
func main() {
book := Book{"Golang"}
myFunc(book)
myFunc(100)
myFunc("Java")
myFunc(false)
}
反射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中记录了实际变量的值和类型:
(value, type)
value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型【对应concrete type】,另外一个指针指向实际的值【对应value】。
interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。
reflect.TypeOf()reflect.ValueOf()
package main
import (
"fmt"
"reflect"
)
func reflectNum(arg interface{}) {
fmt.Println("type: ", reflect.TypeOf(arg))
fmt.Println("value: ", reflect.ValueOf(arg))
}
func main() {
var num float64 = 1.2345
reflectNum(num)
}
结构体标签
基本使用
package main
import (
"fmt"
"reflect"
)
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++ {
tagInfo := t.Field(i).Tag.Get("info")
tagDoc := t.Field(i).Tag.Get("doc")
fmt.Println("info:", tagInfo, " doc:", tagDoc)
}
}
func main() {
var re resume
findTag(&re)
}
在json中的应用
package main
import (
"encoding/json"
"fmt"
)
type Movie struct {
Title string `json:"title"`
Year int `json:"year"`
Price int `json:"rmb"`
Actors []string `json:"actors"`
}
func main() {
movie := Movie{"喜剧之王", 2000, 10, []string{"星爷", "张柏芝"}}
// 编码的过程 结构体 ——> json
jsonStr, err := json.Marshal(movie)
if err != nil {
fmt.Println("json marsha 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.Printf("%v\n", myMovie)
}
jsonStr = {"title":"喜剧之王","year":2000,"rmb":10,"actors":["星爷","张柏芝"]}
{喜剧之王 2000 10 [星爷 张柏芝]}
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语言的并发设计,让我们很轻松就可以达成这一目的。
package main
import (
"fmt"
"time"
)
func newTask() {
i := 0
for {
i++
fmt.Printf("new Goroutine : i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
func main() {
// 创建一个go程,去执行newTask()流程
go newTask()
i := 0
for {
i++
fmt.Printf("main Goroutine : i = %d\n", i)
time.Sleep(1 * time.Second)
}
}
Goroutine特性
主goroutine退出后,其它的工作goroutine也会自动退出:
package main
import (
"fmt"
"time"
)
func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(1 * time.Second) //延时1s
}
}
func main() {
//创建一个 goroutine,启动另外一个任务
go newTask()
fmt.Println("main goroutine exit")
}
Goexit函数
调用 runtime.Goexit() 将立即终止当前 goroutine 执⾏,调度器确保所有已注册 defer 延迟调用被执行。
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
//用go创建承载一个形参为空,返回值为空的一个函数
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
//退出当前goroutine
runtime.Goexit()
fmt.Println("B")
}()
fmt.Println("A")
}()
//死循环
for {
time.Sleep(1 * time.Second)
}
}
B.defer
A.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收发数据的类型。
make(chan Type) //等价于make(chan Type, 0)
make(chan Type, capacity)
当参数capacity= 0 时,channel 是无缓冲阻塞读写的;当capacity > 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity个元素才阻塞写入。
channel非常像生活中的管道,一边可以存放东西,另一边可以取出东西。channel通过操作符 <- 来接收和发送数据,发送和接收数据语法:
package main
import "fmt"
func main() {
//定义一个channel
c := make(chan int)
go func() {
defer fmt.Println("goroutine 结束")
fmt.Println("goroutine 正在运行 ...")
c <- 666 //将666发送给c
}()
num := <-c
fmt.Println("num = ", num)
defer fmt.Println("main_goroutine 结束")
}
默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得goroutine同步变的更加的简单,而不需要显式的lock。
无缓冲的channel
无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何数据值的通道。这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。否则,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。
这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。
阻塞: 由于某种原因数据没有到达,当前go程(线程)持续处于等待状态,直到条件满足,才解除阻塞。
同步: 在两个或多个go程(线程)间,保持数据内容一致性的机制。
下图展示两个 goroutine 如何利用无缓冲的通道来共享一个值:
有缓冲的channel
有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个数据值的通道。这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也不同。
只有通道中没有要接收的值时,接收动作才会阻塞。
只有通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。
这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。
示例图如下:
有缓冲的 channel 创建格式:
make(chan Type, capacity)
如果给定了一个缓冲区容量,通道就是异步的。只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞地进行。
借助函数 len(ch) 求取缓冲区中剩余元素个数,cap(ch) 求取缓冲区元素容量大小。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 3) //有缓冲的channel
fmt.Println("len(c) = ", len(c), ", cap(c) = ", cap(c))
go func() {
defer fmt.Println("子go程结束")
for i := 0; i < 3; i++ {
c <- i
fmt.Println("子go程正在运行:发送的元素=", i, ",len(c) = ", len(c), ", cap(c) = ", cap(c))
}
}()
time.Sleep(2 * time.Second)
for i := 0; i < 3; i++ {
num := <-c
fmt.Println("num = ", num)
}
fmt.Println("main 结束")
}
len(c) = 0 , cap(c) = 3
子go程正在运行:发送的元素= 0 ,len(c) = 1 , cap(c) = 3
子go程正在运行:发送的元素= 1 ,len(c) = 2 , cap(c) = 3
子go程正在运行:发送的元素= 2 ,len(c) = 3 , cap(c) = 3
子go程结束
num = 0
num = 1
num = 2
main 结束
关闭channel
如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现。
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//close可以关闭一个channel
close(c)
}()
for {
//ok如果为true表示channel没有关闭,如果为false表示channel已经关闭
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("Main Finished ...")
}
0
1
2
3
4
Main Finished ...
如果子go程不关闭,会产生死锁。
注意:
- channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;
- 关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
- 关闭channel后,可以继续从channel接收数据;
- 对于nil channel,无论收发都会被阻塞。
range
可以使用 range 来迭代不断操作channel:
for data := range c {
fmt.Println(data)
}
单向channel
默认情况下,通道channel是双向的,也就是,既可以往里面发送数据也可以同里面接收数据。但是,我们经常见一个通道作为参数进行传递而只希望对方是单向使用的,要么只让它发送数据,要么只让它接收数据,这时候我们可以指定通道的方向。
单向 channel 变量的声明非常简单,如下:
var ch1 chan int // ch1是一个正常的channel,是双向的
var ch2 chan <- float64 // ch2是单向channel,只用于写float64数据
var ch3 <- chan int // ch3是单向channel,只用于读int数据
- chan <- 表示数据进入管道,要把数据写进管道,对于调用者就是输出。
- <- chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入。
可以将 channel 隐式转换为单向队列,只收或只发,不能将单向 channel 转换为普通 channel。
select
Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。
有时候我们希望能够借助channel发送或接收数据,并避免因为发送或者接收导致的阻塞,尤其是当channel没有准备好写或者读时。select语句就可以实现这样的功能。
select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。
与switch语句相比,select有比较多的限制,其中最大的一条限制就是每个 case 语句里必须是一个IO操作,大致的结构如下:
select {
case <- chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
在一个select语句中,Go语言会按顺序从头至尾评估每一个发送和接收的语句。
如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。
如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:
- 如果给出了default语句,那么就会执行default语句,同时程序的执行会从select语句后的语句中恢复。
- 如果没有default语句,那么select语句将被阻塞,直到至少有一个通信可以进行下去。
package main
import "fmt"
func fibonacii(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
//如果c可写,则该case就会进来
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 < 6; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacii(c, quit)
}
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
$ go env
GOPATH="/Users/liuwq/go"
...
我们输入go env命令行后可以查看到 GOPATH 变量的结果,我们进入到该目录下进行查看,如下:
go
├── bin
├── pkg
└── src
├── github.com
├── golang.org
├── google.golang.org
├── gopkg.in
....
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 命令来进行查看:
$ go env
GO111MODULE="auto"
GOPROXY="https://proxy.golang.org,direct"
GONOPROXY=""
GOSUMDB="sum.golang.org"
GONOSUMDB=""
GOPRIVATE=""
...
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 模块代理列表,允许设置多个模块代理。
$ 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。并且它们的值都是一个以英文逗号 “,” 分割的模块路径前缀,也就是可以设置多个,例如:
$ go env -w GOPRIVATE="git.example.com,github.com/eddycjy/mquote"
设置后,前缀为 git.xxx.com 和 github.com/eddycjy/mquote 的模块都会被认为是私有模块。如果不想每次都重新设置,我们也可以利用通配符,例如:
$ go env -w GOPRIVATE="*.example.com"
这样子设置的话,所有模块路径为 example.com 的子域名(例如:git.example.com)都将不经过 Go module proxy 和 Go checksum database,需要注意的是不包括 example.com 本身。
使用Go Modules初始化项目
开启Go Modules
$ go env -w GO111MODULE=on
初始化项目
创建项目目录
$ mkdir -p /Users/liuwq/Documents/GoProjects/modules_test
$ cd modules_test
执行 Go modules 初始化
$ go mod init github.com/liuwq/modules_test
go: creating new go.mod: module github.com/liuwq/modules_test
在执行 go mod init 命令时,我们指定了模块导入路径为 github.com/aceld/modules_test。接下来我们在该项目根目录下创建 main.go 文件,如下:
package main
import (
"fmt"
"github.com/aceld/zinx/znet"
"github.com/aceld/zinx/ziface"
)
//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()
}
OK,我们先不要关注代码本身,我们看当前的 main.go 也就是我们的liuwq/modules_test项目,是依赖一个叫github.com/aceld/zinx库的。 znet和ziface只是zinx的两个模块。
接下来我们在本项目的根目录执行:
$ go get github.com/aceld/zinx/znet
$ go get github.com/aceld/zinx/ziface
我们会看到 我们的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情况
github.com/aceld/zinx v1.1.21 h1:8zoZ+hcEAd7gDsl8xOKPaWPEs9vZDRQOvhjG3vuvAnQ=
- go.mod hash 情况
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 的情况。
修改模块的版本依赖关系
$ go mod edit -replace=zinx@v0.0.0-20200306023939-bc416543ae24=zinx@v0.0.0-20200221135252-8a8954e75100
实战-即时通信系统
构建基础Server
main.go
package main
func main() {
server := NewServer("127.0.0.1", 8888)
server.Start()
}
server.go
server类型
type Server struct {
Ip string
Port int
}
方法
创建一个server对象
// 创建一个Server接口
func NewServer(ip string, port int) *Server {
server := &Server{
Ip: ip,
Port: port,
}
return server
}
启动Server服务
// 启动服务器接口
func (this *Server) Start() {
//socket listen
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.Ip, this.Port))
if err != nil {
fmt.Println("net.Listen err: ", err)
return
}
//close listen socket
defer listener.Close()
for {
//accept
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener accept err:", err)
continue
}
//do handler
go this.Handler(conn)
}
}
处理链接业务
func (this *Server) Handler(conn net.Conn) {
// ...当前链接的业务
fmt.Println("连接建立成功~")
}
测试
- 编译
go build -o server main.go server.go
- 启动服务器
./server
- 模拟
nc 127.0.0.1 8888
- 结果
用户上线功能
架构
user.go
user类型
type User struct {
Name string
Addr string
C chan string
conn net.Conn
}
方法
创建一个user对象
// 创建一个用户的API
func NewUser(conn net.Conn) *User {
userAddr := conn.RemoteAddr().String()
user := &User{
Name: userAddr,
Addr: userAddr,
C: make(chan string),
conn: conn,
}
//启动监听当前User channel消息的goroutine
go user.ListenMessage()
return user
}
监听user对应的channel消息
// 监听当前User channel的方法,一旦有消息,就直接发送给客户端
func (this *User) ListenMessage() {
for {
msg := <-this.C
this.conn.Write([]byte(msg + "\n"))
}
}
server.go
server类型
新增OnlineMap和Message属性
type Server struct {
Ip string
Port int
// 在线用户列表
OnlineMap map[string]*User
mapLock sync.RWMutex
// 消息广播的channel
Message chan string
}
// 创建一个Server接口
func NewServer(ip string, port int) *Server {
server := &Server{
Ip: ip,
Port: port,
OnlineMap: make(map[string]*User),
Message: make(chan string),
}
return server
}
在处理客户端上线的Handler创建并添加用户
func (this *Server) Handler(conn net.Conn) {
// ...当前链接的业务
user := NewUser(conn)
// 用户上线,将用户加入到OnlineMap中
this.mapLock.Lock()
this.OnlineMap[user.Name] = user
this.mapLock.Unlock()
// 广播当前用户上线消息
this.BroadCast(user, "已上线")
// 当前handler阻塞
select {}
}
新增广播消息方法
// 广播消息的方法
func (this *Server) BroadCast(user *User, msg string) {
sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg
this.Message <- sendMsg
}
新增监听广播消息channel方法
// 监听Message广播消息channel的goroutine,一旦有消息,就发送给在线的所有User
func (this *Server) ListenMessage() {
for {
msg := <-this.Message
//将msg发送给全部的在线User
this.mapLock.Lock()
for _, cli := range this.OnlineMap {
cli.C <- msg
}
this.mapLock.Unlock()
}
}
⽤一个goroutine单独监听Message
//启动监听message的goroutine
go this.ListenMessage()
测试
- 编译并运行
go build -o server main.go server.go user.go
./server
- 启动多个客户端
nc 127.0.0.1 8888
- 结果
用户消息广播机制
server.go
完善handler处理业务方法,启动一个针对当前客户端的读goroutine。
// 接受客户端发送的消息
go func() {
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if n == 0 {
this.BroadCast(user, "下线")
return
}
if err != nil && err != io.EOF {
fmt.Println("Conn Read err:", err)
return
}
//提取用户的消息(去除'\n')
msg := string(buf[:n-1])
//将得到的消息进行广播
this.BroadCast(user, msg)
}
}()
测试
启动服务后,开启多个客户端进行测试
用户业务层封装
user.go
user类型新增server关联
type User struct {
Name string
Addr string
C chan string
conn net.Conn
server *Server
}
// 创建一个用户的API
func NewUser(conn net.Conn, server *Server) *User {
userAddr := conn.RemoteAddr().String()
user := &User{
Name: userAddr,
Addr: userAddr,
C: make(chan string),
conn: conn,
server: server,
}
//启动监听当前User channel消息的goroutine
go user.ListenMessage()
return user
}
新增Online方法
// 用户的上线业务
func (this *User) Online() {
// 用户上线,将用户加入到OnlineMap中
this.server.mapLock.Lock()
this.server.OnlineMap[this.Name] = this
this.server.mapLock.Unlock()
// 广播当前用户上线消息
this.server.BroadCast(this, "已上线")
}
新增Offline方法
// 用户的下线业务
func (this *User) Offline() {
// 用户下线,将用户从OnlineMap删除
this.server.mapLock.Lock()
delete(this.server.OnlineMap, this.Name)
this.server.mapLock.Unlock()
// 广播当前用户下线消息
this.server.BroadCast(this, "下线")
}
新增DoMessage方法
// 用户处理消息的业务
func (this *User) DoMessage(msg string) {
//将得到的消息进行广播
this.server.BroadCast(this, msg)
}
server.go
将之前user的业务进行替换
在线用户查询
消息格式“who”
user.go
- 提供SendMsg向对象客户端发送消息API
func (this *User) SendMsg(msg string) {
this.conn.Write([]byte(msg))
}
- 在
DoMessage()方法中,加上对“who”指令的处理,返回在线用户信息
// 用户处理消息的业务
func (this *User) DoMessage(msg string) {
if msg == "who" {
//查询当前在线用户都有哪些
this.server.mapLock.Lock()
for _, user := range this.server.OnlineMap {
onlineMsg := "[" + user.Addr + "]" + user.Name + ":" + "在线...\n"
this.SendMsg(onlineMsg)
}
this.server.mapLock.Unlock()
} else {
//将得到的消息进行广播
this.server.BroadCast(this, msg)
}
}
修改用户名
消息格式“rename|小新”
user.go
在DoMessage()方法中,加上对“rename|小新”指令的处理,返回在线用户信息
// 用户处理消息的业务
func (this *User) DoMessage(msg string) {
if msg == "who" {
//查询当前在线用户都有哪些
this.server.mapLock.Lock()
for _, user := range this.server.OnlineMap {
onlineMsg := "[" + user.Addr + "]" + user.Name + ":" + "在线...\n"
this.SendMsg(onlineMsg)
}
this.server.mapLock.Unlock()
} else if len(msg) > 7 && msg[:7] == "rename|" {
//消息格式:rename|小新
newName := strings.Split(msg, "|")[1]
//判断name是否存在
_, ok := this.server.OnlineMap[newName]
if ok {
this.SendMsg("用户名已存在!")
} else {
this.server.mapLock.Lock()
delete(this.server.OnlineMap, this.Name)
this.server.OnlineMap[newName] = this
this.server.mapLock.Unlock()
this.Name = newName
this.SendMsg("您已经更新用户名为:" + this.Name + "\n")
}
} else {
//将得到的消息进行广播
this.server.BroadCast(this, msg)
}
}
测试
超时强踢功能
用户的任意消息表示为用户活跃,长时间不发消息则认为超时,就要强制关闭用户连接。
server.go
- 在用户的
Hander()goroutine 中,添加用户活跃channel,一旦有消息,就向该channel发送数据
- 在用户的
Hander()goroutine 中,添加定时器功能,超时则强踢
// 当前handler阻塞
for {
select {
case <-isLive:
//当前用户是活跃的,应该重置定时器
//不做任何事情,为了激活select,更新下面的定时器
case <-time.After(time.Second * 10):
//已经超时
//将当前的User强制关闭
user.SendMsg("您已被踢~")
//销毁用的资源
close(user.C)
//关闭连接
conn.Close()
//退出当前的Handler
return
}
}
测试
私聊功能
消息格式“to|小新|你好...”
user.go
在DoMessage()方法中,加上对“to|小新|你好...”指令的处理,返回在线用户信息
} else if len(msg) > 5 && msg[:3] == "to|" {
//消息格式:to|小新|你好...
//获取对方用户名
remoteName := strings.Split(msg, "|")[1]
if remoteName == "" {
this.SendMsg("消息格式不正确,请使用"to|小新|你好呀"格式。\n")
return
}
//根据用户名,得到对方的User对象
remoteUser, ok := this.server.OnlineMap[remoteName]
if !ok {
this.SendMsg("发送对象的用户名不存在\n")
return
}
//获取消息内容,通过对方的User对象将消息内容发送过去
content := strings.Split(msg, "|")[2]
if content == "" {
this.SendMsg("消息内容为空,请重新发送!\n")
return
}
remoteUser.SendMsg(this.Name + "对您说:" + content)
}
测试
客户端实现
客户端类型定义与链接
type Client struct {
ServerIp string
ServerPort int
Name string
conn net.Conn
}
func NewClient(serverIp string, serverPort int) *Client {
//创建客户端对象
client := &Client{
ServerIp: serverIp,
ServerPort: serverPort,
}
//连接服务器
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIp, serverPort))
if err != nil {
fmt.Println("net.Dial error:", err)
return nil
}
client.conn = conn
//返回对象
return client
}
func main() {
client := NewClient("127.0.0.1", 8888)
if client == nil {
fmt.Println(">>>>>>连接服务器失败>>>>>>")
return
}
fmt.Println(">>>>>>连接服务器成功>>>>>>")
//启动客户端的业务
select {}
}
解析命令行
init函数初始化命令行参数
var serverIp string
var serverPort int
// ./client -ip 127.0.0.1
func init() {
flag.StringVar(&serverIp, "ip", "127.0.0.1", "设置服务器IP地址(默认是127.0.0.1)")
flag.IntVar(&serverPort, "port", 8888, "设置服务器端口(默认为8888)")
}
main函数解析命令行
//解析命令行
flag.Parse()
client := NewClient(serverIp, serverPort)
测试
菜单显示
client新增flag属性
新增menu()方法,获取用户输入的模式
func (client *Client) menu() bool {
var flag int
fmt.Println("1.公聊模式")
fmt.Println("2.私聊模式")
fmt.Println("3.更新用户名")
fmt.Println("0.退出")
fmt.Scanln(&flag)
if flag >= 0 && flag <= 3 {
client.flag = flag
return true
} else {
fmt.Println(">>>>>>请输入合法的范围内数字>>>>>>")
return false
}
}
新增Run()主业务循环
func (client *Client) Run() {
for client.flag != 0 {
for client.menu() != true {
}
//根据不同的模式处理不同的业务
switch client.flag {
case 1:
//公聊模式
fmt.Println("公聊模式选择...")
case 2:
//私聊模式
fmt.Println("私聊模式选择...")
case 3:
//更新用户名
fmt.Println("更新用户名选择...")
break
}
}
}
main()中调用Run()
//启动客户端的业务
client.Run()
更新用户名
新增UpdateName()更新用户名
func (client *Client) UpdateName() bool {
fmt.Println(">>>>>>请输入用户名")
fmt.Scanln(&client.Name)
sendMsg := "rename|" + client.Name + "\n"
_, err := client.conn.Write([]byte(sendMsg))
if err != nil {
fmt.Println("conn.Write err:", err)
return false
}
return true
}
加入到Run业务分支中
case 3:
//更新用户名
client.UpdateName()
break
}
添加处理server回执消息方法DealResponse()
// 处理server回应的消息,直接显示输出即可
func (client *Client) DealResponse() {
//一旦client.conn有数据,就直接copy到stdout标准输出上,永久阻塞监听
io.Copy(os.Stdout, client.conn)
}
开启一个go程,去承载DealResponse()
//单独开启一个goroutine去处理server的回执消息
go client.DealResponse()
测试
公聊模式
新增PublicChat()公聊模式业务
func (client *Client) PublicChat() {
//提示用户输入消息
var chatMsg string
fmt.Println(">>>>>>请输入聊天内容,exit退出.")
fmt.Scanln(&chatMsg)
for chatMsg != "exit" {
//发送服务器
if len(chatMsg) != 0 {
sendMsg := chatMsg + "\n"
_, err := client.conn.Write([]byte(sendMsg))
if err != nil {
fmt.Println("conn Write err:", err)
break
}
}
chatMsg = ""
fmt.Println(">>>>>>请输入聊天内容,exit退出.")
fmt.Scanln(&chatMsg)
}
}
加入到Run的分支中
case 1:
//公聊模式
client.PublicChat()
break
测试
私聊模式
查询当前都有哪些用户在线
func (client *Client) SelectUsers() {
sendMsg := "who\n"
_, err := client.conn.Write([]byte(sendMsg))
if err != nil {
fmt.Println("conn Write err:", err)
return
}
}
新增私聊模式业务
func (client *Client) PrivateChat() {
var remoteName string
var content string
client.SelectUsers()
fmt.Println(">>>>>>请输入聊天对象[用户名],exit退出.")
fmt.Scanln(&remoteName)
for remoteName != "exit" {
fmt.Println(">>>>>>请输入消息内容,exit退出.")
fmt.Scanln(&content)
for content != "exit" {
//消息不为空则发送
if len(content) != 0 {
sendMsg := "to|" + remoteName + "|" + content + "\n\n"
_, err := client.conn.Write([]byte(sendMsg))
if err != nil {
fmt.Println("conn Write err:", err)
break
}
}
content = ""
fmt.Println(">>>>>>请输入消息内容,exit退出.")
fmt.Scanln(&content)
}
client.SelectUsers()
fmt.Println(">>>>>>请输入聊天对象[用户名],exit退出.")
fmt.Scanln(&remoteName)
}
}
添加到Run业务分支
case 2:
//私聊模式
client.PrivateChat()