Go语言进阶重点-2

三、Go进阶总结

1. goroutine

1.1 进程与线程:

1.2 并发与并行

  1. 多线程程序在单核上运行,就是并发
  2. 多线程程序在多核上运行,就是并行

1.3 Go协程(goroutine)和Go主线程

Go 主线程(有程序员直接称为线程/也可以理解成进程): 一个Go 线程上,可以起多个协程,可以这样理解,协程是轻量级的线程[编译器做优化]。

理解: Go主线程 类比于 进程 Go协程 类比于 优化后的线程

Go 协程的特点:

  1. 独立的栈空间
  2. 共享程序堆空间
  3. 调度由用户控制
  4. 协程是轻量级的线程

1.4 进程、线程、协程的进一步理解

进程、线程、协程对比

通俗描述

有一个老板想要开个工厂进行生产某件商品(例如剪子)
他需要花一些财力物力制作一条生产线,这个生产线上有很多的器件以及材料这些所有的 为了能够生产剪子而准备的资源称之为:进程

只有生产线是不能够进行生产的,所以老板的找个工人来进行生产,这个工人能够利用这些材料最终一步步的将剪子做出来,这个来做事情的工人称之为:线程

这个老板为了提高生产率,想到3种办法:

方式1

在这条生产线上多招些工人,一起来做剪子,这样效率是成倍増长,即单进程 多线程

方式 2
老板发现这条生产线上的工人不是越多越好,因为一条生产线的资源以及材料毕竟有限,所以老板又花了些财力物力购置了另外一条生产线,然后再招些工人这样效率又再一步提高了,即多进程 多线程

方式 3
老板发现,现在已经有了很多条生产线,并且每条生产线上已经有很多工人了(即程序是多进程的,每个进程中又有多个线程),为了再次提高效率,老板想了个损招,
规定:如果某个员工在上班时临时没事或者再等待某些条件(比如等待另一个工人生产完谋道工序 之后他才能再次工作) ,那么这个员工就利用这个时间去做其它的事情, 那么也就是说:如果一个线程等待某些条件,可以充分利用这个时间去做其它事情,其实这就是:协程方式

简单总结
1 进程是资源分配的单位
2 线程是操作系统调度的单位
3 进程切换需要的资源很最大,效率很低
4 线程切换需要的资源一般,效率一般
5 协程切换任务资源很小,效率高
6 多进程、多线程根据cpu核数不一样可能是并行的 也可能是并发的协程的本质就是使用当前进程在不同的函数代码中切换执行,可以理解为并行。 协程是一个用户层面的概念,不同协程的模型实现可能是单线程,也可能是多线程

进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈进程由操作系统调度。(全局变量保存在堆中,局部变量及函数保存在栈中)

线程拥有自己独立的栈和共享的堆,共享堆,不共享栈线程亦由操作系统调度(标准线程是这样的)。

协程和线程一样共享堆,不共享栈协程由程序员在协程的代码里显式调度

一个应用程序一般对应一个进程,一个进程一般有一个主线程,还有若干个辅助线程,线程之间是平行运行的,在线程里面可以开启协程,让程序在特定的时间内运行。

协程和线程的区别是:

协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力

1.5 协程的使用:

传统的写法,会先执行test,在执行主线程下面的内容

func test()  {
    
    
	for i:=0; i<=10; i++ {
    
    
		fmt.Println("test : Hello")
		time.Sleep(time.Second)	//每一秒休眠一次
	}
}

func main() {
    
    
	test()

	for i:=0; i<=10; i++ {
    
    
		fmt.Println("main : Hello")
		time.Sleep(time.Second)	//每一秒休眠一次

	}
}

使用协程的写法:

func test()  {
    
    
	for i:=0; i<=10; i++ {
    
    
		fmt.Println("test : Hello")
		time.Sleep(time.Second)	//每一秒休眠一次
	}
}

func main() {
    
    
	go test()  //只需要加个go  开启一个协程

	for i:=0; i<=10; i++ {
    
    
		fmt.Println("main : Hello")
		time.Sleep(time.Second)	//每一秒休眠一次

	}
}


//交替调用  test与主线程同时执行
test : Hello 0
main : Hello 0
main : Hello 1
test : Hello 1
test : Hello 2
main : Hello 2
main : Hello 3
test : Hello 3
test : Hello 4
main : Hello 4
test : Hello 5
main : Hello 5
test : Hello 6
main : Hello 6
test : Hello 7
main : Hello 7
test : Hello 8
main : Hello 8
test : Hello 9

程序执行的流程图:

协程的结束与否以主线程的运行为准

  1. 主线程是一个物理线程,直接作用在cpu 上(操作系统控制)的。是重量级的,非常耗费cpu 资源

  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。

  3. Golang 的协程机制是重要的特点,可以轻松的开启上万个协程其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang 在并发上的优势了

理解:其实协程就是轻量级的线程,golang通过程序员控制这样的协程去避免大量线程的创建,从而实现强大的并发功能

1.6 goroutine的调度模型

MPG 模式基本介绍:

MPG 模式运行的状态1

MPG 模式运行的状态2

理解:动态的为单个线程中的协程队列切换其他线程(在线程池中取)执行

1.7 设置Golang 运行的cpu 数

学习包:runtime

func main() {
    
    
  //获取当前系统逻辑CPU个数
	numCPU := runtime.NumCPU()
	fmt.Println(numCPU)

	//设置最多运行的cpu个数
	runtime.GOMAXPROCS(runtime.NumCPU() - 1)
	fmt.Println("ok")
}

2.channel管道

2.1 协程资源共享问题

例子:需求:现在要计算1-200 的各个数的阶乘,并且把各个数的阶乘放入到map 中。最后显示出来。要求使用goroutine 完成

var newMap = make(map[int]int, 100)

func Cal(a int) {
    
    
	var res int
	for i := 1; i <= a; i++ {
    
    
		res *= i
	}
	//写入
	newMap[a] = res
}


func main() {
    
    
	for i:=1; i<=200; i++ {
    
    
		//多协程计算
		go Cal(i)
	}
	for i, v := range newMap {
    
    
		fmt.Println(i," : ", v)
	}
}

代码逻辑上没有问题,但是程序执行起来就会出现问题:资源竞争

go判断是否会出现资源竞争问题:在go run 运行的时候添加参数 -race

  1. 使用goroutine 来完成,效率高,但是会出现并发/并行安全问题.
  2. 这里就提出了不同goroutine 如何通信的问题

多个协程同时对一个map空间写入:

2.2 解决方法:

2.2.1 全局变量锁

加入全局变量锁机制

包:sync

代码:

var (
	newMap = make(map[int]int, 100)
	lock sync.Mutex
)



func Cal(a int) {
    
    
	res := 1
	for i := 1; i <= a; i++ {
    
    
		res *= i
	}
	//写入
	lock.Lock()  //锁住资源
	newMap[a] = res
	lock.Unlock() //写入完毕后,解锁
}


