基础篇可访问此链接:
基础篇1:https://blog.csdn.net/asd1358355022/article/details/127905011?spm=1001.2014.3001.5501
基础篇2:https://blog.csdn.net/asd1358355022/article/details/128039005?spm=1001.2014.3001.5501
文章目录
- GO语言类型断言
- 文件操作
- 打开、关闭、读取文件
- 创建、写入文件
- 判断文件是否是否存在
- 序列化、反序列化
- 命令行参数
- 命令行执行
- GoLand里面执行
- GO语言并发编程
- 协程goroutine
- 线程、进程、协程核心概念
- 特点
- Go 协程相比于线程的优势
- 代码示例
- sync.WaitGroup
- 锁
- 互斥锁
- 通道
- 管道(channel)
- select
- 定时器(Timer)
- Ticker
- GO超时检查
- 阻塞
- 超时控制
- select + time.After
- 释放标记
- 并发限制(限制并发数量)
- 并发时的原子操作
- 增减操作
- 载入操作
- 比较并交换
- 交换
- 存储
- 代码示例
- GO语言反射
- 反射的用处
- 代码示例
- 网络编程
- 服务端
- 客户端
GO语言类型断言
类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型。
简单的来说就是判断是不是某一个类型
在Go语言中类型断言的语法格式如下(类似于java的instanceof):
value, ok := x.(T)
其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)。
该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型:
- 如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T。
- 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值。
- 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。
代码示例
package main
import "fmt"
func main() {
var str interface{}
str = "测试"
str = str.(string)
fmt.Println(str)
//str = str.(int) //类型对不上会抛panic: interface conversion: interface {} is string, not int
//基于上述问题,可以按照下述写法来写
str, isInt := str.(int) //返回两个,一个是原值,一个是:是否是括号里的类型[即str这个变量是不是int类型]
println(isInt)
if isInt {
fmt.Printf("类型为:%T \n", str)
} else {
fmt.Println("str变量不是int类型")
}
}
运行结果
测速
false
str变量不是int类型
文件操作
文件在go中是一个结构体,它的定义和相关函数在os包中,所以要先导包
os相关文档: https://studygolang.com/static/pkgdoc/pkg/os.htm
打开、关闭、读取文件
文件内容
创建一个MyFile的txt文件
my file test
next line test
123
示例代码
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
filePath := "/Users/dasouche/go/src/MyTest/file_dir/MyFile"
//Open打开一个文件用于读取。如果操作成功,返回的文件对象的方法可用于读取数据;对应的文件描述符具有O_RDONLY模式。如果出错,错误底层类型是*PathError。
file, err := os.Open(filePath)
fmt.Printf("%v \n", err)
if nil != err { //如果文件不存在则会报错:open file error = open xxx/xxx.txt: The system cannot find the file specified.
fmt.Println("文件读取失败")
//结束运行
return
}
//file结构体里存放着一个指针
fmt.Println("file = ", *file)
println("-----------读取文件内容方式1-------------")
//方式1:使用buffer.ReadLine()
//将读取的文件放入缓冲区, 注意⚠️ :bufio.NewReader(rd io.Reader) 函数内部调用了 NewReaderSize(rd, defaultBufSize),而这个defaultBufSize的值就是4096。
//br := bufio.NewReader(file) //建议使用下面自定义大小的缓冲区
buffer := bufio.NewReaderSize(file, 10240)
var resultBuffer []byte
for {
line, prefix, err := buffer.ReadLine()
fmt.Printf("读取一行内容:%c , prefix:%v, err:%v \n", line, prefix, err)
if err == io.EOF { // 读到文件尾会返回一个EOF异常
break
}
// 追加到自定义缓冲区内
resultBuffer = append(resultBuffer, line...)
// 如果prefix为真,则代表该行还有尚未读取完的数据,跳过后续具体操作,继续读取完该行剩余内容
if prefix {
continue
}
str := string(resultBuffer)
fmt.Printf("--------------------\n")
fmt.Println("len(buf) = ", len(resultBuffer))
fmt.Println("len(str) = ", len(str))
fmt.Println(str)
fmt.Printf("--------------------\n\n")
// 清空切片
resultBuffer = append(resultBuffer[:0], resultBuffer[len(resultBuffer):]...)
}
println("-----------读取文件内容方式2-------------")
//方式2: file.Read
file, err = os.Open(filePath)
var content []byte
var tmp = make([]byte, 128)
for {
n, err := file.Read(tmp)
if err == io.EOF { // 读到文件尾会返回一个EOF异常
fmt.Println("文件读完了")
break
}
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
content = append(content, tmp[:n]...)
}
fmt.Println(string(content))
println("-----------读取文件内容方式3-------------")
//方式3: reader.ReadString
file, err = os.Open(filePath)
reader := bufio.NewReaderSize(file, 10240)
for {
str, err := reader.ReadString('\n') // 一次读取一行
if err == nil {
fmt.Print(str) // reader会把分隔符\n读进去,所以不用Println
} else if err == io.EOF { // 读到文件尾会返回一个EOF异常
fmt.Println("文件读取完毕")
break
} else {
fmt.Println("read error: ", err)
}
}
println("-----------读取文件内容方式4-------------")
//方式4: ioutil.ReadAll、io.ReadAll(file)
file, err = os.Open(filePath)
// return 之前记得关闭文件
if err != nil {
fmt.Println(err)
return
}
//context, _ := ioutil.ReadAll(file) //此方式不建议使用了
context, err := io.ReadAll(file)
fmt.Println(string(context))
//关闭文件
err = file.Close()
if err != nil {
fmt.Println("close file error = ", err)
}
//关闭文件
err = file.Close()
if err != nil {
fmt.Println("close file error = ", err)
}
}
运行结果
<nil>
file = {0x14000182120}
-----------读取文件内容方式1-------------
读取一行内容:[m y f i l e t e s t] , prefix:false, err:<nil>
--------------------
len(buf) = 12
len(str) = 12
my file test
--------------------
读取一行内容:[n e x t l i n e t e s t] , prefix:false, err:<nil>
--------------------
len(buf) = 14
len(str) = 14
next line test
--------------------
读取一行内容:[1 2 3] , prefix:false, err:<nil>
--------------------
len(buf) = 3
len(str) = 3
123
--------------------
读取一行内容:[] , prefix:false, err:EOF
-----------读取文件内容方式2-------------
文件读完了
my file test
next line test
123
-----------读取文件内容方式3-------------
my file test
next line test
文件读取完毕
-----------读取文件内容方式4-------------
my file test
next line test
123
创建、写入文件
代码示例
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
//需要创建文件的路径
filePath := "/Users/dasouche/go/src/MyTest/file_dir/CreateFileTest.txt"
//参数1:文件路径 参数2:读模式或者创建文件模式 参数3:赋予文件777权限,linux系统777代表所有用户可读写
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY, 777)
if err != nil {
fmt.Println("Open file error: ", err)
return
}
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
writer.WriteString("New content" + fmt.Sprintf("%d", i) + "\n") // 写入一行数据
}
writer.Flush() // 把缓存数据刷入文件中
file.Close()
}
运行结果
判断文件是否是否存在
代码示例
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
//文件路径
filePath := "/Users/dasouche/go/src/MyTest/file_dir/MyFile"
_, err := os.Stat(filePath)
var isExit bool
if err == nil {
isExit = true
}
if os.IsNotExist(err) {
isExit = false
}
fmt.Printf("文件是否存在:%v \n", isExit)
}
运行结果
文件是否存在:true
序列化、反序列化
为什么要进行序列话,当各个不同的模块项目交互,一般是以json的格式进行数据交互,数据接收的时候一般需要将json数据转为自己需要的结构体数据,数据传出的时候将数据转为json格式
总的来说就是json数据和结构体数据互相转换
序列化:将结构体数据转为json数据
反序列化:将json数据转为结构体数据
代码示例
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string
Age int
}
type PersonV2 struct {
//如果需要在转换成json的时候字段名为其他需要打个``标签
Name string `json:"name"`
Age int `json:"age"`
UserAddress string `json:"user_address"`
}
func main() {
println("-------序列化测试:结构体转json-------")
person := Person{Name: "szc", Age: 23}
json_bytes, error_ := json.Marshal(&person)
if error_ != nil {
fmt.Println("Json error:", error_)
return
}
fmt.Printf("person结构体转json:%v \n", string(json_bytes))
person_2 := PersonV2{Name: "szc", Age: 23, UserAddress: "北京朝阳区"}
json_bytes, error_ = json.Marshal(&person_2)
if error_ != nil {
fmt.Println("Json error:", error_)
return
}
fmt.Printf("person_2结构体转json:%v \n", string(json_bytes))
println("-------反序列化测试:json转结构体-------")
jsonStr_1 := "{\"Name\":\"szc\",\"Age\":23}"
jsonStr_2 := "{\"name\":\"szc\",\"age\":23,\"user_address\":\"北京朝阳区\"} "
var person1 Person
var person2 PersonV2
err_1 := json.Unmarshal([]byte(jsonStr_1), &person1)
err_2 := json.Unmarshal([]byte(jsonStr_2), &person2)
fmt.Printf("json转Person结构体:%v \n", person1)
fmt.Printf("json转PersonV2结构体:%v \n", person2)
if err_1 != nil || err_2 != nil {
fmt.Println("Json error:", err_1)
fmt.Println("Json error:", err_2)
return
}
}
运行结果
-------序列化测试:结构体转json-------
person结构体转json:{"Name":"szc","Age":23}
person_2结构体转json:{"name":"szc","age":23,"user_address":"北京朝阳区"}
-------反序列化测试:json转结构体-------
json转Person结构体:{szc 23}
json转PersonV2结构体:{szc 23 北京朝阳区}
命令行参数
执行go文件时可以带上一些参数,可以在程序里直接取到这些参数
命令行执行
如下代码
func main() {
for index, arg := range os.Args {
fmt.Println("第", (index + 1), "个参数是", arg)
}
}
执行可得
GoLand里面执行
直接在GoLand里面直接进行测试
运行之后
GO语言并发编程
Go语言的多线程是基于消息传递的,Go语言将基于CSP模型的并发编程内置到了语言中,其特点就是goroutine之间是共享内存的。
协程goroutine
协程是Go语言特有的一种轻量级线程,实际上,所有的Go语言都是通过goroutine运行的。Go 协程是与其他函数或方法一起并发运行的函数或方法。Go 协程可以看作是轻量级线程。与线程相比,创建一个 Go 协程的成本很小。因此在 Go 应用中,常常会看到有数以千计的 Go 协程并发地运行。
线程、进程、协程核心概念
进程:是指具有一定功能的程序关于某数据集合上的一次执行过程,主要包含程序指令和数据。
线程:进场的子集,是由进程创建的拥有自己控制流和栈的轻量级实体,一个进程至少有一个线程。线程是进程的实际存在。
协程goroutine:是Go语言并发程序的最小执行单位,可以理解为goroutine运行在操作系统的线程之上,它更为轻量。
特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
Go 协程相比于线程的优势
- 相比线程而言,Go 协程的成本极低。堆栈大小只有若干 kb,并且可以根据应用的需求进行增减。而线程必须指定堆栈的大小,其堆栈是固定不变的。
- Go 协程会复用(Multiplex)数量更少的 OS 线程。即使程序有数以千计的 Go 协程,也可能只有一个线程。如果该线程中的某一 Go 协程发生了阻塞(比如说等待用户输入),那么系统会再创建一个 OS 线程,并把其余 Go 协程都移动到这个新的 OS 线程。所有这一切都在运行时进行,作为程序员,我们没有直接面临这些复杂的细节,而是有一个简洁的 API 来处理并发。
- Go 协程使用信道(Channel)来进行通信。信道用于防止多个协程访问共享内存时发生竞态条件(Race Condition)。信道可以看作是 Go 协程之间通信的管道。
代码示例
package main
import (
"fmt"
"time"
)
func main() {
go func() { // 开启一个协程,main主线程执行玩的时候协程直接结束,主线程并不会等待协程执行结束才结束
for i := 0; i < 5; i++ {
fmt.Println("协程执行.......", i)
time.Sleep(time.Second) // 休眠1秒
}
}()
for i := 0; i < 3; i++ {
fmt.Println("主线程执行.......", i)
time.Sleep(time.Second)
}
fmt.Println("主线程执行结束")
}
执行结果:可以看出来主线程逻辑执行完直接强制把协程也结束了
主线程执行....... 0
协程执行....... 0
协程执行....... 1
主线程执行....... 1
主线程执行....... 2
协程执行....... 2
主线程执行结束
sync.WaitGroup
sync.WaitGroup的作用就是在主线程里等待各个协程都执行完毕,可理解为Wait-Goroutine-Group,即等待一组goroutine结束。
使用⚠️注意点:
- 计数器不能为负值,否则引发
panic
WaitGroup
对象不是引用类型
代码示例
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var syncWG sync.WaitGroup
syncWG.Add(1) //执行协程+1
go func() { // 开启一个协程,main主线程执行玩的时候协程直接结束,主线程并不会等待协程执行结束才结束
for i := 0; i < 5; i++ {
fmt.Println("协程1执行.......", i)
time.Sleep(time.Second) // 休眠1秒
}
syncWG.Done() //执行协程-1,标识当前协程执行结束
}()
syncWG.Add(1) //执行协程+1,
go func() { // 开启一个协程,main主线程执行玩的时候协程直接结束,主线程并不会等待协程执行结束才结束
for i := 0; i < 5; i++ {
fmt.Println("协程2执行.......", i)
time.Sleep(time.Second) // 休眠1秒
}
syncWG.Done() //执行协程-1,可认为当前协程执行结束,逻辑执行完毕
}()
for i := 0; i < 3; i++ {
fmt.Println("主线程执行.......", i)
time.Sleep(time.Second) //休眠1秒
}
syncWG.Wait() //阻塞 直到协程组内协程数为0时往下执行
fmt.Println("主线程执行结束")
}
运行结果
主线程执行....... 0
协程2执行....... 0
协程1执行....... 0
协程1执行....... 1
主线程执行....... 1
协程2执行....... 1
协程2执行....... 2
主线程执行....... 2
协程1执行....... 2
协程1执行....... 3
协程2执行....... 3
协程2执行....... 4
协程1执行....... 4
主线程执行结束
锁
当我们使用多线程或者多个协程对一个变量进行修改是,可能会导致一些并发问题,比如多个个线程同时对一个变量进行加减,会后算出来的数据可能不准确,所以要对其加以个锁,锁在锁住一个资源后,其他线程或者协程想要修改锁住的资源,必须等锁住的资源执行完逻辑释放锁才能拿到这个锁执行逻辑。
简单的来说就是锁住的东西只能在解锁后进行操作,即数据在上锁后是安全的,不会出现多次执行数据不一致的情况
示例
下述代码是不加锁进行多协程修改,执行多次的结果是不一致的,
package main
import (
"fmt"
"sync"
)
var num int64
func main() {
var syncWG sync.WaitGroup
var maxNum int64
for i := 0; i <= 100; i++ {
syncWG.Add(1) //执行协程+1,
go func() { // 开启一个协程,main主线程执行玩的时候协程直接结束,主线程并不会等待协程执行结束才结束
for j := 0; j <= 100; j++ {
num++
fmt.Printf("当前变量值:%v \n", num)
if num > maxNum {
maxNum = num
}
}
syncWG.Done() //执行协程-1,可认为当前协程执行结束,逻辑执行完毕
}()
}
for i := 0; i <= 100; i++ {
syncWG.Add(1) //执行协程+1,
go func() { // 开启一个协程,main主线程执行玩的时候协程直接结束,主线程并不会等待协程执行结束才结束
for j := 0; j <= 100; j++ {
num++
fmt.Printf("当前变量值:%v \n", num)
if num > maxNum {
maxNum = num
}
}
syncWG.Done() //执行协程-1,可认为当前协程执行结束,逻辑执行完毕
}()
}
syncWG.Wait() //阻塞 直到协程组内协程数为0时往下执行
fmt.Println("主线程执行结束,maxNum:", maxNum)
fmt.Println("主线程执行结束,变量num:", num)
}
多次执行后返回的结果
... 上面的输出省略
主线程执行结束,maxNum: 20320
主线程执行结束,变量num: 20320
主线程执行结束,maxNum: 20332
主线程执行结束,变量num: 20332
。。。//自己可以拿代码多执行几次
互斥锁
互斥锁用来保证在任一时刻,只能有一个例程访问某对象。Mutex
的初始值为解锁状态。Mutex通常作为其它结构体的匿名字段使用,使该结构体具有Lock
和Unlock
方法。
上面的代码我们加一个锁(需要对多个协程或者线程操作的值上锁)之后再看结果
package main
import (
"fmt"
"sync"
)
var num int64
func main() {
var syncLock sync.Mutex
var syncWG sync.WaitGroup
var maxNum int64
for i := 0; i <= 100; i++ {
syncWG.Add(1) //执行协程+1,
go func() { // 开启一个协程,main主线程执行玩的时候协程直接结束,主线程并不会等待协程执行结束才结束
syncLock.Lock() // 请求锁
for j := 0; j <= 100; j++ {
num++
fmt.Printf("协程组1-%v执行.......,当前变量值:%v \n", j, num)
if num > maxNum {
maxNum = num
}
}
syncLock.Unlock() // 释放锁
syncWG.Done() //执行协程-1,可认为当前协程执行结束,逻辑执行完毕
}()
}
for i := 0; i <= 100; i++ {
syncWG.Add(1) //执行协程+1,
go func() { // 开启一个协程,main主线程执行玩的时候协程直接结束,主线程并不会等待协程执行结束才结束
syncLock.Lock() // 请求锁
for j := 0; j <= 100; j++ {
num++
fmt.Printf("协程组1-%v执行.......,当前变量值:%v \n", j, num)
if num > maxNum {
maxNum = num
}
}
syncLock.Unlock() // 释放锁
syncWG.Done() //执行协程-1,可认为当前协程执行结束,逻辑执行完毕
}()
}
syncWG.Wait() //阻塞 直到协程组内协程数为0时往下执行
fmt.Println("主线程执行结束,maxNum:", maxNum)
fmt.Println("主线程执行结束,变量num:", num)
}
运行结果
不管运行几次都是这样的结果
...上面的输出省略
主线程执行结束,maxNum: 20402
主线程执行结束,变量num: 20402
通道
管道(channel)
通道是Go语言提供的一种在goroutine之间进行数据传输的通信机制。当然,通过channel传递的数据只能是一些指定的类型,这些类型被称为通道的元素类型。你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication)。
管道(channel)为引用类型,必须先初始化才能使用;本质是一个队列,有类型,而且线程安全,
管道的定义: var 变量名 chan 数据类型
代码示例
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
println("------管道测试-------")
//简单用法
simpleChannel := make(chan int, 2)
simpleChannel <- 100
simpleChannel <- 200
//取出管道数据丢弃
<-simpleChannel
channelInfo := <-simpleChannel
fmt.Printf("读取simpleChannel管道数据: %v \n", channelInfo)
intsChannel := make(chan int, 50)
//开启协程,用于写数据
go func() {
for true {
//将i写入管道
writeNum := rand.Intn(100)
intsChannel <- writeNum
fmt.Printf("写入管道数据: %v \n", writeNum)
time.Sleep(time.Millisecond * 500)
}
}()
//开启协程,用于读数据
go func() {
for true {
//读取管道数据
readInfo := <-intsChannel
fmt.Printf("读取管道数据: %v \n", readInfo)
time.Sleep(time.Millisecond * 500)
}
}()
//防止数据还没有在协程里打印,main函数退出,main()执行结束后其他相关的协程也会结束
time.Sleep(time.Second * 3)
//程序结束
}
运行结果
------管道测试-------
读取simpleChannel管道数据: 200
写入管道数据: 81
读取管道数据: 81
写入管道数据: 87
读取管道数据: 87
写入管道数据: 47
读取管道数据: 47
写入管道数据: 59
读取管道数据: 59
写入管道数据: 81
读取管道数据: 81
写入管道数据: 18
读取管道数据: 18
select
select
语句选择一组可能的send操作和receive操作去处理。它类似switch
,但是只是用来处理通讯(communication)操作。
它的case
可以是send语句,也可以是receive语句,亦或者default
。
receive
语句可以将值赋值给一个或者两个变量。它必须是一个receive操作。
最多允许有一个default case
,它可以放在case列表的任何位置,尽管我们大部分会将它放在最后。
详细注意事项以及select和switch case的区别可以参考此文章:https://blog.csdn.net/anzhenxi3529/article/details/123644425
使用事宜
select {
case <-ch1 : // 检测有没有数据可读
// 一旦成功读取到数据,则进行该case处理语句
case ch2 <- 1 : // 检测有没有数据可写
// 一旦成功向ch2写入数据,则进行该case处理语句
default:
// 如果以上都没有符合条件,那么进入default处理流程
}
代码示例
package main
import (
"fmt"
)
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x: //写入数据成功时执行此case
fmt.Printf("写入管道数据:%v \n", x)
x, y = y, y+1
case <-quit: //读数据成功时执行此case
fmt.Println("quit")
return //读取完了直接结束
default:
// 如果以上都没有符合条件,那么进入default处理流程
//do nothing
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Printf("读取管道数据:%v \n", <-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
运行结果
读取管道数据:0
写入管道数据:0
写入管道数据:1
读取管道数据:1
读取管道数据:2
写入管道数据:2
写入管道数据:3
读取管道数据:3
读取管道数据:4
写入管道数据:4
写入管道数据:5
读取管道数据:5
读取管道数据:6
写入管道数据:6
写入管道数据:7
读取管道数据:7
读取管道数据:8
写入管道数据:8
写入管道数据:9
读取管道数据:9
quit
定时器(Timer)
Timer顾名思义,就是定时器的意思,可以实现一些定时操作,内部也是通过channel来实现的。Timer只执行一次
package main
import (
"fmt"
"time"
)
func main() {
timer1 := time.NewTimer(time.Second * 2)
t1 := time.Now()
fmt.Printf("t1:%v\n", t1)
t2 := <-timer1.C
fmt.Printf("t2:%v\n", t2)
//如果只是想单纯的等待的话,可以使用 time.Sleep 来实现
timer2 := time.NewTimer(time.Second * 2)
<-timer2.C
fmt.Println("2s后")
time.Sleep(time.Second * 2)
fmt.Println("再一次2s后")
<-time.After(time.Second * 2) //time.After函数的返回值是chan Time
fmt.Println("再再一次2s后")
timer3 := time.NewTimer(time.Second)
go func() {
<-timer3.C
fmt.Println("Timer 3 expired")
}()
stop := timer3.Stop() //停止定时器
阻止timer事件发生,当该函数执行后,timer计时器停止,相应的事件不再执行
if stop {
fmt.Println("Timer 3 stopped")
}
fmt.Println("before")
timer4 := time.NewTimer(time.Second * 5) //原来设置5s
timer4.Reset(time.Second * 1) //重新设置时间,即修改NewTimer的时间
<-timer4.C
fmt.Println("after")
}
运行结果
t1:2022-11-30 18:46:33.617359 +0800 CST m=+0.000374501
t2:2022-11-30 18:46:35.618388 +0800 CST m=+2.001404376
2s后
再一次2s后
再再一次2s后
Timer 3 stopped
before
after
Ticker
Ticker可以周期的执行。
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(time.Second)
counter := 1
for _ = range ticker.C {
fmt.Println("ticker 1") //每秒执行一次
counter++
if counter > 5 {
break
}
}
ticker.Stop() //停止
}
运行结果
ticker 1
ticker 1
ticker 1
ticker 1
ticker 1
GO超时检查
select 可以很方便的完成goroutine的超时检查。超时就是指某个goroutine由于意外退出,导致另一方的goroutine阻塞,从而影响主goroutine。
阻塞
如下述代码所示,协程一直在循环执行,无法主动停止,导致主线程一直被阻塞
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var syncWg sync.WaitGroup
syncWg.Add(1)
go func() {
for {
fmt.Println("协程阻塞中")
time.Sleep(time.Second)
}
syncWg.Done()
}()
//等待协程执行结束【由于上面的协程一直在执行,不会停止,主线程就一直被阻塞无法继续执行】
syncWg.Wait()
fmt.Println("主线程执行结束")
}
运行结果
协程阻塞中
。。。//后面都是协程阻塞中,主线程业务流程一直被阻塞无法执行
超时控制
想要超时控制还有有一些法子的,我这里就展示2中方式
select + time.After
package main
import (
"fmt"
"time"
)
func main() {
stopFlagChannel := make(chan string)
go func() {
for {
fmt.Printf("block process,current Second:%v \n", time.Now().Second())
time.Sleep(time.Second)
}
stopFlagChannel <- "processing..."
}()
select {
//第一个case里阻塞的时间只有比第二个case阻塞的时间长的时候, 才能执行第二个case
case res := <-stopFlagChannel:
fmt.Println(res)
case <-time.After(time.Second * 5):
fmt.Printf("timeout control... stop,current Second:%v \n", time.Now().Second())
}
fmt.Println("主线程执行结束")
}
运行结果
block process,current Second:14
block process,current Second:15
block process,current Second:16
block process,current Second:17
block process,current Second:18
timeout control... stop,current Second:19
主线程执行结束
释放标记
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var syncWg sync.WaitGroup
syncWg.Add(1) //协程计数器加1
go func() {
for {
fmt.Println("协程阻塞中")
time.Sleep(time.Second)
}
syncWg.Done() //上面是循环,所以在这里无法执行
}()
go func() {
//此协程在3秒后执行协程计数器-1操作
time.Sleep(time.Second * 3)
syncWg.Done() //协程计数器减1
}()
//等待协程执行结束
syncWg.Wait()
fmt.Println("主线程执行结束")
}
运行结果
协程阻塞中
协程阻塞中
协程阻塞中
主线程执行结束
并发限制(限制并发数量)
如果执行任务数量太多,不加以限制的并发开启 goroutine 的话,可能会过多的占用资源,服务器可能会爆炸。所以实际环境中并发限制也是一定要做的。
代码示例
package main
import (
"fmt"
"sync"
"time"
)
func main() {
listconlimit := make(chan bool, 10) // 新建长度为10的管道
wg := &sync.WaitGroup{}
for n := 0; n <= 50; n++ { // 50
listconlimit <- true // 管道写入,缓冲为10,写满10就阻塞
fmt.Printf("当前管道长度:%v \n", len(listconlimit))
wg.Add(1)
go func(n int, group *sync.WaitGroup) {
defer func() {
data_info := <-listconlimit
fmt.Printf("读取管道数据:%v, 当前管道长度:%v \n", data_info, len(listconlimit))
group.Done()
}() //释放管道资源
time.Sleep(time.Second) // 模拟耗时操作
//逻辑执行完上面defer释放资源将协程标记释放
}(n, wg)
}
wg.Wait()
fmt.Println("ok")
}
运行结果
当前管道长度:1
当前管道长度:2
当前管道长度:3
当前管道长度:4
当前管道长度:5
当前管道长度:6
当前管道长度:7
当前管道长度:8
当前管道长度:9
当前管道长度:10
读取管道数据:true, 当前管道长度:8
读取管道数据:true, 当前管道长度:7
读取管道数据:true, 当前管道长度:10
读取管道数据:true, 当前管道长度:9
当前管道长度:6
当前管道长度:2
当前管道长度:3
当前管道长度:4
当前管道长度:5
当前管道长度:6
当前管道长度:7
读取管道数据:true, 当前管道长度:6
当前管道长度:8
当前管道长度:9
读取管道数据:true, 当前管道长度:5
当前管道长度:10
读取管道数据:true, 当前管道长度:2
读取管道数据:true, 当前管道长度:3
读取管道数据:true, 当前管道长度:1
读取管道数据:true, 当前管道长度:4
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:9
读取管道数据:true, 当前管道长度:8
读取管道数据:true, 当前管道长度:10
读取管道数据:true, 当前管道长度:7
当前管道长度:7
读取管道数据:true, 当前管道长度:5
读取管道数据:true, 当前管道长度:4
当前管道长度:5
当前管道长度:6
当前管道长度:7
当前管道长度:8
当前管道长度:9
当前管道长度:10
读取管道数据:true, 当前管道长度:6
当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
读取管道数据:true, 当前管道长度:10
当前管道长度:10
读取管道数据:true, 当前管道长度:10
读取管道数据:true, 当前管道长度:6
读取管道数据:true, 当前管道长度:9
读取管道数据:true, 当前管道长度:5
读取管道数据:true, 当前管道长度:8
读取管道数据:true, 当前管道长度:4
读取管道数据:true, 当前管道长度:3
读取管道数据:true, 当前管道长度:2
当前管道长度:2
读取管道数据:true, 当前管道长度:1
读取管道数据:true, 当前管道长度:7
读取管道数据:true, 当前管道长度:0
ok
并发时的原子操作
atomic 提供的原子操作能够确保任一时刻只有一个goroutine对变量进行操作,善用 atomic 能够避免程序中出现大量的锁操作。
atomic常见操作有:
- 增减
- 载入 read
- 比较并交换 cas
- 交换
- 存储 write
增减操作
atomic 包中提供了如下以Add为前缀的增减操作:
- func AddInt32(addr *int32, delta int32) (new int32)
- func AddInt64(addr *int64, delta int64) (new int64)
- func AddUint32(addr *uint32, delta uint32) (new uint32)
- func AddUint64(addr *uint64, delta uint64) (new uint64)
- func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
载入操作
atomic 包中提供了如下以Load为前缀的增减操作:
- func LoadInt32(addr *int32) (val int32)
- func LoadInt64(addr *int64) (val int64)
- func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
- func LoadUint32(addr *uint32) (val uint32)
- func LoadUint64(addr *uint64) (val uint64)
- func LoadUintptr(addr *uintptr) (val uintptr)
载入操作能够保证原子的读变量的值,当读取的时候,任何其他CPU操作都无法对该变量进行读写,其实现机制受到底层硬件的支持。
比较并交换
该操作简称 CAS(Compare And Swap)。 这类操作的前缀为 CompareAndSwap
:
- func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
- func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
- func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
- func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
- func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
- func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
该操作在进行交换前首先确保变量的值未被更改,即仍然保持参数
old
所记录的值,满足此前提下才进行交换操作。CAS的做法类似操作数据库时常见的乐观锁机制。
交换
此类操作的前缀为 Swap
:
- func SwapInt32(addr *int32, new int32) (old int32)
- func SwapInt64(addr *int64, new int64) (old int64)
- func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
- func SwapUint32(addr *uint32, new uint32) (old uint32)
- func SwapUint64(addr *uint64, new uint64) (old uint64)
- func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
相对于CAS,明显此类操作更为暴力直接,并不管变量的旧值是否被改变,直接赋予新值然后返回被替换的值。
存储
此类操作的前缀为 Store
:
- func StoreInt32(addr *int32, val int32)
- func StoreInt64(addr *int64, val int64)
- func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
- func StoreUint32(addr *uint32, val uint32)
- func StoreUint64(addr *uint64, val uint64)
- func StoreUintptr(addr *uintptr, val uintptr)
此类操作确保了写变量的原子性,避免其他操作读到了修改变量过程中的脏数据。
代码示例
package main
import (
"fmt"
"sync"
"sync/atomic"
)
var number int64
func main() {
waitGroup := sync.WaitGroup{}
//两个协程对同一个变量进行加减,如果在没有加锁的情况下,使用atomic能够确保任一时刻只有一个goroutine对变量进行操作
println("----------atomic.Addxxx测试加法-------------")
waitGroup.Add(1)
go func() {
for i := 0; i < 100*100*100*100; i++ {
//原子操作
atomic.AddInt64(&number, 1)
}
waitGroup.Done()
}()
waitGroup.Add(1)
go func() {
for i := 0; i < 100*100*100*100; i++ {
//原子操作
atomic.AddInt64(&number, 1)
}
waitGroup.Done()
}()
waitGroup.Wait()
fmt.Println("number:", number)
println("atomic.Addxxx测试加法执行结束")
println("----------atomic.Addxxx测试加减法-------------")
number = 0
waitGroup.Add(1)
go func() {
for i := 0; i < 100*100*100*100; i++ {
//原子操作
atomic.AddInt64(&number, 1)
}
waitGroup.Done()
}()
waitGroup.Add(1)
go func() {
for i := 0; i < 100*100*100*100; i++ {
//原子操作
atomic.AddInt64(&number, -1)
}
waitGroup.Done()
}()
waitGroup.Wait()
fmt.Println("number:", number)
println("atomic.Addxxx测试加减法执行结束")
println("----------atomic.Loadxxx测试加减法-------------")
//载入操作能够保证原子的读变量的值,当读取的时候,任何其他CPU操作都无法对该变量进行读写,其实现机制受到底层硬件的支持。
number = 0
waitGroup.Add(1)
go func() {
for i := 0; i < 100*100*100*100; i++ {
//原子操作
loadInt64 := atomic.LoadInt64(&number)
loadInt64++
}
waitGroup.Done()
}()
waitGroup.Add(1)
go func() {
for i := 0; i < 100*100*100*100; i++ {
//原子操作
loadInt64 := atomic.LoadInt64(&number)
loadInt64--
}
waitGroup.Done()
}()
waitGroup.Wait()
fmt.Println("number:", number)
println("atomicLoadxxx测试加减法执行结束")
println("----------atomic.Loadxxx测试加减法-------------")
//载入操作能够保证原子的读变量的值,当读取的时候,任何其他CPU操作都无法对该变量进行读写,其实现机制受到底层硬件的支持。
number = 0
waitGroup.Add(1)
go func() {
for i := 0; i < 100*100*100*100; i++ {
//原子操作
loadInt64 := atomic.LoadInt64(&number)
loadInt64++
}
fmt.Println("加逻辑结束, number:", number)
waitGroup.Done()
}()
waitGroup.Add(1)
go func() {
for i := 0; i < 100*100*100*100; i++ {
//原子操作
loadInt64 := atomic.LoadInt64(&number)
loadInt64--
}
fmt.Println("减逻辑结束, number:", number)
waitGroup.Done()
}()
waitGroup.Wait()
fmt.Println("number:", number)
println("atomicLoadxxx测试加减法执行结束")
println("----------atomic.CompareAndSwapxxx测试比较和替换-------------")
//CAS操作是先比较变量的值是否等价于给定的值,如果是才进行替换
number = 0
waitGroup.Add(1)
go func() {
for i := 0; i < 200; i++ {
if number <= 100 {
number++
}
//原子操作
atomic.CompareAndSwapInt64(&number, 100, 999) //只有number满足100的时候才能将number替换成999
}
fmt.Println("加逻辑结束, number:", number)
waitGroup.Done()
}()
waitGroup.Wait()
fmt.Println("number:", number)
println("atomic.CompareAndSwapxxx测试加减法执行结束")
println("----------atomic.Swapxxx测试替换-------------")
//Swap不管变量的旧值是否被改变,直接赋予新值然后返回背替换的值。
number = 0
waitGroup.Add(1)
go func() {
//原子操作
swapInt64 := atomic.SwapInt64(&number, 999) //只有number满足100的时候才能将number替换成999
fmt.Println("加逻辑结束, number:", number)
fmt.Println("替换后返回的值, swapInt64:", swapInt64)
waitGroup.Done()
}()
waitGroup.Wait()
fmt.Println("number:", number)
println("atomic.Swapxxx测试替换执行结束")
println("----------atomic.Storexxx测试替换-------------")
//写操作,直接赋值
number = 0
atomic.StoreInt64(&number, 1000)
fmt.Println("number:", number)
println("atomic.Storexxx测试替换执行结束")
}
运行结果
----------atomic.Addxxx测试加法-------------
number: 200000000
atomic.Addxxx测试加法执行结束
----------atomic.Addxxx测试加减法-------------
number: 0
atomic.Addxxx测试加减法执行结束
----------atomic.Loadxxx测试加减法-------------
number: 0
atomicLoadxxx测试加减法执行结束
----------atomic.Loadxxx测试加减法-------------
加逻辑结束, number: 0
减逻辑结束, number: 0
number: 0
atomicLoadxxx测试加减法执行结束
----------atomic.CompareAndSwapxxx测试比较和替换-------------
加逻辑结束, number: 999
number: 999
atomic.CompareAndSwapxxx测试加减法执行结束
----------atomic.Swapxxx测试替换-------------
加逻辑结束, number: 999
替换后返回的值, swapInt64: 0
number: 999
atomic.Swapxxx测试替换执行结束
----------atomic.Storexxx测试替换-------------
number: 1000
atomic.Storexxx测试替换执行结束
GO语言反射
Go语言的反射需要理解两个概念Type和Value,它们也是Go语言中reflect空间里最重要的两个类型
reflect包实现了运行时反射,允许程序操作任意类型的对象。典型用法是用静态类型interface{}保存一个值,通过调用TypeOf获取其动态类型信息,该函数返回一个Type类型值。调用ValueOf函数返回一个Value类型值,该值代表运行时的数据。Zero接受一个Type类型参数并返回一个代表该类型零值的Value类型值。
中文官方文档可参考:https://studygolang.com/static/pkgdoc/pkg/reflect.htm
反射的用处
- 反射可以动态获取变量的类型、结构体的属性和方法,以及设置属性、执行方法等信息
- 通过反射可以修改变量的值,可以调用关联的方法
代码示例
package main
import (
"fmt"
"reflect"
)
var number int
type User struct {
Name, address string
age int `json:"Age"`
}
// 新增-设置名称方法
func (s *User) SetName(name string) {
s.Name = name
fmt.Printf("有参数方法 通过反射进行调用:%d \n", s)
}
// 新增-打印信息方法
func (s User) PrintStudent() {
fmt.Printf("无参数方法 通过反射进行调用:%v\n", s)
}
func main() {
println("--------基本数据类型反射--------")
number = 100
//获取反射类型
reflectType := reflect.TypeOf(number)
fmt.Println("reflectType = ", reflectType) // int
fmt.Println("reflectType name = ", reflectType.Name()) // int
// 获取属性值
reflectValue := reflect.ValueOf(number)
fmt.Printf("reflectValue = %v,reflectValue type = %T\n", reflectValue, reflectValue) // 100, reflect.Value
n1 := 100 + reflectValue.Int() // 获取反射值持有的整型值
fmt.Println("n1 = ", n1)
iV := reflectValue.Interface() // 反射值转换成空接口
num, ok := iV.(int) // 类型断言
fmt.Println("num = ", num, ok)
//注意⚠️:type和kind有时候有时候是一样的,有时候是不一样的(基本类型一样,结构体不一样)
fmt.Printf("----%v, %v, %v \n", reflectType.Kind(), reflectValue.Type(), reflectValue.Kind())
//获取类别
k := reflectValue.Kind()
switch k {
case reflect.Int:
fmt.Printf("number is int\n")
case reflect.String:
fmt.Printf("number is string\n")
}
println("--------结构体反射--------")
user := User{Name: "无名", address: "山洞", age: 18}
reflectType_2 := reflect.TypeOf(user)
reflectvalue_2 := reflect.ValueOf(user)
iV_2 := reflectValue.Interface() // 反射值转换成空接口
fmt.Println("reflectType_2 = ", reflectType_2)
fmt.Printf("reflectvalue_2 = %v,reflectvalue_2 type = %T\n", reflectvalue_2, reflectvalue_2)
fmt.Printf("iV_2 value=%d, type=%T \n ", iV_2, iV_2)
//获取字段数量
numFieldCount := reflectvalue_2.NumField()
fmt.Printf("获取到结构体字段数量:%d \n", numFieldCount)
for i := 0; i < numFieldCount; i++ {
field := reflectvalue_2.Field(i)
fmt.Printf("第 %d 个字段值 = %v, 类别 = %v \n", i+1, field, field.Kind()) // 获取字段值
}
//获取方法数量
numMethodCount := reflectvalue_2.NumMethod()
fmt.Printf("获取到结构体方法数量:%d \n", numMethodCount)
for i := 0; i < numMethodCount; i++ {
method := reflectvalue_2.Method(i)
fmt.Printf("第 %d 个方法地址 = %v, 类别 = %v \n", i+1, method, method.Kind()) // 获取方法相关信息
}
//通过reflect.Value获取对应的方法并调用
m1 := reflectvalue_2.MethodByName("PrintStudent")
var args []reflect.Value
m1.Call(args)
//修改结构体字段属性,方式1
user_2 := User{Name: "无名-2", address: "山洞", age: 18}
reflectvalue_3 := reflect.ValueOf(&user_2)
m2 := reflectvalue_3.MethodByName("SetName")
var args2 []reflect.Value
name := "stu01"
nameVal := reflect.ValueOf(name)
args2 = append(args2, nameVal)
m2.Call(args2)
//修改结构体字段属性,方式2
fmt.Printf("修改字段前属性:%s \n", user_2)
//根据字段下标修改,注意⚠️:在struct中的属性,严格区分首字母大小写,大写为公有属性,外面可以访问到,小写为私有,外面访问不到。
//reflectvalue_3.Elem().Field(2).SetString("小利") //❌
reflectvalue_3.Elem().Field(0).SetString("小利")
fmt.Printf("修改字段后属性:%s \n", user_2)
//根据字段名修改
//reflectvalue_3.Elem().FieldByName("age").SetInt(99) //❌
reflectvalue_3.Elem().FieldByName("Name").SetString("---")
fmt.Printf("修改字段后属性:%s \n", user_2)
//获取字段中的tag信息 `json:"Age"`
numFieldCount = reflectvalue_2.NumField()
for i := 0; i < numFieldCount; i++ {
structField := reflectvalue_3.Type().Elem().Field(i)
fmt.Printf("获取字段结构信息, 字段名称:%v, 字段类型:%v, 字段位置:%v, 字段包路径:%v, 字段tag:%v \n",
structField.Name, structField.Type, structField.Index, structField.PkgPath, structField.Tag)
}
}
运行结果
--------基本数据类型反射--------
reflectType = int
reflectType name = int
reflectValue = 100,reflectValue type = reflect.Value
n1 = 200
num = 100 true
----int, int, int
number is int
--------结构体反射--------
reflectType_2 = main.User
reflectvalue_2 = {无名 山洞 18},reflectvalue_2 type = reflect.Value
iV_2 value=100, type=int
获取到结构体字段数量:3
第 1 个字段值 = 无名, 类别 = string
第 2 个字段值 = 山洞, 类别 = string
第 3 个字段值 = 18, 类别 = int
获取到结构体方法数量:1
第 1 个方法地址 = 0x100b34e50, 类别 = func
无参数方法 通过反射进行调用:{无名 山洞 18}
有参数方法 通过反射进行调用:&{%!d(string=stu01) %!d(string=山洞) 18}
修改字段前属性:{stu01 山洞 %!s(int=18)}
修改字段后属性:{小利 山洞 %!s(int=18)}
修改字段后属性:{--- 山洞 %!s(int=18)}
获取字段结构信息, 字段名称:Name, 字段类型:string, 字段位置:[0], 字段包路径:, 字段tag:
获取字段结构信息, 字段名称:address, 字段类型:string, 字段位置:[1], 字段包路径:main, 字段tag:
获取字段结构信息, 字段名称:age, 字段类型:int, 字段位置:[2], 字段包路径:main, 字段tag:json:"Age"
网络编程
Golang的主要 设计目标之一就是面向大规模后端服务程序,网络通信这块是服务端 程序必不可少也是至关重要的一部分。在日常应用中,我们也可以看到Go中的net以及其subdirectories下的包均是“高频+刚需”,而TCP socket则是网络编程的主流,即便您没有直接使用到net中有关TCP Socket方面的接口,但net/http总是用到了吧,http底层依旧是用tcp socket实现的。
以tcp为例,服务端建立监听套接字,然后阻塞等待客户端连接。客户端连接后,开启协程处理客户端。
服务端
package main
import (
"fmt"
"net"
)
func process_client(conn net.Conn) {
for {
var bytes []byte = make([]byte, 1024)
n, err := conn.Read(bytes)
// 从客户端读取数据,阻塞。返回读取的字节数
if err != nil {
fmt.Println("Read from client error:", err)
fmt.Println("Connection with ", conn.RemoteAddr().String(), " down")
break
}
fmt.Println(string(bytes[:n])) // 字节切片转string
}
}
func main() {
fmt.Println("Server on..")
listen, err := net.Listen("tcp", "0.0.0.0:9999")
// 建立tcp的监听套接字,监听本地9999号端口
if (err != nil) {
fmt.Println("Server listen error..")
return
}
defer listen.Close()
for {
fmt.Println("Waiting for client to connect..")
conn, err := listen.Accept() // 等待客户端连接
if err != nil {
fmt.Println("Client connect error..")
continue
}
defer conn.Close()
fmt.Println("Connection established with ip:", conn.RemoteAddr().String()) // 获取远程地址
go process_client(conn)
}
}
客户端
直接连接服务端,然后通过连接套接字发送信息即可
package main
import (
"fmt"
"net"
"bufio"
"os"
"strings"
)
func main() {
conn, err := net.Dial("tcp", "localhost:9999") // 和本地9999端口建立tcp连接
if err != nil {
fmt.Println("Connect to server failure..")
return
}
fmt.Println("Connected to server whose ip is ", conn.RemoteAddr().String())
reader := bufio.NewReader(os.Stdin) // 建立控制台的reader
for {
line, err := reader.ReadString('\n') // 读取控制台一行信息
if err != nil {
fmt.Println("Read String error :", err)
}
line = strings.Trim(line, "\r\n")
if line == "quit" {
break
}
_, err = conn.Write([]byte(line)) // 向服务端发送信息,返回发送的字节数和错误
if err != nil {
fmt.Println("Write to server error:", err)
}
}
}