极速Go语言入门(超全超详细)-进阶篇

news2024/10/6 11:40:44

基础篇可访问此链接:
基础篇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)
	}
}

执行可得

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bKSWM02r-1669892980925)(/Users/dasouche/Library/Application Support/typora-user-images/image-20221201100416911.png)]

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通常作为其它结构体的匿名字段使用,使该结构体具有LockUnlock方法。
​ 上面的代码我们加一个锁(需要对多个协程或者线程操作的值上锁)之后再看结果

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 
 获取到结构体字段数量:31 个字段值 = 无名, 类别 = string2 个字段值 = 山洞, 类别 = string3 个字段值 = 18, 类别 = int 
获取到结构体方法数量:11 个方法地址 = 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)
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/52062.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第二证券|券商12月金股出炉!多只地产股成热门,科创仍是中长期主线

跟着券商12月金股战略陈述连续出炉&#xff0c;主流组织的配备风向也浮出水面。 到券商我国记者发稿时&#xff0c;已有10多家券商发布12月金股战略陈述&#xff0c;从职业散布来看&#xff0c;信息技术、工业范畴的金股数量最多&#xff0c;其次是材料、可选消费、医疗。值得一…

前端二倍图

物理像素&物理像素比&#xff1a; 物理像素点指的是屏幕显示的最小颗粒&#xff0c;是物理真实存在的&#xff0c;这是厂商在出厂时设置好了我们开发时候1px不是一定等于1个物理像素的Pc端页面&#xff0c;1px等于1个物理像素点&#xff0c;但是移动端就不尽相同一个px能显…

打包发布自己的app

创建自己的app 一、 安装HBuilderX 二、 引入代码&#xff0c;引入组件 三、 配置app信息 四、 云打包 1、第一步 2、第二步 3、证书创建是用的jdk8创建的&#xff0c;软件里带教程&#xff0c;也可以用公共测试证书。 五、打出的包是apk文件&#xff0c;配合我的搭建网…

ROG幻15电脑开机自动安装软件怎么U盘重装系统

ROG幻15电脑开机自动安装软件怎么U盘重装系统。今天和大家一起来分享如何解决ROG幻15电脑开机的时候会自动安装软件的问题。用户反馈开机之后自动后台安装很多软件&#xff0c;导致无法操作卡死。这个情况我们可以使用U盘来重装一些系统&#xff0c;这样就可以解决问题&#xf…

(二)正则表达式——捕获

&#xff08;二&#xff09;正则表达式——捕获 正则捕获的懒惰性 实现正则捕获的方法&#xff1a;exec exec返回的结果&#xff1a; 懒惰性 这就是正则捕获的懒惰性&#xff1a;默认只捕获第1个 lastIndex&#xff1a;下次匹配的开始位置 懒惰的原因&#xff1a;默认lastIndex…

视频播放 (一) VideoView的使用

1. 配置参数 1.1 AndroidManifest.xml 文件添加网络权限 <uses-permission android:name"android.permission.INTERNET" /> 1.2 http 明文请求设置 android:usesCleartextTraffic"true" 1.3 activity 配置屏幕变化&#xff0c;不重新加载 Activity …

多数据中心多活相关知识

Cell&#xff1a;业务可封闭收敛最小执行分片&#xff1b;业务对请求空间按一定维度&#xff08;比如会员、门店等&#xff09;划分分片。 LDC&#xff1a;逻辑数据中心&#xff0c;是由多个业务可封闭 cell 组成的集合单元&#xff0c;拥有独立的基础中间件系统&#xff08;包…

树莓派4b通过docker安装部署jenkins

借鉴&#xff1a;https://blog.csdn.net/wz_coming/article/details/113523610 树莓派的docker安装及其他操作请看&#xff1a;https://blog.csdn.net/weixin_44578029/article/details/127987795 前言 我的环境是树莓派4b&#xff0c;安装的官方64 debian11系统 arm架构 4h…

[附源码]SSM计算机毕业设计疫情状态下病房管理平台JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Kafka第一讲:应用场景及架构设计详解