func main() {
    
    
	for i:=1; i<=10; i++ {
    
    
		//多协程计算
		go Cal(i)
	}

	time.Sleep(time.Second * 5) //添加睡眠,让主线程等待协程结束

	for i, v := range newMap {
    
    
		fmt.Println(i," : ", v)
	}
}

这种方法不是推荐的方法,因为不知道主进程要等多久,channel是更好的办法

1.主线程在等待所有goroutine 全部完成的时间很难确定,我们这里设置10 秒,仅仅是估算。

2.如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有goroutine 处于工作
状态,这时也会随主线程的退出而销毁

3.通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。

4.上面种种分析都在呼唤一个新的通讯机制-channel

主进程需要等待所有的协程都跑完了才能继续执行,但是这样的等待是不可知晓的,要用特殊的方法去实现

2.3 channel

2.3.1 概念

  1. channel 本质就是一个数据结构-队列【示意图】
  2. 数据是先进先出**【FIFO : first in first out】**
  3. 线程安全,多goroutine 访问时,不需要加锁,就是说channel 本身就是线程安全的。可认为不管多少协程通过管道去操作共享资源都可以认为是安全的,由底层编译器实现
  4. channel 有类型的,一个string 的channel 只能存放string 类型数据。

2.3.2 基本语法

  • var 变量名 chan 数据类型
  • 举例:
    var intChan chan int (intChan 用于存放int 数据)
    var mapChan chan map[int]string (mapChan 用于存放map[int]string 类型)
    var perChan chan Person
    var perChan2 chan *Person

说明:

  1. channel 是引用类型

  2. channel 必须初始化(make)才能写入数据, 即make 后才能使用

  3. 管道是有类型的,intChan 只能写入整数int

  4. channel是在创建时固定大小的,不能够自动扩容(能自动扩容点的目前就是map)

func main() {
    
    
	//创建一个channel
	var intChannel chan int
	//初始化
	intChannel = make(chan int, 3)
	//输出一下
	fmt.Println(intChannel)
	fmt.Println("容量:",cap(intChannel), len(intChannel)) //3 0
	//加入数据进管道
	intChannel<- 1
	num := 55
	intChannel<- num
	intChannel<- 108
	//输出容量
	fmt.Println("容量:",cap(intChannel), len(intChannel))// 3 2
	//取管道,读取数据
	num2 := <-intChannel
	fmt.Println(num2)		//108
	num3 := <-intChannel
	fmt.Println(num3)		//55
	//也可以取出来不处理
	<-intChannel
	fmt.Println("容量:",cap(intChannel), len(intChannel))// 3 0
}

2.3.3 注意事项

  1. channel 中只能存放指定的数据类型
  2. channle 的数据放满后,就不能再放入了
  3. 如果从channel 取出数据后,可以继续放入
  4. 在没有使用协程的情况下,如果channel 数据取完了,再取,就会报dead lock

2.3.4 案例演示

  • 放Map

  • 结构体

  • 指针

  • 存放任何数据类型,使用空接口

注意:使用空接口取出数据时一定要注意类型断定

func main() {
    
    
   var allChannel chan interface{
    
    }
   allChannel = make(chan interface{
    
    }, 20)
   dog := Dog{
    
    
      Name:  "xx",
      Age:   10,
      Phone: 10,
      sex:   "mu",
   }
   allChannel<- dog
   dog2 := &Dog{
    
    
      Name:  "哈哈哈",
      Age:   20,
      Phone: 20,
      sex:   "lal",
   }
   allChannel<- dog2
   allChannel<- 10
   allChannel<- "shjdhaj"
   pop := <- allChannel
   //取出
   //弹出的类型main.Dog, 值:{%!V(string=xx) %!V(int=10) %!V(int8=10) %!V(string=mu)}
   fmt.Printf("弹出的类型%T, 值:%V\n", pop, pop)
   //重点!!!!虽然运行期把底层编译器会识别interface为Dog类型,但是在编译器编译器不知道,所以会直接报错
   //fmt.Println(pop.Name)
   //解决方法:类型断言
   a := pop.(Dog) //断言是这个类型
   fmt.Println(a.Name)
}

2.3.5 channel的关闭与遍历

1. 关闭

使用内置函数close 可以关闭channel, 当channel 关闭后,就不能再向channel 写数据了,但是仍然可以从该channel 读取数据

func main() {
    
    
	intChan := make(chan int, 20)
	intChan<- 1
	intChan<- 44
	intChan<- 56
	intChan<- 56
	intChan<- 46
	intChan<- 7
	intChan<- 6
	//读出数据
	fmt.Println(<-intChan)
	close(intChan)			//panic: send on closed channel
	intChan<- 55  			//写入
	fmt.Println(<-intChan)	//读取
}

关闭可以看做在管道最后加入一个“EOF”标志能够防止死锁:当管道中的最后一个元素被取出去,下一次管道中已经没有值了,协程只能死死的等待取管道的值从而陷入死锁。

2. 遍历

不要使用普通的for循环遍历

channel 支持for–range 的方式进行遍历,请注意两个细节

  1. 在遍历时,如果channel 没有关闭,则回出现deadlock 的错误
  2. 在遍历时,如果channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
func main() {
    
    
	intChannel := make(chan int, 100)
	//写入数据
	for i:=1; i<=100; i++ {
    
    
		intChannel<-i
	}
	//遍历管道  不能用len,因为每次都会弹出一个len也是递减的
	//for i:=1; i<=len(intChannel); i++ {
    
    
	//	fmt.Println(<-intChannel)
	//}

	//可以用cap,但是不建议
	//for i:=1; i<=cap(intChannel); i++ {
    
    
	//	fmt.Println(<-intChannel)
	//}
	close(intChannel)			//在遍历之前关闭管道则会正确输出,理解为在末尾添加EOF标识
	for v := range intChannel{
    
    
		fmt.Println(v)			//fatal error: all goroutines are asleep - deadlock! 如果没有关闭则会出现死锁
	}
}

==>知道那个协程能够做完时就使用管道去关闭它

2.4 goroutine与channel协同工作

重点!!!(这是一种方法,必须将管道关闭,后面还有select的方法)

func WriteData(intChannel chan int)  {
    
    
	for i:=0; i<50; i++ {
    
    
		intChannel <- i //因为管道是线程安全的,不许要加锁就可以多协程共享读取
		fmt.Println("写入数据 ", i)
		//time.Sleep(time.Second * 1) 	//【非必须】等待一下不然写入太快看不到交叉
	}
	//写完了之后就关闭数据管道(打上结束标记),因为读取协程一直在阻塞读取,等待结束
	close(intChannel)
}

func ReadData(intChannel chan int, overChannel chan bool) {
    
    
	for {
    
    
		v, ok := <-intChannel	//读取管道数据
		//当管道数据被读完,就阻塞,直到读取到结束标记(返回的ok表示当前管道是否被关闭了,如果是关闭了那么就返回false)
		if !ok {
    
    
			break
		}
		fmt.Println("读取数据 ", v)
		//time.Sleep(time.Second * 1)		//【非必须】等待一下
	}
	//读取完毕了就写入结束标志,并关闭
	overChannel<-true
	close(overChannel)
}

