Go基础语法速通 三

130 阅读14分钟

继续前面的整理,这将是最后一部分的整理内容,包括了goroutine channel的使用以及Go反射的使用。

Go goroutine channel

进程 线程 并行 并发

进程 线程

进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位,进程是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有自己的地址空间,一个进程中至少有5个基本状态,初始态、执行态、等待状态、就绪状态、终止状态。通俗的讲进程就是一个正在执行的程序。

线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位,一个进程可以创建多个线程,同一个进程中的多个线程可以并发执行,一个程序要运行的话至少有一个进程

CleanShot 2024-01-21 at 12.15.30@2x.png

并发 并行

  • 并发 多个线程同时竞争一个位置,竞争到的才可以执行,同一个时间段内只有一个线程在执行
  • 并行 多个线程可以同时执行,每个时间段内,可以有多个线程同时执行

通俗的讲多线程程序在单核CPU上面运行就是并发,多线程程序在多核CPU上运行就是并行,如果线程数大于CPU数,则多线程程序在多核CPU上面运行既有并行又有并发。

CleanShot 2024-01-21 at 12.16.18@2x.png

CleanShot 2024-01-21 at 12.16.31@2x.png

区别在于同一时刻是否有多个任务正在被执行

Go中的协程(goroutine)以及主线程

Go主线程,在主线程上可以起多个协程,Go中多协程可以实现并行或并发。 协程,可以理解为用户级别的线程,这是对于内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的,Go的一个特色就是从语言层面原生支持协程,在函数或者方法前面加go关键字就可以创建一个协程,可以说go中的写成就是goroutine。

多协程与多线程,Go中每个Goroutine默认占用内存远比java、c中的线程少的多,OS线程一般都固定了栈内存(2m),一个Goroutine占用的内存非常小只有2k左右,多协程Goroutine切换调度开销方面远比线程少的多,这也就是go更加适合并发场景的原因。

CleanShot 2024-01-21 at 12.18.23@2x.png

CleanShot 2024-01-21 at 12.21.25@2x.png

CleanShot 2024-01-21 at 12.19.00@2x.png

多协程切换调度开销远比线程要少

Goroutine的使用以及sync.WaitGroup

Goroutine的使用

在主线程中,开启一个Goroutine,该协程每隔50毫秒输出“你好go”在主线程中也没个50毫秒输出“你好go”,输出10次后退出程序,要求主线程和Goroutine同时执行

CleanShot 2024-01-21 at 12.29.28@2x.png

CleanShot 2024-01-21 at 12.27.29@2x.png

主线程执行完毕后即使协程没有执行完毕程序也会退出,使用sync.WaitGroup来等待协程执行完毕

sync.WaitGroup

实现等待协程执行完毕 var wg sync.WaitGroup CleanShot 2024-01-21 at 12.32.28@2x.png

设置Go中并行运行的时候占用CPU的数目

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行GO代码,默认值是机器上的CPU核心数,例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上。

Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU核心数,Go1.5版本之前是单核心执行,1.5版本后默认使用全部的CPU核心数。

func main() {
    // 获取当前计算机上面的cpu个数
    cpuNum := runtime.NumCPU()
    fmt.Println("cpuNum=", cpuNum)

    runtime.GOMAXPROCS(cpuNum - 1)
    fmt.Println("ok")
}

Goroutine 例子

CleanShot 2024-01-21 at 12.39.17@2x.png

统计1-10000000的数字中的素数

  1. 传统方法 通过一个for循环判断各个数是不是素数 CleanShot 2024-01-21 at 16.29.42@2x.png

  2. 使用并发或者并行的方式,将统计素数的任务分配给多个goroutine去完成,这个时候就用到了goroutine CleanShot 2024-01-21 at 16.31.29@2x.png

start:(n-1)*30000+1 end: n*30000

CleanShot 2024-01-21 at 16.36.31@2x.png

CleanShot 2024-01-21 at 16.36.14@2x.png

开启四个goroutine 进行循环

协程之间的通信 -- Channel管道

