Golang基础
Golang基础
一、Go语言
0.Go前景
- 云计算基础设施领域
代表项目:docker、kubernetes、etcd、consul、cloudflare CDN、七牛云存储等。 - 基础软件
代表项目:tidb、influxdb、cockroachdb 等。 - 微服务
代表项目:go-kit、micro、monzo bank 的 typhon、bilibili 等。 - 互联网基础设施
代表项目:以太坊、hyperledger 等。
Go框架组件:
| Web | |
|---|---|
| beego | 国内开发,文档比较全 |
| gin | 主流web框架,轻量级,高性能 |
| echo | 类似于gin |
| Iris | 重量级,高性能 |
| 微服务 | |
|---|---|
| go kit | 一套集成组合方案,包含很多工具,比较灵活。 |
| Istio | 一体化集成式框架,包括熔断降级、安全审计等一整套方案,适合比较繁琐的大型微服务项目。 |
| Tcp长链接 | |
|---|---|
| zinx | 轻量级服务器 |
| 容器编排 | |
|---|---|
| kubernetes | google研发 |
| swarm | docker团队研发 |
| 服务发现 | |
|---|---|
| consul | 服务发现与注册 |
| 存储引擎 | |
|---|---|
| etcd | k/v存储,类似于redis,支持分布式存储 |
| tidb | 国内开发,分布式sql存储 |
| 静态建站 | |
|---|---|
| hugo | 轻松创建门户网站 |
| 中间件 | |
|---|---|
| nsq | 消息队列 |
| leaf | 游戏服务器 |
| grpc | RPC框架,主要功能远程调度 |
| codis | redis集群 |
| 爬虫 | goquery |
|---|
1.Go基础
安装:源码、标准包(推荐)、三方工具。如果想在一个系统安装多版本Go,推荐GVM。
1.1 Go组成
Go 语言的基础组成有以下几个部分:
1 | package main //包声明 每个Go应用程序都包含一个名为 main 的包,main方法必须在main包中运行 |
1 | $ go run xxx.go //运行以上代码的命令 |
行分隔符:在 Go 程序中,一行代表一个语句结束。不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。
字符串连接:通过 + 实现
1 | package main |
go语言关键字:
1 | break default func interface select case defer go map |
1.2 Go命令
1 | go get 动态获取远程包(github) // 第一步是下载源码包,第二步是执行go install |
1 | $ go run helloworld.go //编译一个或多个以.go结尾的源文件,链接库文件,生成可执行文件helloworld.exe |
1 | go tool: |
Go语言原生支持Unicode,它可以处理全世界任何语言的文本。
自动添加import声明:
goimports,可以根据代码需要,自动地添加或删除import声明。这个工具并没有包含在标准的分发包中,可以用下面的命令安装:
1 | $ go get golang.org/x/tools/cmd/goimports |
1.3 命令行参数
os包以跨平台的方式,提供了一些与操作系统交互的函数和变量。
程序的命令行参数可从os包的Args变量获取;os包外部使用os.Args访问该变量。
使用一条短变量声明来声明并初始化s:
1 | // 以下四条等同 |
1.4 类型
类型转换
假设x为int类型,将x转为byte类型:
1 | byte(x) |
标准库strconv可在不同进制(字符串)间转换。
1 | import "strconv" |
输出:
1 | 100 100 100 |
引用类型
引用类型(reference type)特指slice、map、channel这三种预定义类型。
相比数字、数组等类型,引用类型拥有更复杂的存储结构。除分配内存外,它们还须初始化一系列属性,诸如指针、长度,甚至包括哈希分布、数据队列等。
内置函数new按指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式。而引用类型则必须使用make函数创建,编译器会将make转换为目标类型专用的创建函数(或指令),以确保完成全部内存分配和相关属性初始化。
2.Go 语法
2.1 变量
运行时内存分配操作会确保变量自动初始化为二进制零值(zero value),避免出现不可预测行为
声明变量的一般形式是使用 var 关键字:
1 | var identifier type |
可以一次声明多个变量:
1 | var identifier1, identifier2 type |
:= 是一个简短变量声明和初始化的操作符:
一行代码中声明一个新的变量,并同时给它赋值;
但是,这种不带声明格式的只能在函数体中出现!!!
注意!!!:如果变量已经存在并且在同一作用域内,使用:=会导致编译错误,因为它会尝试声明一个已存在的变量。另一种情况,当变量在函数体外被全局声明,再次在函数体内声明时会覆盖之前的变量值。
1 | intVal := 1 |
四种声明方法:
1 | //类型相同多个变量, 非全局变量 |
交换两个变量的值
1 | a, b = b, a |
2.2 常量
常量的定义格式:
1 | const identifier [type] = value |
优雅的常量 iota:
自增长:
1 | const ( |
2.3 控制和函数
2.3.1 流程控制
if有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内。
1 | // 计算获取值x,然后根据x返回的大小,判断是否大于10。 |
goto跳转到必须在当前函数内定义的标签。
1 | func myFunc() { |
_下划线:
由于 Go 支持 “多值返回”, 而对于“声明而未被调用”的变量, 编译器会报错, 在这种情况下, 可以使用_来丢弃不需要的返回值。
1 | for _, v := range map{ |
switch:
Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代码。
2.3.2 函数
通过关键字func来声明。
1 | func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) { |
关键字
func用来声明一个函数funcName函数可以有一个或者多个参数,每个参数后面带有类型,通过
,分隔函数可以返回多个值
上面返回值声明了两个变量
output1和output2,如果你不想声明也可以,直接就两个类型如果只有一个返回值且不声明返回值变量,那么你可以省略 包括返回值 的括号
如果没有返回值,那么就直接省略最后的返回信息
如果有返回值, 那么必须在函数的外层添加return语句
如果函数有两个参数,它们的类型都是int,那么第一个变量的类型可以省略(即 a,b int,而非 a int, b int),默认为离它最近的类型
i. 返回多个值:
Go语言比C更先进的特性,其中一点就是函数能够返回多个值。
例子:
1 | package main |
上面的例子直接返回了两个参数,当然也可以命名返回参数的变量,这样提高生成的文档可读性。
改成如下这样的定义,然后返回的时候不用带上变量名,因为直接在函数里面初始化了。
1 | func SumAndProduct(A, B int) (add int, Multiplied int) { |
ii. 变参:
接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参:
1 | func myfunc(arg ...int) {} |
arg ...int告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是int。在函数体中,变量arg是一个int的slice:
1 | for _, n := range arg { |
iii. 传值与传指针
当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。
需要传这个参数本身,该怎么办呢?
这就牵扯到了所谓的指针。变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。只有add1函数知道x变量所在的地址,才能修改x变量的值。所以我们需要将x所在地址&x传入函数,并将函数的参数的类型由int改为*int,即改为指针类型,才能在函数中修改x变量的值。此时参数仍然是按copy传递的,只是copy的是一个指针。
1 | //简单的一个函数,实现了参数+1的操作 |
优点:
- 传指针使得多个函数能操作同一个对象。
- 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。
- Go语言中
channel,slice,map这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变slice的长度,则仍需要取地址传递指针)
iv. Defer:
defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数。
在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。
特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。
没有defer:
1 | func ReadWrite() bool { |
有defer:
1 | func ReadWrite() bool { |
defer作用:
- 释放占用的资源
- 捕捉处理异常
- 输出日志
结果
如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行。
1 | func Demo(){ |
★return先被调用、defer后被调用;即return之后的语句先执行,defer之后的语句后执行。
v. 函数作为值、类型
1 | type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...]) |
把这个类型的函数当做值来传递
1 | type testInt func(int) bool // 声明了一个函数类型,类型名为testInt,输入类型为int,返回类型为bool |
函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到testInt这个类型是一个函数类型,然后两个filter函数的参数和返回值与testInt类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。
vi. Panic和Recover
Go没有像Java那样的异常机制,它不能抛出异常,是使用了panic和recover机制。一定要记住,你应当把它作为最后的手段来使用。
Panic(恐慌)
是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数
F调用panic,函数F的执行被中断,但是F中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。这一过程继续向上,直到发生panic的goroutine中所有调用的函数返回,此时程序退出。恐慌可以直接调用panic产生。也可以由运行时错误产生,例如访问越界的数组。
Recover
是一个内建的函数,可以让进入令人恐慌的流程中的
goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine陷入恐慌,调用recover可以捕获到panic的输入值,并且恢复正常的执行。
1 | var user = os.Getenv("USER") |
下面这个函数检查作为其参数的函数在执行时是否会产生panic:
1 | func throwsPanic(f func()) (b bool) { |
vii. main函数和init函数
一张图全部解析。
main函数引入包初始化流程图:

viii. import
- 点操作
我们有时候会看到如下的方式导入包
1 | import( |
这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println(“hello world”)可以省略的写成Println(“hello world”)
- 别名操作
别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字
1 | import( |
别名操作的话调用包函数时前缀变成了我们的前缀,即f.Println(“hello world”)
- _操作
这个操作经常是让很多人费解的一个操作符,请看下面这个import
1 | import ( |
_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。
2.4 slice和map
Go 语言切片是对数组的抽象。
2.5.1 slice
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),即slice,但实际slice是一个引用类型。与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
定义切片:
你可以声明一个未指定大小的数组来定义切片:
1 | var identifier []int |
slice:
1 | s :=[]int{1,2,3 } // 切片初始化。 数组中字符要加'',数字不用 |
slice可以从一个数组或一个已经存在的slice中再次声明。slice通过array[i:j]来获取,其中i是数组的开始位置,j是结束位置,但不包含array[j],它的长度是j-i。
1 | slice := []byte {'a', 'b', 'c', 'd'} |
1 | // 声明一个数组 |
备注:cap函数只计算左指针到原array最后的值的个数。假设array[3:7],左指针为3,那么cap的容量就是3-10,为7。(左闭右开)
cap指切片的容量或数组的长度。
直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3

slice像一个结构体,这个结构体包含了三个元素:
一个指针,指向数组中
slice指定的开始位置长度,即
slice的长度(len)最大长度,也就是
slice开始位置到数组的最后位置的长度(cap)实际存储结构图:

切片的追加与截取:
len获取slice的长度cap获取slice的最大容量append向slice里面追加一个或者多个元素,然后返回一个和slice一样类型的slicecopy函数copy从源slice的src中复制元素到目标dst,并且返回复制的元素的个数
1 | slice = array[2:4:7] |
2.5.2 map
map和slice数据结构不同。map的使用方式:引用传递。map是无序的。
1 | map[keyType]valueType // 格式 |
1 | package main |
2.5 struct
我们可以声明新的类型,作为其它类型的属性或字段的容器。例如,我们可以创建一个自定义类型person代表一个人的实体。这个实体拥有属性:姓名和年龄。这样的类型我们称之struct。
1 | type person struct { |
- 一个string类型的字段name,用来保存用户名称这个属性
- 一个int类型的字段age,用来保存用户年龄这个属性
1 | var P person // P现在就是person类型的变量了 |
除了上面这种P的声明使用之外,还有另外几种声明使用方式:
1.按照顺序提供初始化值
P := person{“Tom”, 25}2.通过
field:value的方式初始化,这样可以任意顺序P := person{age:24, name:”Tom”}3.当然也可以通过
new函数分配一个指针,此处P的类型为*personP := new(person)
struct的匿名字段
当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。
能够实现字段的继承。也称为嵌入字段。
1 | type Human struct { |
问题:如果human里面有一个字段叫做phone,而student也有一个字段叫做phone,那么该怎么办呢?
Go里面很简单的解决了这个问题,最外层的优先访问,也就是当你通过student.phone访问的时候,是访问student里面的字段,而不是human里面的字段。
1 | fmt.Println("Bob's work phone is:", Bob.phone) |
2.6 面向对象方法
函数的另一种形态,带有接收者的函数,我们称为method
method是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在func后面增加了一个receiver(也就是method所依从的主体)。
语法:
1 | func (r ReceiverType) funcName(parameters) (results) |
- 虽然method的名字一模一样,但是如果接收者不一样,那么method就不一样
- method里面可以访问接收者的字段
- 调用method通过
.访问,就像struct里面访问字段一样
指针:Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。
一般解决问题都是通过问题的描述,去写相应的代码实现。
指针作为receiver
一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method。
1 | //下面代码将int用另一种声明代替,之后在声明int型时可以使用myint |
method继承
Go继承:子类继承父类,添加属性。
method也是可以继承的。如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。
1 | type Human struct { |
method重写
上面的例子中,如果Employee想要实现自己的SayHi,怎么办?简单,和匿名字段冲突一样的道理,我们可以在Employee上面定义一个method,重写了匿名字段的方法。
1 | //Human定义method |
2.7 interface
多态:一定有父类(有接口);有子类(实现了父类的全部接口);父类类型的变量(指针)指向(引用)子类的具体数据变量
通用万能类型:interface{},空接口。,任意的类型都实现了空interface,也就是包含0个method的interface。
interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。
interface就是一组抽象方法的集合,它必须由其他非interface类型实现,而不能自我实现, Go通过interface实现了duck-typing:即”当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子”。
interface变量存储的类型
我们知道interface的变量里面可以存储任意类型的数值(该类型实现了interface)。那么我们怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?目前常用的有两种方法:
- Comma-ok断言
Go语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。
如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。
- switch测试
嵌入interface
如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。
2.8 反射
所谓反射就是能检查程序在运行时的状态。
要去反射是一个类型的值(这些值都实现了空interface),首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数)。这两种获取方式如下:
1 | t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素 |
转化为reflect对象之后我们就可以进行一些操作了,也就是将reflect对象转化成相应的值,例如
1 | tag := t.Elem().Field(0).Tag //获取定义在struct里面的标签 |
获取反射值能返回相应的类型和数值
1 | var x float64 = 3.4 |
最后,反射的话,那么反射的字段必须是可修改的,
1 | var x float64 = 3.4 |
动态推测并获取数据的类型(type);
反射包:report
反射解析结构体标签Tag:
结构体标签在JSON中的使用:
3. Gorountine
3.1 介绍
多进程造成:高消耗调度cpu、高内存占用。
协程(co-rounine)介绍:
- 将线程一分为二:用户线程 + 内核线程(绑定),1:1
- (进阶)协程调度器,连接多个协程,1:n
Gorountine:
GoProcessor的数量就是Go并行的数量。
特点:
复用线程:
- work stealing机制,当有线程(a)的本地队列有等待,而有线程(b)的本地队列无等待时,b会偷取a的一个队列成员。
- hand off机制,
利用并行:
抢占:
全局G队列: 在work stealing机制中,如果其他本地队列都没有,某空队列会尝试去全局队列偷取。
3.2 创建
只需在函数调⽤语句前添加 go 关键字,就可创建并发执⾏单元。开发⼈员无需了解任何执⾏细节,调度器会自动将其安排到合适的系统线程上执行。
通过return返回是无法拿到函数布尔值bool,所以需要通信通道channel
1 | runtime.Goexit() //退出当前的协程(goroutine) |
4. Channel
4.1定义
Channel:管道 ➡ 用来通信 通过它并发核心单元就可以发送或者接收数据进行通讯
1 | // channel的定义,先创建再使用 操作符: <- |
capacity:当参数capacity**= 0** 时,channel 是无缓冲阻塞读写的;当capacity > 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity个元素才阻塞写入。(检查通道是否关闭)
1 | channel <- value //发送value到channel ex. channel <- 666 |
通过阻塞,同步读取
channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得Goroutines同步变的更加的简单,而不需要显式的lock。
所谓阻塞,也就是如果读取(value := <-ch)它将会被阻塞,直到有数据接收。其次,任何发送(ch<-5)将会被阻塞,直到数据被读出。无缓冲channel是在多个goroutine之间同步很棒的工具。
4.2 缓冲和无缓冲同步问题
无缓冲channel
ch:= make(chan bool, 4),创建了可以存储4个元素的bool 型channel。在这个channel 中,前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞,直到其他goroutine从channel 中读取一些元素,腾出空间。
当 value = 0 时,channel 是无缓冲阻塞读写的,当value > 0 时,channel 有缓冲、是非阻塞的,直到写满 value 个元素才阻塞写入。
两个goRoutine都到达通道,但那个都没有开始执行发送或者接收。
执行发送的goRoutine将手伸进通道,如果没人接收,这个goRoutine就会被锁住。
执行接收的goRoutine将手伸进通道,如果没人发送,这个goRoutine也会被锁住。
两者都将手伸进通道,并进行交换后,两个goRoutine才会将手收回,也就是被锁住的goRoutine得到了释放,才可以继续执行后续的流程。

有缓冲channel(即设定容量的channel)
- 发送和接收数据的goRoutine互不影响,均可独立完成发送和接收。即容量满之前没有阻塞限制。
- 若盒子满了,则发送数据的goRoutine会阻塞,直到数据被接收取走,才可继续发送数据。
关闭channel特点:
1 | close() // 关闭一个channel |
l channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;
l 关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
l 关闭channel后,可以继续从channel接收数据;
l 对于nil channel,无论收发都会被阻塞。
4.3 range
可以通过range读取多次c,像操作slice或者map一样操作缓存类型的channel。
1 | func main() { |
4.4 select
通过select可以监听channel上的数据流动,与switch语句相比,select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作。
当多个channel都准备好的时候,select是随机的选择一个执行的。
1 | select { |
select里面还有default语法,select其实就是类似switch的功能,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)。
超时
有时候会出现goroutine阻塞的情况,那么我们如何避免整个程序进入阻塞的情况呢?我们可以利用select来设置超时。
runtime goroutine
runtime包中有几个处理goroutine的函数:
Goexit
退出当前执行的goroutine,但是defer函数还会继续调用
Gosched
让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
NumCPU
返回 CPU 核数量
NumGoroutine
返回正在执行和排队的任务总数
GOMAXPROCS
用来设置可以并行计算的CPU核数的最大值,并返回之前的值。
超时强踢:
1 | for { |
5. Go Modules
Go语言的依赖解决方案。淘汰了Gopath的使用模式。Go moudles 目前集成在 Go 的工具链中,只要安装了 Go,自然而然也就可以使用 Go moudles 了。
1 | go env //查看环境变量 |
GOPATH的弊端:
A. 无版本控制概念. 在执行
go get的时候,你无法传达任何的版本信息的期望,也就是说你也无法知道自己当前更新的是哪一个版本,也无法通过指定来拉取自己所期望的具体版本。B.无法同步一致第三方版本号. 在运行 Go 应用程序的时候,你无法保证其它人与你所期望依赖的第三方库是相同的版本,也就是说在项目依赖库的管理上,你无法保证所有人的依赖版本都一致。
C.无法指定当前项目引用的第三方版本号. 你没办法处理 v1、v2、v3 等等不同版本的引用问题,因为 GOPATH 模式下的导入路径都是一样的,都是
github.com/foo/bar。
5.1 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 | 查看为什么需要依赖某模块 |
(2)go mod环境变量
1 | go env |
GOPROXY
这个环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时直接通过镜像站点来快速拉取。
GOPROXY 的默认值是:https://proxy.golang.org,direct
proxy.golang.org国内访问不了,需要设置国内的代理.
1 | $ go env -w GOPROXY=https://goproxy.cn,direct |
5.2 初始化项目
改变模块依赖关系:
edit -replace
6. 即时通信系统
6.1 Sever构建
定义一个Server结构体,
1 | type Server struct |
创建一个Server接口,仅仅是一个接口
1 | func NewServer(ip string, port int) *Server |
启动服务器的接口,不仅是接口,还是Server结构体的成员方法
1 | func (s *Server) Start() |
此时就可以建立链接了。
分别建立server.go 和 main.go
1 | //main.go |
1 | //server.go |
写好代码后,在终端中
1 | $ go build -0 server.exe //编译 |
新建终端,链接server
1 | $ telnet 127.0.0.1 8888 //windows |
显示:
1 | 建立成功咯 |
6.2 用户上线功能

user1中,建立有关user的结构体User,并定义变量user。
1 | type User struct |
创建一个用户的API
1 | func NewUser(conn net.Conn) *User{} |
同时在API中,需要开启一个goroutine去监听channel,一旦有消息,就将消息发送给客户端。
1 | go user.ListenMessage() |
最后再写一个goroutine监听的方法。
server中,在Server结构体中加入OnlineMap(在线用户列表),再加入消息广播的Message,
在server接口中加入OnlineMap和Message(channnel)。在handler中用户上线后将用户加入OnlineMap中,将map上锁,将上线的user.Name传给Map表中的user,
创建Handler接受处理http请求,再广播当前用户上线的消息(需要一个广播消息的方法func,在此方法中将Message(“已上线”)发送给客户),handler需要阻塞,所以加入一个select{}。
需要一个goroutine方法不断监听广播消息的channel,有消息就发送给user,
然后在start()接口中加入启动go监听的代码。
gorountine:负责监听、发送
channel:负责通信
user.go
1 | package main |
server.go
1 | package main |
编写完成后,控制台输入
1 | $ go build -o server.exe main.go server.go user.go |
成功编译文件生成server.exe文件,再输入./server运行server
1 | $ ./server |
新建两个终端,先输入chcp 65001,修改编码为utf-8
1 | $ chcp 65001 |
再输入地址连接server
1 | $ telnet 127.0.0.1 8888 //telnet + 本机号 + 端口号 |

运行成功
遇到的问题:
1 | sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg |
在之前我将上述代码中user中的u写成了大写,导致运行出了些问题,在之后我必须严格按照之前定义的变量来使用
1 | userAddr := conn.RemoteAddr().String() //获取客户端连接地址 |
上述代码从User结构体中定义了一个user结构体变量。
6.3 用户消息广播
6.4 用户业务封装
6.4 在线用户查询
6.5 修改用户名
6.6 超时强踢功能
6.7 私聊功能
6.8 在线连接
6.9 命令行解析
6.10 菜单显示
6.11 更新用户名
6.12 公聊模式
6.13 私聊模式
二、标准库
1.1 sync
sync.Map 是适合于读多写少的场景。
sync.Map 的各个字段。
- mu:互斥锁,用于保障 read 和 dirty 的并发安全;
- read:可直接并发读的 map 数据,未存储全量数据;
- dirty:并发不安全的 map 数据,存储全量数据或为 nil (如果为 nil 时 read 存储全量数据);
- misses:未命中 read 的次数,用于记录是否需要将 dirty 转换为 read。
适合于写多的场景
对 map 进行分片,通过多个 map 降低写操作加锁阻塞的概率。
传入分片的数量和分片函数,然后我们将根据分片数量 N 初始化 N 个 sync.RWMutex 和 N 个 map。每个分片之间相互独立,当需要读或写操作时,先计算出对应的 key 在哪个分片上,然后再操作该分片。
1.2 os
stderr介绍:
stderr 是标准错误流(standard error stream),通常用于输出程序的错误信息。与 stdout(标准输出流)不同,stderr 通常用于显示程序运行时的错误消息、警告和其他诊断信息。
标准输出流 (stdout) 与标准错误流 (stderr)
- **
stdout**:标准输出流,通常用于输出程序的正常输出。默认情况下,这些输出会显示在终端上,但也可以被重定向到文件或其他地方。 - **
stderr**:标准错误流,通常用于输出程序的错误信息和调试信息。默认情况下,这些输出也会显示在终端上,但可以通过不同的方式处理,例如重定向到单独的日志文件。
在 Go 中使用 stderr
1 | func main() { |
抛出 Panic 并向 stderr 输出信息
在 Go 中抛出一个 panic 时,默认情况下,Go 运行时会捕获这个 panic 并将相关信息输出到 stderr。
1 | func main() { |
自定义 Panic 处理并输出到 stderr
自定义 panic 的处理逻辑,并且明确地将信息写入 stderr,可以使用 recover 函数来捕获 panic,并在 defer 语句中处理它。
1 | func main() { |
Go代码规范及意义
1. 错误数据处理
1 | if err != nil { |
2.命名规范
2.1 Go程序设计的一些规则
Go之所以会那么简洁,是因为它有一些默认的行为:
- 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公有变量;小写字母开头的就是不可导出的,是私有变量。
- 大写字母开头的函数也是一样,相当于
class中的带public关键词的公有函数;小写字母开头的就是有private关键词的私有函数。
2.2 Go是不是面向编程?
- Go 语言中,没有对象(但是开发时通常叫做对象),没有类,也没有继承,也不能直接使用多态。
- 它通过组合匿名字段的手段,来达到类似继承的效果。
- 它只能通过接口,来实现的多态,使一个类可以有不同的实现。
- 将一类事物,抽象成struct的过程,这才是封装的主要部分
- Go语言 sturct 方法的写法,和其他面向对象语言中 this 的本质很像。
x.VsCode使用Go
a.设置goproxy
GoProxy是一个由Go语言编写的代理服务器,它具有多种功能和特点。
首先,GoProxy是一个高效、安全的HTTP/HTTPS代理服务器,旨在帮助开发者在进行网络请求时实现数据的中继和过滤,从而增强安全性并提升开发效率。它的核心设计基于Go语言的并发模型(goroutines和channels),这使得它能够充分利用多核CPU,提供出色的性能表现。
1 | go env -w GOPROXY=https://goproxy.cn,direct //cmd控制台修改 |
go mod: 管理一切
1 | go mod init +项目名 //生成.mod程序 初始化项目 |
1 | go build //生成exe文件 |
改用cmd控制台