func main() {
    
    
	//创建两个channel
	var intChannel = make(chan int, 50)			//数据管道
	var overChannel = make(chan bool, 1)		//创建一个新的channel储存读完标记
	//写和读是两个线程,是交互、并行的
	go WriteData(intChannel) 				//创建一个协程写文件进管道
	go ReadData(intChannel, overChannel)	//创建一个协程读取文件
	//主进程不等待上面的两个协程运行,继续跑!
	//主进程一直阻塞读取该channel,直到读取到值并且为true则代表go协程跑完了,那么主进程就可以继续了
	for {
    
    
		res, ok := <-overChannel
		if !ok {
    
    
			break
		}
		fmt.Println("等待goroutine中。。。。。", res)	//读到true时会输出
	}
	//要分析一下:
	//if !<-overChannel{
    
    
	//	fmt.Println("等待goroutine中。。。。。")
	//}
	fmt.Println("主进程结束!")
}

2.5 Waitgroup协程退出的第二种方式

sync包中的Waitgroup结构,是Go语言为我们提供的多个goroutine之间同步的好刀。下面是官方文档对它的描述:

A WaitGroup waits for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for. 
Then each of the goroutines runs and calls Done when finished. At the same time, Wait can be used to block until all goroutines have finished.

通常情况下,我们像下面这样使用waitgroup:

  1. 创建一个Waitgroup的实例,假设此处我们叫它wg
  2. 在每个goroutine启动的时候,调用wg.Add(1)这个操作可以在goroutine启动之前调用,也可以在goroutine里面调用。当然,也可以在创建n个goroutine前调用wg.Add(n) 建议在协程启动之前调用不然主进程跑的太快太快可能导致协程还未启动就结束
  3. 每个goroutine完成任务后,调用wg.Done()
  4. 等待所有goroutine的地方调用wg.Wait()它在所有执行了wg.Add(1)的goroutine都调用完wg.Done()前阻塞当所有goroutine都调用完wg.Done()之后它会返回

那么,如果我们的goroutine是一匹不知疲倦的牛,一直孜孜不倦地工作的话,如何在主流程中告知并等待它退出呢?像下面这样做:

type Service struct {
    
    
	// Other things

	ch        chan bool
	waitGroup *sync.WaitGroup
}

func NewService() *Service {
    
    
	s := &Service{
    
    
		// Init Other things
		ch:        make(chan bool),
		waitGroup: &sync.WaitGroup{
    
    },
	}
	return s
}

func (s *Service) Stop() {
    
    
	close(s.ch)
	s.waitGroup.Wait()
}

func (s *Service) Serve() {
    
    
	s.waitGroup.Add(1)
	defer s.waitGroup.Done()

	for {
    
    
		select {
    
    
			case <-s.ch:
				fmt.Println("stopping...")
				return
			default:
		}
		s.waitGroup.Add(1)
		go s.anotherServer()
	}
}

func (s *Service) anotherServer() {
    
    
	defer s.waitGroup.Done()
	for {
    
    
		select {
    
    
			case <-s.ch:
				fmt.Println("stopping...")
				return
			default:
		}

	// Do something
	}
}

func main() {
    
    
	service := NewService()
	go service.Serve()
	// Handle SIGINT and SIGTERM.
	ch := make(chan os.Signal)
	signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
	fmt.Println(<-ch)
	// Stop the service gracefully.
	service.Stop()
}

2.5.1 waitgroup的两个大坑

  1. 在goroutine函数中使用Add并且在末尾Done,但是却goroutine还是被主程序中断了

    原因:主进程执行的太快了,先wait了,协程还没有来得及跑

    修改:Add一般不要写在goroutine函数中

  2. 在goroutine开始之前ADD,使用Waitgroup作为参数传递给goroutine函数,在函数末尾Done

    原因:创建时是:wg := sync.WaitGroup{}, 传参时是值拷贝,所以在函数中Done的只是你的wg副本

    修改:创建时要使用WaitGroup指针!

2.6 练习

练习1

package main

import (
	"fmt"
	"math/big"
	"sync"
)

//多协程求阶乘

const SUM_NUM  = 5000

type Server struct {
    
    
	NumChan  chan int  			//数据channel
	ResChan chan *big.Int		//存放阶乘大数
	Wg sync.WaitGroup			//实例化一个Waitgroup实例
}

func (s *Server) SetData()  {
    
    
	//添加进程ADD
	s.Wg.Add(1)
	defer s.Wg.Done()	//延迟结束

	for i:=1; i<=SUM_NUM; i++ {
    
    
		s.NumChan<- i
	}
	//写入完毕就关闭管道
	close(s.NumChan)
}


func (s *Server) Fun()  {
    
    
	//添加进程ADD
	s.Wg.Add(1)
	defer s.Wg.Done()	//延迟结束

	for  {
    
    
		num, ok := <- s.NumChan
		if !ok {
    
    
			//遇到结束标记就结束
			break
		}
		//计算
		mulRes := big.NewInt(int64(1))
		for i:=1; i<=num; i++ {
    
    
			mulRes.Mul(mulRes, big.NewInt(int64(i)))
		}
		//写入
		s.ResChan<- mulRes
	}
}

func (s *Server) Stop()  {
    
    
	//注意这里的顺序,要先让所有的数据处理协程算完后写入完毕后,再关闭管道
	s.Wg.Wait()			//让主进程等待所有协程结束
	close(s.ResChan)	//关闭运算结果管道
}


func main() {
    
    
	//实例化一个服务
	s := Server{
    
    
		NumChan: make(chan int, SUM_NUM),
		ResChan: make(chan *big.Int, SUM_NUM),
		Wg:      sync.WaitGroup{
    
    },
	}
	go s.SetData()
	//用8个协程去取出数处理
	for i:=1; i<=200; i++ {
    
    
		fmt.Println("开启协程 ", i)
		go s.Fun()
	}
	//结束所有协程
	s.Stop()
	//输出运算结果
	for v := range s.ResChan{
    
    
		fmt.Println(v)
	}
	fmt.Println("主进程结束!")
}

练习2

package main

import (
	"bufio"
	"bytes"
	"encoding/binary"
	"fmt"
	"math/rand"
	"os"
	"sort"
	"strconv"
	"sync"
)

type Fileserver struct {
    
    
	DataChan chan int
	FileName string
	Wg sync.WaitGroup
}

func (s *Fileserver) WriteChanData()  {
    
    
	//预先关闭协程
	defer s.Wg.Done()
	//打开文件
	file, err := os.OpenFile(s.FileName, os.O_WRONLY|os.O_CREATE, 0666)
	if err != nil {
    
    
		fmt.Println("文件打开错误!", err)
	}
	defer file.Close()
	writer := bufio.NewWriter(file)
	defer writer.Flush()
	//生成随机数
	for i:=1; i<=1000; i++ {
    
    
		//生成数据
		data := rand.Intn(1000)
		//写入管道
		s.DataChan<- data
		//写入文件
		_, err := writer.Write([]byte(strconv.Itoa(data) + " "))
		if err != nil {
    
    
			fmt.Println("写入失败!", err)
		}
	}
	//关闭当前的数据channel
	close(s.DataChan)
	fmt.Println(s.FileName, "文件写入完成")

}