管道是Go在语言级别上提供的goroutine间的通信方式,可以使用channel在多个goroutine之间传递消息,如果goroutine是Go程序并发的执行体,channel就是他们之间的连接,channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go并发模型是CSP,提倡通过通信共享内存而不是通过共享内存而实现通信。 Go语言中的管道是一种特殊的类型,管道像一个传送带或者队列,总是遵从先入先出的规则,保证收发数据的顺序。每一个管道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

channel 类型

channel是一种类型,一种引用类型,声明管道类型的格式如下

var 变量 chan 元素类型

var ch1 chan int // 声明一个传递整型的管道
var ch2 chan bool // 声明一个传递bool类型的管道
var ch3 chan []int // 声明一个切片int切片的管道

创建channel

声明的管道后需要使用make函数初始化之后才能使用 make(chan 元素类型, 容量)

// 创建一个能存储10个int类型数据的管道
ch1 := make(chan int, 10)
// 创建一个能存储4个 bool类型数据的管道
ch2 := make(chan bool, 4)
// 创建一个能存储3个[]int切片类型数据的管道
ch3 := make(chan []int, 3)

channel操作

管道有发送、接受和关闭三种操作,发送和接收都是使用<-符号,现在我们先使用以下语句定义一个管道 ch := make(chan int, 3)

  1. 发送 将数据放在管道内 将一个值发送到管道中 ch <- 10 把10发送到ch中

  2. 接受 (从管道内取值) 从一个管道中接收值

x := <- ch // 从ch中接收值并赋值给变量x
<- ch // 从ch中接收值,忽略结果

管道是引用数据类型 使用make初始化

CleanShot 2024-01-23 at 00.38.18@2x.png

CleanShot 2024-01-23 at 00.40.43@2x.png

CleanShot 2024-01-23 at 00.47.40@2x.png

CleanShot 2024-01-23 at 00.49.08@2x.png

放不下数据的时候会阻塞,已经取完了再取的时候会阻塞

管道循环

当向管道中发送完数据时,我们可以通过close函数关闭管道,当管道被关闭时,再往管道发送值可能会引发panic,从改管道取值的操作会先取完管道中的值,然后取到的值一直都是对应类型的零值。那么该如何判断管道是否被关闭了呢?

使用for range遍历管道,当管道被关闭的时候就会退出for range,如果没有关闭管道就会报个错误fatal error all Goroutines are asleep - deadlock

通过for range来遍历管道数据,管道没有key

for val := range ch1 {
    fmt.Println(val)
}

CleanShot 2024-01-23 at 12.37.19@2x.png