本节是Kafka专题第一篇&#xff0c;主要介绍Kafka的发展历史、应用场景以及Kafka的基本架构&#xff0c;后续还会对Kafka的生产者、Broker、消费者、集群做详细讲解&#xff0c;敬请期待。 1.kafka的发展历史及应用场景 1.1kafka的定位 可以实现如下功能&#xff1a; 1.2为什…

『航班乘客满意度』场景数据分析建模与业务归因解释 ⛵

&#x1f4a1; 作者&#xff1a;韩信子ShowMeAI &#x1f4d8; 数据分析实战系列&#xff1a;https://www.showmeai.tech/tutorials/40 &#x1f4d8; 机器学习实战系列&#xff1a;https://www.showmeai.tech/tutorials/41 &#x1f4d8; 本文地址&#xff1a;https://www.sho…

vue+elementUI实现级联表格el-table级联多选

vueelementUI实现级联表格el-table级联多选 <template><div id"app"><el-button type"primary" click"getAllSelect()">获取选中集合</el-button><el-table:data"renderDynamic"ref"lendontable"…

DataFun: 微信NLP算法微服务治理

管理问题 性能问题 PyInter&#xff1a;暂未开源&#xff0c;有开源打算 调度问题 P50&#xff1a; 响应的中位数P999&#xff1a;耗时最慢的千分之一 让p999下降为p50的1.5倍

DIY正则图片下载工具

一、初心&#xff1a;如果您擅长正则表达式&#xff0c;可以自定义抓取自定义网页的图片。 二、效果&#xff1a; 目前支持 <img>标签抓取图片正则。更多正则欢迎分项。支持base64图片预览。 三、使用方法&#xff1a; 修改正则表达式&#xff1a;选中即可。同时工具几…

Springboot毕业设计毕设作品,纯净水销售配送管理系统设计与实现

功能清单 在系统里面我们将纯净水的产品统称为商品 【后台管理员功能】 广告管理&#xff1a;设置小程序首页轮播图广告和链接 留言列表&#xff1a;所有用户留言信息列表&#xff0c;支持删除 会员列表&#xff1a;查看所有注册会员信息&#xff0c;支持删除 资讯分类&#…

基于Abaqus-Simpack联合仿真车辆-浮置板轨道耦合动力学仿真

作者&#xff1a; CAE兮枫如秋 仿真秀专栏作者 一、城市轨道交通中钢弹簧浮置板高级减振轨道 城市轨道交通不仅作为城市亮丽的名片&#xff0c;还在解决城市交通问题中具有特殊的地位和作用。城市轨道交通也是一种安全、快捷、准时、方便、舒适的理想交通工具。伴随着全世界各…

【MM小贴士】SAP创建成本中心采购订单带出默认会计科目和成本中心

在实施SAP项目梳理MM模块业务需求的时候&#xff0c;很多公司都会有这样需求&#xff0c;就是在创建成本中心采购订单的时候&#xff0c;因为成本中心和会计科目是必须输的&#xff0c;所以用户希望系统能够自动带出默认的会计科目和成本中心&#xff08;如下图&#xff09;。 …

14.前端笔记-CSS-浮动

1、传统网页布局的三种方式 网页布局的本质&#xff1a;用CSS摆放盒子 传统布局方式&#xff1a; - 普通流&#xff08;标准流&#xff09;- 浮动- 定位1.1 普通流&#xff08;文档流/标准流&#xff09; 就是标签按照规定好默认方式排列 &#xff08;1&#xff09;块级元素独…

项目前的知识回顾

杂谈 什么是框架 应用方面&#xff1a;框架是整个或者部分系统的可重用设计 目的方面&#xff1a;框架是可被开发者定制的应用骨架 统一的舞台&#xff0c;不同人表演不同的节目 框架解决什么问题 框架主要解决技术整合的问题 MYBATIS 什么是Mybatis Mybatis是一款半自动…

Ubuntu20.4安装QT6

前言&#xff1a; 本教程基于Ubuntu20.4&#xff0c;在Ubuntu22.4上也测试过。Ubuntu18.04由于GCC版本太低&#xff0c;无法正常工作。 1.下载QT安装程序&#xff1a; Open Source Development | Open Source License | Qt 2.安装libxcb-xinerama(必须在执行QT安装程序前执行…