//int转Bytes
func IntToBytes(n int) []byte {
    
    
	data := int64(n)
	bytebuf := bytes.NewBuffer([]byte{
    
    })
	binary.Write(bytebuf, binary.BigEndian, data)
	return bytebuf.Bytes()
}

//读取数据,并执行操作
func (s *Fileserver) ReadChanData()  {
    
    
	//预关闭进程
	defer s.Wg.Done()
	//添加协程任务
	s.Wg.Add(1)
	defer s.Wg.Done()
	//创建数组
	intAry := make([]int, 1000)
	for{
    
    
		//阻塞读取,保证写入完毕
		data, ok := <-s.DataChan
		if !ok {
    
    
			break
		}
		intAry = append(intAry, data)
	}
	//操作函数-排序
	sort.Ints(intAry)
	//重新写入文件
	file, err := os.OpenFile(s.FileName, os.O_WRONLY|os.O_TRUNC, 0666)
	defer file.Close()
	if err != nil {
    
    
		fmt.Println("打开文件失败!", err)
	}
	writer := bufio.NewWriter(file)
	defer writer.Flush()
	for v := range intAry{
    
    
		_, err := writer.Write([]byte(strconv.Itoa(v) + " "))
		if err != nil {
    
    
			fmt.Println("写文件失败!", err)
		}
	}
	fmt.Println(s.FileName, "排序后文件写入完毕!")
}

func main() {
    
    
	//创建10个任务
	for i:=1; i<=10; i++ {
    
    
		newFileServer := &Fileserver{
    
    
			DataChan: make(chan int, 1000),
			FileName: "../data/" + "file"  + strconv.Itoa(i) + ".txt",
			Wg:       sync.WaitGroup{
    
    },
		}
		//写入文件
		newFileServer.Wg.Add(1)
		go newFileServer.WriteChanData()
		//读取文件并排序写入
		newFileServer.Wg.Add(1)
		go newFileServer.ReadChanData()
		newFileServer.Wg.Wait()
	}

}

2.7 channel的细节

2.7.1 只读与只写

  • channel 可以声明为只读,或者只写性质

    只写:var intChan chan<- int

    只读:var intChan <-chan int

    应用: 在只需要写/读的函数参数中加入此声明, 这样可以防止误操作,并且效率更加高

    注意:不管是读还是写,管道的类型仍然还是chan int, 只读只写只是一种属性

2.7.2 select解决阻塞

不想关闭管道,但是也不想管道遍历的时候阻塞该怎么办?

select中的case语句即使管道中数据被读完了也不会阻塞而是掉到下一个case去执行

例1

func main() {
    
    
	intChan := make(chan int, 10)
	stringChan := make(chan string, 5)
	for i:=0; i<10; i++ {
    
    
		intChan<- i
	}
	for i:=0; i<5; i++ {
    
    
		stringChan<- "哈喽" + fmt.Sprintf("%d", i)
	}

	exit:
	for{
    
    
		select {
    
    
			//注意:此处即使读完了管道内容,没有结束标记也不会报错死锁阻塞而是继续向下执行,这是select的特殊之处
			case i := <-intChan:
				fmt.Println("intChan", i)
			case s := <-stringChan:
				fmt.Println("stringChan", s)
			default:
				fmt.Println("两个管道都取完了!结束吧")
				break exit			//不加标记的break只是跳出select循环,所以要使用标记或者使用return
		}
	}
}

例2

func run(done chan int) {
    
    
	for {
    
    
		select {
    
    
		case <-done:
			//一开始没有读出内容阻塞,一读出内容就结束了
			fmt.Println("exiting...")
			done <- 1		//写入,让阻塞的一方结束(主进程)
			break
		default:
		}

		time.Sleep(time.Second * 1)
		fmt.Println("do something")
	}
}

func main() {
    
    
	c := make(chan int)

	go run(c)

	fmt.Println("wait")
	time.Sleep(time.Second * 5)

	c <- 1		//写入就代表想让goroutine结束了
	<-c			//阻塞等待goroutine结束,一旦有值就结束

	fmt.Println("main exited")
}

2.7.3 goroutine中panic的捕获

goroutine 中使用recover,解决协程中出现panic,导致程序崩溃问题

注意:一个主进程开启的多个协程中一个协程崩溃panic,那么所有其他协程都会崩溃

=> 解决方法:使用panic捕获 defer+ recover

var wg = &sync.WaitGroup{
    
    }

func test1()  {
    
    
	defer wg.Done()
	for i:=0; i<10; i++ {
    
    
		fmt.Println(i)
	}

}

func test2()  {
    
    
	defer func() {
    
    
		if err := recover(); err!=nil {
    
    
			fmt.Println(err)
		}
	}()

	defer wg.Done()
	//使用defer + recover捕获


	var newMap map[int]string
	newMap[1] = "dsad"  // 错误:没有make就赋值

}

func main() {
    
    
	wg.Add(2)
	go test1()
	go test2()	//一但一个协程发生错误,所有的协程都会崩溃
	wg.Wait()
	fmt.Println("主进程结束!")
}

3. 反射

使用反射机制,编写函数的适配器, 桥连接

应用:反射开发框架

3.1 基本介绍

1)反射可以在运行时动态获取变量的各种信息, 比如变量的类型(type),类别(kind)
2) 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段、方法)
3) 通过反射,可以修改变量的值,可以调用关联的方法
4) 使用反射,需要import (“reflect”)

3.2 应用场景

3.3 反射函数与概念