要关闭管道不然的话会死锁 (写入完成后

CleanShot 2024-01-23 at 12.38.18@2x.png

CleanShot 2024-01-23 at 12.39.53@2x.png转存失败,建议直接上传图片文件

通过for循环循环遍历管道的时候可以不关闭管道,不知道啥时候管道关闭的时候使用for循环来遍历管道 但是要知道数据有多少不然的话就一直读取了零值

goroutine 结合Channel管道

需求: 定义两个方法一个方法给管道里面写数据,一个给管道里面读取数据,要求同步进行

  • 开启一个fn1的协程给管道inChan中写入100条数据
  • 开启一个fn2的协程读取inChan中写入的数据
  • 注意fn1和fn2同时操作一个管道
  • 主线程必须等待操作完成后才可以退出

CleanShot 2024-01-23 at 13.17.09@2x.png

使用goroutine的时候管道没数据不会报错会等待 继续等待读取 多个goroutine可以共享channel中的数据

Goruntine 与 Channel 统计素数例子

  • 协程的个数
  • 统计和打印并行执行

CleanShot 2024-01-25 at 00.21.36@2x.png

极致的并行

//TODO: 完善写下这个代码 视频地址

单向管道

有时候我们会将管道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用管道会对其进行限制,比如限制管道在函数中只能发送或者只能接收。

// 在默认情况下管道是双向的
var chan1 chan int // 可读可写

// 声明只写的管道
var chan2 chan <- int
chan2 = make(chan int, 3)
chan2 <- 20
// num := <- chan2 // error

// 声明为只读
var chan3 <- chan int
num2 := <- chan3
// chan3 <- 30 // error

ch := make(chan int, 2) 读写管道 ch := make(chan<- int, 2) 只写管道 ch := make(<-chan int, 2) 只读管道

select多路复用

在某些场景下我们需要同时从多个通道接受数据,这个时候可以使用go中给我们提供的select多路复用,通常情况下载通道接受数据时,如果没有数据可以接收将会发生阻塞,比如下面的代码来实现从多个通道接收数据的时候就会发生阻塞。

CleanShot 2024-01-25 at 22.40.39@2x.png

CleanShot 2024-01-25 at 22.41.58@2x.png

使用select多路复用来获取chan的数据的时候不需要将chan关闭

goroutine recover 解决协程中出现的panic问题

CleanShot 2024-01-25 at 22.47.11@2x.png

CleanShot 2024-01-25 at 22.47.40@2x.png

Golang并发安全和锁

互斥锁

互斥锁是传统并发编程中对于共享资源进行访问控制的主要手段,他是由标准库sync中的Mutex结构体类型表示,sync.Mutex类型只有两个公开的指针方法,Lock和Unlock。Lock锁定当前共享资源,Unlock进行解锁。

var mutex sync.Mutex
mutex.Lock()
mutex.Unlock()

CleanShot 2024-01-25 at 22.56.51@2x.png

顺序输出,不会由于加了go而数据错乱,因为添加了mutex.Lock()

读写互斥锁

互斥锁的本质是当一个goroutine访问的时候,其他的goroutine都不能访问,这样在资源同步,避免竞争的同时也降低了程序的并发性能,程序由原本的并行执行变成了串行执行

其实当对一个不会变化的数据只做读操作的话是不会存在资源竞争的问题,因为数据是不变的,不管怎么读取,多少goroutine同时读取,都是可以的。

所以问题不是出在读上,而是写上,修改的数据要同步,这样其他的goroutine才可以感知的到,所以真正的互斥应该是读取和修改,修改与修改之间的,读和读是没有互斥的必要性的。

所以对于互斥锁提出了一种性能更好的读写锁,读写锁可以让多个读操作并发执行同时读取,但是对于写操作是完全互斥的,也就是说,当一个goroutine进行写操作的时候,其他的goroutine既不能进行读操作,也不能进行写操作

读写锁是 读与读操作不互斥,读与写操作互斥,写与写操作互斥

CleanShot 2024-01-25 at 23.20.43@2x.png

CleanShot 2024-01-25 at 23.26.24@2x.png

CleanShot 2024-01-25 at 23.26.03@2x.png

完整代码

var wg sync.WaitGroup
var mutex sync.RWMutex

func write() {
	mutex.Lock()
	fmt.Println("执行写操作")
	time.Sleep(time.Second * 2)
	mutex.Unlock()
	wg.Done()
}

func read() {
	mutex.RLock()
	fmt.Println("执行读操作")
	time.Sleep(time.Second * 1)
	mutex.RUnlock()
	wg.Done()
}

func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go read()
	}

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go write()
	}

	wg.Wait()
}

Go 反射

有时候我们需要一个函数,这个函数有能力统一处理各个值类型,而这些类型可能无法共享同一个接口,也可能是布局未知,也有可能这个类型在我们设计函数的时候还不存在,这个使用我们可以使用反射

  1. 空接口可以存储任意类型的变量,那我们如何知道这个空接口保存数据的类型是什么,值是什么呢
  • 可以使用类型断言
  • 可以使用反射实现,也就是在程序运行时动态获取一个变量的类型信息和值信息
  1. 把结构体序列化为json字符串,自定义结构体tab标签的时候就用到了反射
  2. 后期我们会给大家讲ORM框架,这个ORM框架就用到了反射技术 ORM,对象关系映射是通过使用描述对象和数据库之间的映射的元数据,将面向对象语言中的对象自动持久化到关系数据库中。