变量interface{} 和reflect.Value 是可以相互转换的,这点在实际开发中,会经常使用到。示意图:

  • reflect.TypeOf()

    func TypeOf(i interface{}) Type
    

    返回值类型是reflect.Type是一个丰富的接口:type Type

    type Type interface {
        // Kind返回该接口的具体分类
        Kind() Kind
        // Name返回该类型在自身包内的类型名,如果是未命名类型会返回""
        Name() string
        // PkgPath返回类型的包路径,即明确指定包的import路径,如"encoding/base64"
        // 如果类型为内建类型(string, error)或未命名类型(*T, struct{}, []int),会返回""
        PkgPath() string
        // 返回类型的字符串表示。该字符串可能会使用短包名(如用base64代替"encoding/base64")
        // 也不保证每个类型的字符串表示不同。如果要比较两个类型是否相等,请直接用Type类型比较。
        String() string
        // 返回要保存一个该类型的值需要多少字节;类似unsafe.Sizeof
        Size() uintptr
        // 返回当从内存中申请一个该类型值时,会对齐的字节数
        Align() int
        // 返回当该类型作为结构体的字段时,会对齐的字节数
        FieldAlign() int
        // 如果该类型实现了u代表的接口,会返回真
        Implements(u Type) bool
        // 如果该类型的值可以直接赋值给u代表的类型,返回真
        AssignableTo(u Type) bool
        // 如该类型的值可以转换为u代表的类型,返回真
        ConvertibleTo(u Type) bool
        // 返回该类型的字位数。如果该类型的Kind不是Int、Uint、Float或Complex,会panic
        Bits() int
        // 返回array类型的长度,如非数组类型将panic
        Len() int
        // 返回该类型的元素类型,如果该类型的Kind不是Array、Chan、Map、Ptr或Slice,会panic
        Elem() Type
        // 返回map类型的键的类型。如非映射类型将panic
        Key() Type
        // 返回一个channel类型的方向,如非通道类型将会panic
        ChanDir() ChanDir
        // 返回struct类型的字段数(匿名字段算作一个字段),如非结构体类型将panic
        NumField() int
        // 返回struct类型的第i个字段的类型,如非结构体或者i不在[0, NumField())内将会panic
        Field(i int) StructField
        // 返回索引序列指定的嵌套字段的类型,
        // 等价于用索引中每个值链式调用本方法,如非结构体将会panic
        FieldByIndex(index []int) StructField
        // 返回该类型名为name的字段(会查找匿名字段及其子字段),
        // 布尔值说明是否找到,如非结构体将panic
        FieldByName(name string) (StructField, bool)
        // 返回该类型第一个字段名满足函数match的字段,布尔值说明是否找到,如非结构体将会panic
        FieldByNameFunc(match func(string) bool) (StructField, bool)
        // 如果函数类型的最后一个输入参数是"..."形式的参数,IsVariadic返回真
        // 如果这样,t.In(t.NumIn() - 1)返回参数的隐式的实际类型(声明类型的切片)
        // 如非函数类型将panic
        IsVariadic() bool
        // 返回func类型的参数个数,如果不是函数,将会panic
        NumIn() int
        // 返回func类型的第i个参数的类型,如非函数或者i不在[0, NumIn())内将会panic
        In(i int) Type
        // 返回func类型的返回值个数,如果不是函数,将会panic
        NumOut() int
        // 返回func类型的第i个返回值的类型,如非函数或者i不在[0, NumOut())内将会panic
        Out(i int) Type
        // 返回该类型的方法集中方法的数目
        // 匿名字段的方法会被计算;主体类型的方法会屏蔽匿名字段的同名方法;
        // 匿名字段导致的歧义方法会滤除
        NumMethod() int
        // 返回该类型方法集中的第i个方法,i不在[0, NumMethod())范围内时,将导致panic
        // 对非接口类型T或*T,返回值的Type字段和Func字段描述方法的未绑定函数状态
        // 对接口类型,返回值的Type字段描述方法的签名,Func字段为nil
        Method(int) Method
        // 根据方法名返回该类型方法集中的方法,使用一个布尔值说明是否发现该方法
        // 对非接口类型T或*T,返回值的Type字段和Func字段描述方法的未绑定函数状态
        // 对接口类型,返回值的Type字段描述方法的签名,Func字段为nil
        MethodByName(string) (Method, bool)
        // 内含隐藏或非导出方法
    }
    
  • reflect.ValueOf()

    func ValueOf(i interface{}) Value
    

    返回类型reflect.Value是一个空接口,但是有非常多的方法

3.4 使用

3.4.1 三者转化案例

请编写一个案例,演示对**(基本数据类型、interface{}、reflect.Value)**进行反射的基本操作代码演示:

//请编写一个案例,演示对(基本数据类型、interface{}、reflect.Value)进行反射的基本操作代码演示,见下面的表格:
func reflectTest01(b interface{
    
    }){
    
    		//使用interface是因为可以使用任意接口进行反射
	//通过反射拿到一系列信息type类型、kind类别、value值
	//1. 先获取到reflect.Type类型   接口转reflect.Type
	rType := reflect.TypeOf(b) 	//rType是反射的类型而不是参数的类型
	fmt.Println("rType =", rType)	//rType= int  注意这个int不是基本数据类型的int,他是int的接口,可以调取函数
	fmt.Printf("rType的类型是:%T\n", rType)	//rType的类型是:*reflect.rtyperVal =  15
	//2. 获取到reflect.Value		接口转reflect.Value
	rVal := reflect.ValueOf(b)
	fmt.Println("rVal = ", rVal)	//rVal =  15 注意这个15不是普通数字15,而是reflect.Value的结构体类型,不能做算术运算。
	// 输出是100是因为底层的机制输出代表的值
	fmt.Printf("rVal的类型是%T\n", rVal)		//rVal的类型是reflect.Value

	//3. reflect.Value转变量原本类型
	val := rVal.Int()
	fmt.Printf("变量的值是:%d, 变量的类型是:%T\n", val, val)	//变量的值是:15, 变量的类型是:int64
	//但是上方的做法如果传入的参数类型是int那么就会导致错误
	//所以就要使用断言的方法,见下面

	//4. 将reflect.Value转为interface
	iV := rVal.Interface()
	//利用断言转换成为需要的类型
	num2 := iV.(int)
	fmt.Println("num2 = ", num2)
}


func main() {
    
    
	var num int  = 15
	reflectTest01(num)
}

请编写一个案例,演示对**(结构体类型、interface{}、reflect.Value)**进行反射的基本操作

type Rap struct {
    
    
	Name string
	Age int
}

//对结构体的反射案例
func reflectStruct(i interface{
    
    })  {
    
    
	//1. 获取reflect.Type
	rType := reflect.TypeOf(i)
	fmt.Println("rType : ", rType)		//rType :  *main.Rap
	//2. 获取reflect.Value
	rVal := reflect.ValueOf(i)
	fmt.Println("rVal : ", rVal)		//rVal :  &{Gai 30}
	//3. 结构体转换成为interface
	iV := rVal.Interface()
	fmt.Printf("iV=%v, iV的类型%T\n", iV, iV)//iV=&{Gai 30}, iV的类型*main.Rap
	//注意:这个类型是底层运行的时候自动识别出来的,编译器是无法知道其类型的
	//fmt.Println("rap的名字:", iV.Name)	//这样在编译器是无法知道类型的,所以也无法取其字段,这就是要先通过断言再取值的原因
	//4. 断言获取其值
	rap := iV.(Rap)
	fmt.Printf("rap=%v, rap的类型:%T, rap.Name=%v, rap.Age=%v\n", rap, rap, rap.Name, rap.Age)	// 此时就不会错了 rap={Gai 30}, rap的类型:main.Rap, rap.Name=Gai, rap.Age=30

	//5. 多个类型断言可以用switch,或者使用返回值判断断言是否正确
	if str, ok := iV.(string); !ok{
    
    
		fmt.Println("断言失败!", str)
	}
}


func main() {
    
    
	//1. 定义一个int类型的基本数据类型
	//var num int  = 15
	//reflectTest01(num)

	//2. 定义一个结构体数据类型
	newRap := Rap{
    
    
		Name: "Gai",
		Age:  30,
	}
	reflectStruct(newRap)
}

注意:反射是在代码运行过程中起作用的,断言是为了在编译期让编译器知道接口类型的

反射是运行时的反射

3.4.2 Kind类别与Type类型

Kind是类别范围比Type大,例如:类型是自定义类型student,而他的类别就是Struct结构体

Type 是类型, Kind 是类别, Type 和Kind 可能是相同的,也可能是不同的.
比如: var num int = 10 num 的Type 是int , Kind 也是int
比如: var stu Student stu 的Type 是pkg1.Student , Kind 是struct

type Kind

type Kind uint

Kind代表Type类型值表示的具体分类。零值表示非法分类。

const (
    Invalid Kind = iota
    Bool
    Int
    Int8
    Int16
    Int32
    Int64
    Uint
    Uint8
    Uint16
    Uint32
    Uint64
    Uintptr
    Float32
    Float64
    Complex64
    Complex128
    Array
    Chan
    Func
    Interface
    Map
    Ptr
    Slice
    String
    Struct
    UnsafePointer
)

反射中获取Kind:

reflect.Type接口具有该函数和reflect.Value接口具有该方法,都可以获得Kind

func reflectTest02(b interface{
    
    }){
    
    

	rType := reflect.TypeOf(b) 	//rType是反射的类型而不是参数的类型

	rVal := reflect.ValueOf(b)

	kind1 := rType.Kind()
	kind2 := rVal.Kind()
	fmt.Println(kind1, kind2)	//int int

}

3.4.3 反射修改变量

在改变之前注意:想要修改有效果(修改到原值),那么一定函数要传递值的指针而不是本身

既然是指针直接使用set等方法是不行的,需要使用的方法:

func (Value) Elem

func (v Value) Elem() Value

Elem返回v持有的接口保管的值的Value封装,或者v持有的指针指向的值的Value封装。如果v的Kind不是Interface或Ptr会panic;如果v持有的值为nil,会返回Value零值。

理解:Elem()方法可以理解为类似于指针取数的*号,如果reflect.Value的类别是Ptr(指针)的话都要使用此方法才能改值,否则报错

//通过反射修改student的值
func reflectTest03(i interface{
    
    })  {
    
    
	//rType := reflect.TypeOf(i)
	rVal := reflect.ValueOf(i)
	//输出rVal的类型
	fmt.Printf("rVal的类型是%T,rVal的类别是%v\n", rVal, rVal.Kind()) //rVal的类型是reflect.Value,rVal的类别是ptr(指针)
	//rVal的类别是指针类别
	//rVal.SetInt(20)   //直接这样写报错
	rVal.Elem().SetInt(25) //正确的写法
}


func main() {
    
    
	num := 15
	fmt.Println(num)	//15
	reflectTest03(&num)
	fmt.Println(num)	//25
}

3.4.4 反射调取方法

核心的两个方法:

注意1: Method的参数i是按方法名的ASCII排序下来的,并不是定义时的顺序

注意2: 调用函数Call方法,参数是reflect.Value类型的切片,返回值也是该类型的切片!所以可以说调用函数时的基本类型就是Value

其他要使用的方法:

func (v Value) Field(i int) Value

​ 返回结构体的第i个字段(的Value封装)。如果v的Kind不是Struct或i出界会panic

func (v Value) NumField() int

返回v持有的结构体类型值的字段数,如果v的Kind不是Struct会panic

  • 结构体字段标签相关
type Type interface {
	......
    Field(i int) StructField
        // 返回索引序列指定的嵌套字段的类型,
        // 等价于用索引中每个值链式调用本方法,如非结构体将会panic
    ......
}

type StructField

type StructField struct {
    // Name是字段的名字。PkgPath是非导出字段的包路径,对导出字段该字段为""。
    // 参见http://golang.org/ref/spec#Uniqueness_of_identifiers
    Name    string
    PkgPath string
    Type      Type      // 字段的类型
    Tag       StructTag // 字段的标签
    Offset    uintptr   // 字段在结构体中的字节偏移量
    Index     []int     // 用于Type.FieldByIndex时的索引切片
    Anonymous bool      // 是否匿名字段
}

StructField类型描述结构体中的一个字段的信息。

func (StructTag) Get

func (tag StructTag) Get(key string) string

Get方法返回标签字符串中键key对应的值。如果标签中没有该键,会返回""。如果标签不符合标准格式,Get的返回值是不确定的。

案例 : 使用反射来遍历结构体的字段,调用结构体的方法,并获取结构体标签的值

type Teacher struct {
    
    
	Name string	`json:"name"`
	Age int		`json:"age"`
	ID int		`json:"id"`
	Classroom int	`json:"classroom"`
	Salary float64	`json:"salary"`
}

//结构体的三个方法

func (t Teacher) Print()  {
    
    
	fmt.Println("-------Start--------")
	fmt.Println(t)
	fmt.Println("-------End--------")
}


func (t Teacher) GetSum(i, j int) int {
    
    
	return i + j
}

func (t Teacher) Set(name string, age int, id int, classroom int, salary float64)  {
    
    
	t.Name = name
	t.Age = age
	t.ID = id
	t.Classroom = classroom
	t.Salary = salary
}


//反射方法
//遍历结构体的字段,调用结构体的方法,并获取结构体标签的值
func Reflect(i interface{
    
    })  {
    
    
	typeOf := reflect.TypeOf(i)
	valueOf := reflect.ValueOf(i)
	kd := valueOf.Kind()

	//1. 判断是否是结构体  注意判断时要用reflect包使用
	if kd != reflect.Struct {
    
    
		fmt.Println("不是个结构体!")
		return
	}
	//2. 获取结构体字段总数
	sum := valueOf.NumField()
	//3. 遍历字段
	for i:=0; i < sum; i++ {
    
    
		fmt.Printf("字段%d: %v\n", i, valueOf.Field(i))
		//获取struct的tag字段
		tagVal := typeOf.Field(i).Tag.Get("json")	//注意json可以不是写死的,但是反序列化写死是json,所以一般不改
		if tagVal != "" {
    
    
			fmt.Printf("字段%d的标签为:%v\n", i, tagVal)
		}
	}

	//4. 获取到有多少方法
	Msum := valueOf.NumMethod()
	fmt.Printf("一共有%d个方法", Msum)
	//5. 获取到第二个方法 并执行  调用的是Print方法
	valueOf.Method(1).Call(nil)

	//6. 调用多参数方法
	var valueSlice []reflect.Value	//创建Value切片
	valueSlice = append(valueSlice, reflect.ValueOf(10), reflect.ValueOf(20))	//加入到切片中
	values := valueOf.Method(0).Call(valueSlice)	// 调用
	res := values[0].Interface().(int)   //!!!!难点;断言结果类型, 记得返回值也是一个切片,所以取下标;然后需要转换成为interface才能断言
	fmt.Printf("函数%s的运行结果是:%d\n", typeOf.Method(0).Name, res)

}


func main() {
    
    
	newTeacher := Teacher{
    
    
		Name:      "马保国",
		Age:       69,
		ID:        012,
		Classroom: 10,
		Salary:    100000,
	}
	Reflect(newTeacher)
}

3.4.5 反射修改结构体字段案例

核心思路:传递指针,并且在反射函数中使用Elem(), 不然会报错(如果是方法则可以直接调用,这类似于*p.fun() 与 p.fun() 是相同的,来自于Go底层的优化解析)