反射的基本概念

反射是指在程序运行期间对于程序本身进行访问和修改的能力,正常情况程序在编译时,变量被转化为内存地址,变量名不会被编译器写入到可执行部分,在运行程序时,程序无法获取自身的信息,支持反射的语言可以在程序编译期将变量的反射信息,如字段名称,类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期间获取类型的反射信息,并且有能力修改他们。

动态获取变量的能力

使用反射可以实现的功能

  • 反射可以在程序运行期间动态的获取变量的各种信息,比如变量的类型 类别
  • 如果是结构体,通过反射还可以获取结构体本身的信息,比如结构体的字段、结构体的方法等
  • 通过反射,可以修改变量的值,可以调用关联的方法

reflect.typeOf

使用reflect.typeOf获取任意值的类型对象,Go语言中的变量分为两个部分

  • 类型信息 预先定义好的元信息
  • 值信息 程序运行过程中可动态变化的 在Go语言中的反射机制中,任意接口值都是由一个具体类型和具体类型的值两个部分组成的,在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由reflect.Type和reflect.value两部分组成。并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个重要函数来获取任意对象的Value 和Type。
func reflectFn(x interface{}) {
    v := reflect.TypeOf(x)
    fmt.Println(v)
}

func main() {
    a := 10
    b := 23.4
    c := true
    d := "你好go"
    reflectFn(a) // int
    reflectFn(b) // float64
    reflectFn(c) // bool 
    reflectFn(d) // string
}

通过反射回去对象的具体信息 typeOf类型信息

type Name 和 type Kind

在反射中关于类型还划分为类型Type和种类Kind,因为在go语言中我们可以使用type关键字构造很多自定义类型,而种类Kind就是指底层的类型,但是在反射中,当需要区分指针、结构体等大品种的类型时,就会用到种类Kind,举个例子,我们定义了两个指针类型和两个结构体类型,通过反射查看他们的类型和种类。 Go语言的反射中像数组、切片、map、指针等类型的变量,他们的.Name()都是返回空。

返回的反射对象的有type和kind两种,其中种类kind是指向底层的类型

CleanShot 2024-01-27 at 00.57.37@2x.png

CleanShot 2024-01-27 at 00.59.50@2x.png

指针、数组、切片 没有类型名称

reflect.ValueOf

reflect.ValueOf返回的是reflect.Value类型,其中包含了原始值的值信息,reflect.Value与原始值之间可以相互转换。 reflect.Value类型提供的获取原始值的方法如下 CleanShot 2024-01-30 at 16.54.16@2x.png

通过反射设置变量的值

CleanShot 2024-01-30 at 17.02.55@2x.png

CleanShot 2024-01-30 at 17.06.54@2x.png

CleanShot 2024-01-30 at 17.07.16@2x.png

CleanShot 2024-01-30 at 17.23.10@2x.png

Elem() 用来获取指针对应的值

结构体反射

获取结构体中的属性与值

任意值通过reflect.TypeOf()获得反射对象信息后,如果他的类型是结构体,可以通过反射值对象的NumField()和Field()方法获得结构体成员的详细信息。

CleanShot 2024-01-30 at 19.11.46@2x.png

CleanShot 2024-01-30 at 19.17.01@2x.png

CleanShot 2024-01-30 at 19.19.04@2x.png

CleanShot 2024-01-30 at 19.21.44@2x.png

CleanShot 2024-01-30 at 23.47.30@2x.png

获取结构体中的方法

获取结构体中的方法并执行 CleanShot 2024-01-30 at 23.50.44@2x.png

CleanShot 2024-01-31 at 00.22.10@2x.png

修改结构体属性

CleanShot 2024-01-31 at 00.24.28@2x.png

CleanShot 2024-01-31 at 00.26.13@2x.png

由于要修改结构体属性,所以要传入结构体指针类型

CleanShot 2024-01-31 at 00.28.39@2x.png

类型判断

CleanShot 2024-01-31 at 00.30.02@2x.png