import (
	"fmt"
	"reflect"
)

//使用反射的方式来获取结构体的tag 标签, 遍历字段的值,修改字段值,调用结构体方法


type Monster struct {
    
    
	Name string	`json:"name"`
	Age int		`json:"age"`
	ID int		`json:"id"`
	Classroom int	`json:"classroom"`
	Salary float64	`json:"salary"`
}

//结构体的三个方法

func (t Monster) Print()  {
    
    
	fmt.Println("-------Start--------")
	fmt.Println(t)
	fmt.Println("-------End--------")
}


func (t Monster) GetSum(i, j int) int {
    
    
	return i + j
}

func (t Monster) Set(name string, age int, id int, classroom int, salary float64)  {
    
    
	t.Name = name
	t.Age = age
	t.ID = id
	t.Classroom = classroom
	t.Salary = salary
}


//反射方法
//遍历结构体的字段,调用结构体的方法,并获取结构体标签的值
func Reflect02(i interface{
    
    })  {
    
    
	typeOf := reflect.TypeOf(i)
	valueOf := reflect.ValueOf(i)
	kd := valueOf.Kind()

	//1. 判断是否是结构体指针  注意判断时要用reflect包使用
	if kd != reflect.Ptr {
    
    
		fmt.Println("不是个结构体!")
		return
	}
	//2. 获取结构体字段总数
	sum := valueOf.Elem().NumField()		//注意要加上ELem()
	//3. 遍历字段
	for i:=0; i < sum; i++ {
    
    
		fmt.Printf("字段%d: %v\n", i, valueOf.Elem().Field(i))
		//获取struct的tag字段
		tagVal := typeOf.Elem().Field(i).Tag.Get("json")	//注意json可以不是写死的,但是反序列化写死是json,所以一般不改
		if tagVal != "" {
    
    
			fmt.Printf("字段%d的标签为:%v\n", i, tagVal)
		}
	}

	//4. 获取到有多少方法
	Msum := valueOf.Elem().NumMethod()
	fmt.Printf("一共有%d个方法", Msum)
	//5. 获取到第二个方法 并执行  调用的是Print方法
	valueOf.Method(1).Call(nil)

	//6. 调用多参数方法
	var valueSlice []reflect.Value	//创建Value切片
	valueSlice = append(valueSlice, reflect.ValueOf(10), reflect.ValueOf(20))	//加入到切片中
	values := valueOf.Method(0).Call(valueSlice)	// 调用
	res := values[0].Interface().(int)   //!!!!难点;断言结果类型, 记得返回值也是一个切片,所以取下标;然后需要转换成为interface才能断言
	fmt.Printf("函数%s的运行结果是:%d\n", typeOf.Method(0).Name, res)


	//7. 修改字段值
	age := valueOf.Elem().FieldByName("Age")
	fmt.Println("原本的Age字段值:", age)		//69
	valueOf.Elem().FieldByName("Age").SetInt(99) 	//重新设置值
	fmt.Println("现在的Age字段值:", age)		//99
}


func main() {
    
    
	newTeacher := &Monster{
    
    
		Name:      "马保国",
		Age:       69,
		ID:        012,
		Classroom: 10,
		Salary:    100000,
	}
	Reflect02(newTeacher) 	//注意此处传递的是指针
	fmt.Println("外部输出的字段值:", newTeacher.Age)	//99
}

反射的理解:以前结构体执行方法是创建结构体实例执行方法,而通过反射则可以创建实例,交给反射,反射会帮你执行所有需要的步骤 ===>底层框架

很多的框架规范(比如函数名必须以xxx开头,都是因为反射机制在后面要求好的)

3.4.6 函数适配器

定义了两个函数test1 和test2,定义一个适配器函数用作统一处理接口

import (
	"reflect"
	"testing"
)

func TestFun(t *testing.T)  {
    
    
	//定义两个函数

	call1 := func(i, j int) {
    
    
		t.Log(i, j)
	}

	call2 := func(i, j int, s string) {
    
    
		t.Log(i, j, s)
	}

	//创建桥梁
	brige := func(call interface{
    
    }, args...interface{
    
    }) {
    
    
		//1. 获取参数个数
		n := len(args)
		//2. 创建参数Value切片
		argSlice := make([]reflect.Value, n)
		//添加进切片
		for i:=0; i<n; i++ {
    
    
			argSlice[i] = reflect.ValueOf(args[i])
		}
		//3. 执行函数
		fun := reflect.ValueOf(call)	//创建Value
		fun.Call(argSlice) 				//执行
	}

	//测试
	brige(call1, 1, 2)
	brige(call2, 3, 4, "哈哈哈")
}

可以基于此实现函数的重写功能

3.4.7 反射操作任意结构体

package test

import (
	"reflect"
	"testing"
)

type user struct {
    
    
	Name string
	Age int
}

func TestXxx(t *testing.T)  {
    
    
	newUser := &user{
    
    }		//注意创建的是指针
	nV := reflect.ValueOf(newUser)
	t.Log("nV的类别", nV.Kind())
	nV = nV.Elem()  		//关键的一步
	t.Log("nV的类别", nV.Kind())
	//修改值
	nV.FieldByName("Name").SetString("xwj")
	nV.FieldByName("Age").SetInt(18)
	t.Log("该结构体的值为", newUser)
}

3.4.8 反射创建并操作结构体

用到的方法:

func New

func New(typ Type) Value

New返回一个Value类型值,该值持有一个指向类型为typ的新申请的零值的指针,返回值的Type为PtrTo(typ)。

type man struct {
    
    
	Name string
	Age int
}

func TestStruct(t *testing.T)  {
    
    
	model := &man{
    
    }  				//创建一个指向man空间的指针
	t.Logf("model指向的地址%p", model)	//0xc000004500
	mV := reflect.TypeOf(model)		//获取其Type
	t.Logf("mv的类型%T,类别%v", mV, mV.Kind())
	mV = mV.Elem()					//取这个指针指向的类别(之前本身的类别是指针)
	t.Logf("mv的类型%T,类别%v", mV, mV.Kind())

	//前面这些工作都是为了帮助newPtr创建该类型的Value
	//新建了一个man空间并指向但类型还是Value
	newPtr := reflect.New(mV)		//关键一步 New返回一个Value类型值,该值持有一个指向类型为typ的新申请的零值的指针
	t.Logf("newPtr的类型是%T, 类别是%v\n", newPtr, newPtr.Kind())

	//把newPtr的类型转换为*man, 并赋值给model,让model也指向该空间
	model = newPtr.Interface().(*man)
	t.Logf("model指向的地址%p", model)	//0xc000004580
	t.Logf("model的类型是%T, newPtr的类型是%T, 类别是%v\n", model, newPtr, newPtr.Kind())
	//修改值
	newPtr = newPtr.Elem() 	//Value转向其指向的结构体值
	newPtr.FieldByName("Name").SetString("xwj")
	newPtr.FieldByName("Age").SetInt(25)

	t.Logf("model:%v, mode.Name:%v\n", model, model.Name)
}

4. Tcp编程

4.1 网络编程的两种模式

  • TCP socket 编程,是网络编程的主流。之所以叫Tcp socket 编程,是因为底层是基于Tcp/ip 协议的. 比如: QQ 聊天[示意图]
  • b/s 结构的http 编程,我们使用浏览器去访问服务器时,使用的就是http 协议,而http 底层依旧是用tcp socket 实现的。[示意图] 比如: 京东商城【这属于go web 开发范畴】

tcp socket 编程,简称socket 编程.下图为Golang socket 编程中客户端和服务器的网络分布:

4.2 Socket编程小Demo

4.2.1 net包学习

package net

import "net"

net包提供了可移植的网络I/O接口,包括TCP/IP、UDP、域名解析和Unix域socket。

虽然本包提供了对网络原语的访问,大部分使用者只需要Dial、Listen和Accept函数提供的基本接口;以及相关的Conn和Listener接口。crypto/tls包提供了相同的接口和类似的Dial和Listen函数。

Dial函数和服务端建立连接:(客户端)

conn, err := net.Dial("tcp", "google.com:80")
if err != nil {
    
    
	// handle error
}
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
status, err := bufio.NewReader(conn).ReadString('\n')
// ...

Listen函数创建的服务端

ln, err := net.Listen("tcp", ":8080")
if err != nil {
    
    
	// handle error
}
for {
    
    
	conn, err := ln.Accept()
	if err != nil {
    
    
		// handle error
		continue
	}
	go handleConnection(conn)
}

type Listener

type Listener interface {
    
    
    // Addr返回该接口的网络地址
    Addr() Addr
    // Accept等待并返回下一个连接到该接口的连接
    Accept() (c Conn, err error)
    // Close关闭该接口,并使任何阻塞的Accept操作都会不再阻塞并返回错误。
    Close() error
}

func Listen

func Listen(net, laddr string) (Listener, error)

返回在一个本地网络地址laddr上监听的Listener。网络类型参数net必须是面向流的网络:

“tcp”、“tcp4”、“tcp6”、“unix"或"unixpacket”。参见Dial函数获取laddr的语法。

type Conn

type Conn interface {
    // Read从连接中读取数据
    // Read方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
    Read(b []byte) (n int, err error)
    // Write从连接中写入数据
    // Write方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
    Write(b []byte) (n int, err error)
    // Close方法关闭该连接
    // 并会导致任何阻塞中的Read或Write方法不再阻塞并返回错误
    Close() error
    // 返回本地网络地址
    LocalAddr() Addr
    // 返回远端网络地址
    RemoteAddr() Addr
    // 设定该连接的读写deadline,等价于同时调用SetReadDeadline和SetWriteDeadline
    // deadline是一个绝对时间,超过该时间后I/O操作就会直接因超时失败返回而不会阻塞
    // deadline对之后的所有I/O操作都起效,而不仅仅是下一次的读或写操作
    // 参数t为零值表示不设置期限
    SetDeadline(t time.Time) error
    // 设定该连接的读操作deadline,参数t为零值表示不设置期限
    SetReadDeadline(t time.Time) error
    // 设定该连接的写操作deadline,参数t为零值表示不设置期限
    // 即使写入超时,返回值n也可能>0,说明成功写入了部分数据
    SetWriteDeadline(t time.Time) error
}

Conn接口代表通用的面向流的网络连接。多个线程可能会同时调用同一个Conn的方法。

4.2.2 demo要求

  • 服务器端

    func main() {
          
          
    	fmt.Println("服务器开始监听....")
    	//第一个参数: 网络协议名
    	//第二个参数: IP + 端口
    	listener, err := net.Listen("tcp", "0.0.0.0:8888")
    	if err != nil {
          
          
    		fmt.Println("服务器开启失败!", err)
    	}
    	//fmt.Println(listener)
    	defer listener.Close()	//演示关闭
    	//Accept函数始终帮助我们等待连接,而不会一下就结束
    	// Accept等待并返回下一个连接到该接口的连接
    	for  {
          
          
    		fmt.Println("等待客户端连接.....")
    		conn, err := listener.Accept()
    		if err != nil {
          
          
    			fmt.Println("连接失败!")
    		} else {
          
          
    			fmt.Printf("----------连接成功!客户端IP:%v----------\n", conn.RemoteAddr().String())
    		}
    		//这里启动协程为客户端服务
    		go Process(conn)		//每一个连接都是新的conn
    	}
    
    }
    
    
    //协程工作
    func Process(conn net.Conn)  {
          
          
    	defer conn.Close()	//延迟关闭
    	for  {
          
          
    		//创建一个新的切片
    		buf := make([]byte, 2048)
    		//等待客户端通过conn发送信息
    		//如果客户端没有write【发送】, 那么协程就阻塞在这!
    		//如果客户端宕机,这里的conn也会发现,并报错
    		//fmt.Printf("服务器在等待客户端[%v]的输入....\n", conn.RemoteAddr().String())
    		n, err := conn.Read(buf)
    		if err != nil {
          
          
    			fmt.Println("客户端已退出!连接结束", err)
    			break
    		}
    		//数据显示到服务器终端
    		//注意:
    		// 1. 使用Print而不是Println,因为客户端的\n也是读取过来的
    		// 2. buf[:n]必须要进行切片到n(n是读取到的字节数),不然缓冲buf的大小是2048,后面的字段可能未知
    		info := string(buf[:n])
    		fmt.Print(conn.RemoteAddr().String(), "说:", info)
    	}
    	fmt.Println("--------------连接结束-------------")
    }
    
  • 客户端

    func main() {
          
          
       //参数与服务器端一致
       conn, err := net.Dial("tcp", "127.0.0.1:8888")
       if err != nil {
          
          
          fmt.Println("连接失败!", err)
          return
       }
       //fmt.Println(conn)
       //1. 发送单行数据,然后退出
       reader := bufio.NewReader(os.Stdin) //os.Stdin终端输入
       for  {
          
          
          fmt.Println("请输入:")
          //从终端读取用户的数据,并发送给服务器
          line, err := reader.ReadString('\n')
          if err != nil {
          
          
             fmt.Println("读取输入失败!", err)
             return
          }
          if "exit" == strings.Trim(line, " \r\n"){
          
           //去除掉回车换行空格与exit, Trim不会修改原字符串
             fmt.Println("Bye!")
             return
          }
          fmt.Println(line)
          //发送Line终端输入给服务器
          n, err := conn.Write([]byte(line))
          if err != nil {
          
          
             fmt.Println("向服务器写失败!", err)
             return
          }
          fmt.Printf("成功传输%d个字节\n", n)
       }
    }
    

Tips

1. 注意给结构体打tag的时候不要空格

(错误写法)json: "name" : 后面不能有空格

正确写法:

type Teacher struct {
    
    
	Name string	`json:"name"`
	Age int		`json:"age"`
	ID int		`json:"id"`
	Classroom int	`json:"classroom"`
	Salary float64	`json:"salary"`
}

猜你喜欢

转载自blog.csdn.net/weixin_43988498/article/details/111408037