Golang基础-13

【达梦8】多实例监视集群的部署测试 确认监视器具有状态确认和自动接管两个功能。在数据守护系统的故障自动切换模式下,必须部署一个确认监视器,否则在出现数据库故障时,会导致数据库服务中断。DM 提供了两种确认监视器的配置形式,分别为单实例和多实例。但是当单实例确认监视器故障时,无法继续进行集群的故障自动接管和备库故障确认,影响正常使用,故提供了多实例确认监控器来进一步提高集群的高可用性。在多实例确认监视器中,只有一个实例作为确认监视器提供服务,其它实例作为备库存在而不提供服务。 阅读详情

Go语言基础

介绍

并发

介绍

  • 本文介绍Go语言中 channel、goroutine、互斥锁、读写锁、原子操作、select、超时处理、sync包、runtime包等相关知识。

并发

  • 进程是是最小的资源管理单元,属于操作系统对一个正在运行的程序的一种抽象。可以同时运行多个进程,每个进程都独立的使用系统资源。
  • 线程属于轻量级进程,从属于进程,是 CPU 调度的最小单位。一个进程可以开启多个线程,各线程共享进程的部分资源,同时具有少量各自的资源。
  • 协程又称微线程,纤程。不同于进程或线程,协程拥有自己的寄存器上下文和栈。通过语言层面支持,在用户态执行,避免操作系统多次系统状态切换。一个线程可以拥有多个协程。
  • 并发主要目的是充分利用现代 CPU 多核处理器,通过合理的软件设计将原本顺序执行的任务变成并行执行的任务(同时处理),提高系统的吞吐率。
  • 在 Go 语言中,每个并发执行的单元叫 goroutine,使用关键字 go 启动协程,可较容易开发高并发程序。
channel
  • channel 是 go 语言中特殊的数据类型,可以看作队列(FIFO)。通道提供了一种安全、同步的方式来共享数据,所以通常用来在各个 goroutine 之间传输数据,数据在不同协程中的传输都是通过拷贝的形式完成。
  • channel 按照接收和发送数据分为三种:

chan T // 发送和接收类型 T 的值
chan<- T // 发送类型 T 的值
<-chan T // 接收类型 T 的值

  • channel 声明与初始化。
package main

import "fmt"

func main() {
	// 声明通道,存储 []int 切片数据类型,默认为 nil
	var c1 chan []int
	fmt.Println("c1: ", c1)

	// 使用 make 创建通道,默认容量为 0,无缓冲通道
	c2 := make(chan int)
	fmt.Println("c2: ", c2, "cap: ", cap(c2))

	// 使用 make 创建通道,指定通道容量,有缓冲通道
	c3 := make(chan int, 1)
	fmt.Println("c3: ", c3, "cap: ", cap(c3))

	// 初始化只发送通道
	c4 := make(chan<- int)
	fmt.Println("c4: ", c4, "cap: ", cap(c4))

	// 初始化只接收通道
	c5 := make(<-chan int)
	fmt.Println("c5: ", c5, "cap: ", cap(c5))
}
  • channel 发送操作: 将数据发送到 channel 中,语法为 ch <- data。
  • channel 接收操作: 从 channel 中接收数据,语法为 data := <- ch。
  • channel 关闭操作: 关闭 channel,语法为 close(ch)。
  • 发送和接收操作都是阻塞的,当没有 goroutine 同时操作相同通道时,通道将一直阻塞(deadlock),直到有 goroutine 进行操作。
  • 关闭操作是非阻塞的。若 channel 已经被关闭,仍然可以从中读取数据。
package main

import (
	"fmt"
	"time"
)

func main() {
	// 初始化通道,存储 int 数据类型,无缓冲通道
	c1 := make(chan int)
	defer close(c1)
	// 开启 goroutine 访问通道
	go func() {
		data := <-c1
		fmt.Println("go func c1 len: ", len(c1), ", cap: ", cap(c1), ", data: ", data)
	}()

	// 发送数据到通道,此时需要其它 goroutine 访问此通道,否则死锁编译报错
	c1 <- 1
	fmt.Println("c1 len: ", len(c1), ", cap: ", cap(c1))

	// main 函数延时 10 ms,goroutine 在 main 函数中启动,需要一点点时间
	// 若启动过程中 main 函数退出,则 goroutine 也会退出,看不到结果输出
	time.Sleep(time.Millisecond * 10)
}
  • channel 有未关闭、已关闭和 nil 三种状态:
操作未关闭已关闭nil
发送阻塞或成功发送panic永久阻塞
读取阻塞或成功读取成功读取或返回零值永久阻塞
关闭成功关闭panicpanic
  • 无并发情况下,无缓冲区通道发送与读取会报错误fatal error: all goroutines are asleep - deadlock!。
package main

import (
	"fmt"
)

func main() {
	// 初始化通道,存储 int 切片数据类型,无缓冲通道,以下编译报错
	c1 := make(chan int)
	// c1 := make(chan int, 1) // 应该使用有缓冲区通道
	defer close(c1)

	c1 <- 1
	fmt.Println("c1 len: ", len(c1), ", cap: ", cap(c1))
	<-c1
}
  • channel 有缓冲区发送时,缓冲区数据容量满了后,继续发送数据会报错fatal error: all goroutines are asleep - deadlock!。
package main

import (
	"fmt"
)

func main() {
	// 初始化通道,存储 int 切片数据类型,有缓冲通道
	c1 := make(chan int, 1)
	// 	c1 := make(chan int, 2) // 应该预留两个缓冲区
	defer close(c1)

	c1 <- 1
	fmt.Println("c1 len: ", len(c1), ", cap: ", cap(c1))
	c1 <- 2
	fmt.Println("c1 len: ", len(c1), ", cap: ", cap(c1))
	<-c1
}
  • channel 数据遍历。
package main

import "fmt"

func Test() {
	// 初始化通道,存储 int 切片数据类型,无缓冲通道
	c1 := make(chan int)

	// 创建协程,发送数据
	go func(c chan int) {
		for i := 0; i < 5; i++ {
			c1 <- 1 + i
		}

		close(c1)
	}(c1)

	// 使用 for range 遍历,实际上由于不知道通道中的数据个数,如果不在协程中关闭通道,
	// 会一直遍历,通道中无数据时,报错 deadlock
	for v := range c1 {
		fmt.Println(v)
	}
}

func Test2() {
	// 初始化通道,存储 int 切片数据类型,无缓冲通道
	c1 := make(chan int)

	// 创建协程,发送数据
	go func(c chan int) {
		for i := 0; i < 5; i++ {
			c1 <- 1 + i
		}
	}(c1)

	// 使用普通 for 循环,知道发送多少个数据,此处接收多少个数据
	for i := 0; i < 5; i++ {
		fmt.Println(<-c1)
	}
	close(c1)
}

func main() {
	Test()
	Test2()
}
goroutine
  • go 语言中,使用每个并发执行的单元叫 goroutine,启动 goroutine 使用 go 关键字,后边跟函数创建。可以在单个进程中执行成千上万的并发任务。
  • go 的并发编程采用的 CSP (Communicating Sequential Process) 模型,通过 GMP 调度模型,在用户态实现了 M:N 的线程模型(M 个 goroutine 在 N 个线程上调度,go 语言实现了其调度机制)。golang 内置的调度器,可以充分利用多核 CPU 资源。
  • 创建协程,由此例可知,使用 go 关键字创建 goroutine 非常容易,但观察输出结果可以发现,创建的 goroutine 执行顺序并不是按照创建顺序依次执行,多次执行此程序可以发现会输出多种不同的执行结果。
package main

import (
	"fmt"
	"time"
)

func Test(index int) {
	fmt.Println("go Test ", index)
}

func main() {
	// 使用匿名函数创建 goroutine
	go func() {
		fmt.Println("go func")
	}()

	// 使用函数创建多个 goroutine
	for i := 0; i < 10; i++ {
		go Test(i)
	}

	// 主协程等待工作协程执行
	time.Sleep(time.Second)
}

输出结果
go func
go Test 9
go Test 0
go Test 1
go Test 2
go Test 3
go Test 4
go Test 5
go Test 6
go Test 7
go Test 8

  • 使用 time.Sleep 函数等待工作协程结束,实际上并不能确定工作协程执行多长时间,所以 go 语言中提供等待组(sync.WaitGroup)来等待所有协程结束。
package main

import (
	"fmt"
	"sync"
)

func main() {
	// 初始化等待组变量,默认内部计数为 0
	wg := &sync.WaitGroup{}

	// 由于知道创建多少个协程,所以此处直接将内部计数添加 10,
	// 注意添加的数不能大于需要等待的协程数量,否则执行报错
	wg.Add(10)
	// 使用匿名函数创建 goroutine
	for i := 0; i < 10; i++ {
		// wg.Add(1) // 或创建协程时依次增加内部计数
		go func() {
			fmt.Println(i)
			wg.Done()
		}()
	}

	wg.Wait()
}
  • 使用协程并发处理业务时,由于资源数据可能在不同的协程中访问修改,为了保持资源数据访问的正确性,使其运行符合预期逻辑流程,需要对共享资源数据(多个协程修改资源数据时)进行保护,达到同一时间资源数据(尤其是修改资源数据时)只能被一个协程访问。
  • 下边的例程是未对共享资源数据做任何保护,多次执行从其结果分析,预期的结果输出不稳定,也就是说在多协程同时修改共享资源数据时,结果不一定符合预期,存在资源访问竞争。
package main

import (
	"fmt"
	"sync"
)

func main() {
	// 初始化等待组变量,默认内部计数为 0
	wg := &sync.WaitGroup{}
	wg.Add(10) // 等待 10 个子协程

	var index int32 = 0      // 共享变量
	for i := 0; i < 5; i++ { // 创建协程 5 * 2 = 10 个
		go func() { // 协程 1
			for j := 0; j < 1000; j++ {
				index++
			}
			wg.Done()
		}()

		go func() { // 协程 2
			for j := 0; j < 1000; j++ {
				index--
			}
			wg.Done()
		}()
	}

	wg.Wait()                     // 主协程等待子协程结束
	fmt.Println("index: ", index) // 打印修改后的最终值
}

输出结果
共享资源竞争

互斥锁
  • Go语言中提供锁机制来处理数据竞争状态,基本的锁有互斥锁(Mutex)与读写锁(RWMutex),通过对资源加锁和释放锁提供对资源同步方式访问(同一时刻只能有一个协程访问)。
  • 注意:使用互斥锁会影响程序性能(主要是程序执行速度),所以对性能要求较高的模块开发人员需要仔细斟酌取舍。
package main

import (
	"fmt"
	"sync"
)

func main() {
	// 初始化等待组变量,默认内部计数为 0
	wg := &sync.WaitGroup{}

	lock := sync.Mutex{} // 初始化互斥锁
	wg.Add(10)           // 等待 10 个子协程

	var index int32 = 0      // 共享变量
	for i := 0; i < 5; i++ { // 创建协程 5 * 2 = 10 个
		go func() { // 协程 1
			for j := 0; j < 1000; j++ {
				// atomic.AddInt32(&index, 1)
				lock.Lock() // 加锁
				index++
				lock.Unlock() // 解锁
			}
			wg.Done()
		}()

		go func() { // 协程 2
			for j := 0; j < 1000; j++ {
				// atomic.AddInt32(&index, -1)
				lock.Lock() // 加锁
				index--
				lock.Unlock() // 解锁
			}
			wg.Done()
		}()
	}

	wg.Wait()                     // 主协程等待子协程结束
	fmt.Println("index: ", index) // 打印修改后的最终值
}
读写锁
  • Go 语言也提供读写锁来保证资源同步,此锁应用于读需求远大于写需求,也就是不需要频繁修改共享资源内容。读时加锁不影响,写时加锁保证资源数据同步性。
package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	// 初始化等待组变量,默认内部计数为 0
	wg := &sync.WaitGroup{}

	lock := sync.RWMutex{} // 初始化互斥锁
	wg.Add(6)              // 等待 6 个子协程

	var index int32 = 0 // 共享变量
	go func() {         // 协程, 定时修改变量值
		for j := 0; j < 5; j++ {
			lock.Lock() // 加写锁
			index++
			lock.Unlock() // 解锁

			time.Sleep(time.Second) // 定时 1 秒
		}
		wg.Done()
	}()

	for i := 0; i < 5; i++ { // 创建协程 5 个
		go func() { // 协程 1
			for {
				lock.RLock() // 加读锁
				fmt.Printf("%v, ", index)
				lock.RUnlock()                     // 解锁
				time.Sleep(time.Millisecond * 250) // 定时 0.25 秒

				if index >= 5 { // 满足条件退出协程
					break
				}
			}
			wg.Done()
		}()
	}

	wg.Wait()                       // 主协程等待子协程结束
	fmt.Println("\nindex: ", index) // 打印修改后的最终值
}

输出结果
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
index: 5

原子操作
  • 原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch(上下文切换)。
  • go 语言中也提供原子操作用于解决并发编程中的资源数据竞争问题,在 sync/atomic 包提供了对基本数据类型的原子操作支持。
  • 原子操作主要提供了五类操作函数:Add*(增加值)、Load*(读取值)、Store*(存储值)、Swap*(更新值)、CompareAndSwap*(比较第一个参数引用的值是否与第二个参数值相同,若相同则将第一个参数值更新为第三个参数,同时返回 true,若不相同,第一个参数值不变,返回 false)。
package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	// 初始化等待组变量,默认内部计数为 0
	wg := &sync.WaitGroup{}

	wg.Add(10) // 等待 10 个子协程

	var index int32 = 0      // 共享变量
	for i := 0; i < 5; i++ { // 创建协程 5 * 2 = 10 个
		go func() { // 协程 1
			for j := 0; j < 1000; j++ {
				atomic.AddInt32(&index, 1)
			}
			wg.Done()
		}()

		go func() { // 协程 2
			for j := 0; j < 1000; j++ {
				atomic.AddInt32(&index, -1)
			}
			wg.Done()
		}()
	}

	wg.Wait()                     // 主协程等待子协程结束
	fmt.Println("index: ", index) // 打印修改后的最终值
}

输出结果
原子操作避免资源竞态

select
  • 对于 channel,当写入无缓冲区通道或有缓冲区通道已被写满时,如果继续写入通道会阻塞,直到通道中有数据被读取。同样的,当通道中无元素,继续读取也会阻塞,直到通道被写入数据。
  • go 语言从语言层面提供了一种 IO 多路复用机制,可同时对多个通道进行监听(写入或读取)。select-case 是一种控制结构,写法有点类似于用于 switch 语句,实际规则不相同。
  • 如果所有的 case 的 channel 都不可读或不可写,此时若有 default 分支会执行此分支然后退出 select 流程。
package main

import (
	"fmt"
	"time"
)

func main() {
	// 定义两个通道
	chan1 := make(chan int)
	chan2 := make(chan int)

	go func() { // 协程 1,向通道1中写入数据
		time.Sleep(time.Second) // 延时 1 秒后写入数据
		chan1 <- 1
	}()

	go func() { // 协程 2,向通道2中写入数据
		for {
			time.Sleep(time.Second) // 延时 1 秒后写入数据
			chan2 <- 1
		}
	}()

	select { // 主协程监听两个通道
	case <-chan2:
		fmt.Println("channel 2 ready.")
	case <-chan1:
		fmt.Println("channel 1 ready.")
	default: // 默认分支
		fmt.Println("default case")
	}

	fmt.Println("main function exit.")
}

输出结果
default case
main function exit.

  • 若没有 default 分支,select 将阻塞所在协程,直至有可读或可写的通道为止。若存在多个 case 的 channel 可读或可写,则随机选择一个 case 进行处理,然后退出 select 流程。
package main

import (
	"fmt"
	"time"
)

func main() {
	// 定义两个通道
	chan1 := make(chan int)
	chan2 := make(chan int)

	go func() { // 协程 1,向通道1中写入数据
		time.Sleep(time.Second)
		chan1 <- 1
	}()

	go func() { // 协程 2,向通道2中写入数据
		for {
			time.Sleep(time.Second)
			chan2 <- 1
		}
	}()
	
    // 无 default 分支,会阻塞主协程,直到有通道可读或可写时执行 case 分支,然后退出 select 流程
	select { 
	case <-chan2:
		fmt.Println("channel 2 ready.")
	case <-chan1:
		fmt.Println("channel 1 ready.")
	}

	fmt.Println("main function exit.")
}

输出结果
存在多个 case 分支

  • 若 select 语句块为空,则会阻塞 select 所在协程,遇到 panic 退出,若无通道,空 select 执行报错。
package main

import (
	"fmt"
	"time"
)

func main() {
	// 定义两个通道
	chan1 := make(chan int)
	chan2 := make(chan int)

	go func() { // 协程 1,向通道1中写入数据
		for {
			time.Sleep(time.Second)
			chan1 <- 1
		}
	}()

	go func() { // 协程 2,向通道2中写入数据
		for {
			time.Sleep(time.Second)
			chan2 <- 1
		}
	}()

	go func() { // 读取通道值,避免死锁
		for {
			<-chan1
			<-chan2
		}
	}()

	// 主协程在此阻塞,不能执行到最后一行的 fmt 语句
	select {}

	fmt.Println("main function exit.")
}
超时处理
  • 在实际开发中,超时处理机制比较常见,可以通过 select、select + time.After、select + context 实现对执行操作超时的控制。
  • select 方式实现超时处理机制。
package main

import (
	"fmt"
	"time"
)

func main() {
	// 定义两个通道
	chan1 := make(chan int)
	timeout := make(chan bool)

	go func() { // 协程 1,定时向通道1中写入数据
		i := 100
		for {
			time.Sleep(time.Second)
			i++
			chan1 <- i
		}
	}()

	go func() { // 协程 2,模拟 5 秒超时
		time.Sleep(time.Second * 5)
		timeout <- true
	}()

	for { // 循环监听通道
		var tmout bool = false
		select {
		case data := <-chan1:
			fmt.Printf("%v  ", data)
		case tmout = <-timeout:
			fmt.Println("\ntimeout ", tmout)
		}

		if tmout { // 超时,退出监听通道
			break
		}
	}

	fmt.Println("main function exit.")
}

输出结果
101 102 103 104
timeout true
main function exit.

  • 使用 select + time.After 方式实现超时处理机制。
package main

import (
	"fmt"
	"time"
)

func main() {
	// 定义两个通道
	chan1 := make(chan int)

	go func() { // 协程 1,定时向通道1中写入数据
		i := 100
		var t int64 = 0
		for {
			t++
			time.Sleep(time.Second * time.Duration(t)) // 模拟写数据延时 1,2,3...
			i++
			chan1 <- i
		}
	}()

	for { // 循环监听通道
		var tmout bool = false
		select {
		case data := <-chan1:
			fmt.Printf("%v  ", data)
		case <-time.After(time.Second * 2): // 模拟 select 2 秒未处理通道表示超时
			tmout = true
			fmt.Println("\ntimeout ", tmout)
		}

		if tmout { // 超时,退出监听通道
			break
		}
	}

	fmt.Println("main function exit.")
}

输出结果
101
timeout true
main function exit.

  • 使用 select + context 方式实现超时处理机制。
package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	// 定义两个通道
	chan1 := make(chan int)

	ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) // 设置模拟 3 秒超时时间
	defer cancel()
	go func() { // 协程 1,定时向通道1中写入数据
		i := 100
		var t int64 = 0
		for {
			t++
			time.Sleep(time.Second * time.Duration(t)) // 模拟写数据延时 1,2,3...
			i++
			chan1 <- i
		}
	}()

	for { // 循环监听通道
		var tmout bool = false
		select {
		case data := <-chan1:
			fmt.Printf("%v  ", data)
		case <-ctx.Done(): // 模拟 3 秒时间超时
			tmout = true
			fmt.Println("\ntimeout ", tmout)
		}

		if tmout { // 超时,退出监听通道
			break
		}
	}

	fmt.Println("main function exit.")
}

输出结果
101 102
timeout true
main function exit.

sync包
  • go 语言中的 sync 包是一个重要的同步原语库,它提供了一些基本的同步原语。包中括互斥锁、读写锁、原子变量、等待组、条件变量、单次执行、协程安全映射、对象池等。前边已经使用过互斥锁、读写锁、原子变量、等待组,接下来对其它几个使用例子进行简单介绍。
  • 条件变量(sync.Cond)是用于多个goroutine之间进行同步和互斥的一种机制。如下例程实现了一个简单的生产者-消费者模型。
package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	mtx  sync.Mutex      // 定义互斥锁
	cond *sync.Cond      // 定义条件变量
	wg   *sync.WaitGroup // 定义等待组
)

func Consumer(stop *bool, ch chan int, id int) {
	cond.L.Lock()         // 加锁
	defer cond.L.Unlock() // 延迟解锁
	for !(*stop) {
		cond.Wait() // 等待条件变量触发
		fmt.Println("go ", id, ": ", <-ch)
	}

	fmt.Println("exit ", id, "Consumer!!!")
	wg.Done()
}

func Producer(stop *bool, ch chan int) {

	for i := 0; i < 8; i++ {
		time.Sleep(time.Second)
		ch <- i + 25 // 写入数据

		cond.L.Lock()   // 加锁
		cond.Signal()   // 通知一个消费者
		cond.L.Unlock() // 解锁
	}

	*stop = true
	fmt.Println("exit Producer!!!")
	wg.Done()

	close(ch) // 关闭通道
	cond.L.Lock()
	cond.Broadcast() // 通知所有消费者
	cond.L.Unlock()
}

func main() {
	cond = sync.NewCond(&mtx) // 初始化条件变量

	stop := false
	ch := make(chan int, 10)

	wg = &sync.WaitGroup{}
	wg.Add(6)

	go Producer(&stop, ch) // 启动生产者

	for i := 0; i < 5; i++ { // 创建 5 个消费者
		go Consumer(&stop, ch, i)
	}

	wg.Wait() // 等待所有协程结束
	fmt.Println("exit main function")
}

输出结果
go 4 : 25
go 0 : 26
go 1 : 27
go 2 : 28
go 3 : 29
go 4 : 30
go 0 : 31
exit Producer!!!
go 1 : 32
exit 1 Consumer!!!
go 0 : 0
exit 0 Consumer!!!
go 3 : 0
exit 3 Consumer!!!
go 2 : 0
exit 2 Consumer!!!
go 4 : 0
exit 4 Consumer!!!
exit main function

  • 单次执行(sync.Once)保证某个操作只执行一次。如下例程模拟初始化环境函数只执行一次的场景。
package main

import (
	"fmt"
	"sync"
)

var once sync.Once

func InitEnv() { // 此初始化环境函数全局只执行一次
	fmt.Println("InitEnv")
}

func Test(wg *sync.WaitGroup, id int) { // 协程测试函数
	once.Do(InitEnv) // 调用执行函数
	fmt.Printf("Test %v\t", id)
	wg.Done()
}

func main() {
	var wg sync.WaitGroup
	wg.Add(10)
	for i := 0; i < 10; i++ { // 开启 10 个测试协程
		go Test(&wg, i)
	}
	wg.Wait()
	fmt.Println("\nexit main function")
}

输出结果
InitEnv
Test 0 Test 9 Test 1 Test 2 Test 3 Test 4 Test 5 Test 6 Test 7 Test 8
exit main function

  • go 语言内置的 map 数据类型不是并发安全的,在 sync 包中提供(go 1.9 及之后)的协程安全映射(sync.Map)是并发安全的。sync.Map 的读取、插入、删除一般都是常数级的时间复杂度。
  • sync.Map 的零值是有效的,就是一个空 map。适用于读多写少的场景,当 sync.Map 在第一次使用之后,就不允许被拷贝。
package main

import (
	"fmt"
	"sync"
)

func main() {
	var m sync.Map
	// 写入
	m.Store("Intel", 10000)
	m.Store("AMD", 10001)

	// 读取
	str, ok2 := m.Load("Intel")
	fmt.Println("read: ", ok2, "value: ", str.(int))

	// 遍历
	m.Range(func(key, value any) bool {
		name := key.(string)
		id := value.(int)
		fmt.Println("name: ", name, "id: ", id)
		return true
	})

	// 删除
	m.Delete("AMD")
	str1, ok3 := m.Load("AMD")
	if ok3 {
		fmt.Println("read: ", ok3, "value: ", str1.(int))
	} else {
		fmt.Println("read: ", ok3, "value: ", "***")
	}

	// 读取或写入
	m.LoadOrStore("Intel+", 10001)
	str2, ok4 := m.Load("Intel+")
	fmt.Println("read: ", ok4, "value: ", str2.(int))
	
    fmt.Println(m)
}

输出结果
read: true value: 10000
name: Intel id: 10000
name: AMD id: 10001
read: false value: ***
read: true value: 10001
{{0 0} {[] {} 0xc0000240f0} map[Intel:0xc000056028 Intel+:0xc000056040] 1}

  • 对象池,可以理解为存储对象的容器,对于很多需要重复分配、回收内存的地方,对象池(sync.Pool)是一个很好的选择。因为频繁的创建对象和回收内存,垃圾回收机制(GC)会频繁调用从而影响程序性能,可以提前创建一些对象放入对象池中,若需要使用对象时从对象池中获取,使用完成还给对象池,这样避免运行时使用时创建对象,用完时GC清理对象的开销。
package main

import (
	"fmt"
	"sync"
)

var pool *sync.Pool

type Cpu struct {
	Name string
	Id   int
}

func Init() {

}

func main() {
	pool = &sync.Pool{
		New: func() any { // 创建 New 函数
			fmt.Println("create a new Cpu object")
			return new(Cpu)
		},
	}

	p := pool.Get().(*Cpu) // 获取对象
	fmt.Println("get object: ", p)

	// 修改对象属性
	p.Name = "Intel"
	p.Id = 10000
	fmt.Println("p value: ", p)

	pool.Put(p)                                      // 将对象放入对象池中
	fmt.Println("get object 1: ", pool.Get().(*Cpu)) // 可以获取到对象

	fmt.Println("get object 2:", pool.Get().(*Cpu)) // 获取到临时对象
	p = pool.Get().(*Cpu)
	p.Name = "AMD"
	p.Id = 10010
	fmt.Println("p value: ", p)

	pool.Put(p)
	fmt.Println("get object3: ", pool.Get().(*Cpu)) // 获取对象
	fmt.Println("get object 4:", pool.Get().(*Cpu)) // 获取对象
}

输出结果
create a new Cpu object
get object: &{ 0}
p value: &{Intel 10000}
get object 1: &{Intel 10000}
create a new Cpu object
get object 2: &{ 0}
create a new Cpu object
p value: &{AMD 10010}
get object3: &{AMD 10010}
create a new Cpu object
get object 4: &{ 0}

runtime包
  • go 语言中提供 runtime 包,用于和程序的运行时环境进行交互,包中提供了一系列函数和变量方便控制、管理和监视程序的执行情况。
常用操作函数描述
Gosched当前 goroutine 让出时间片
GOROOT获取 Go 安装路径
NumCPU获取可使用的逻辑 CPU 数量
GOMAXPROCS设置当前进程可使用的逻辑 CPU 数量
NumGoroutine获取当前进程中 goroutine 的数量
Goexit退出当前 goroutine(defer语句会照常执行)
GOOS获取目标操作系统
GC执行垃圾回收机制
package main

import (
	"fmt"
	"runtime"
	"sync"
	"time"
)

func main() {
	fmt.Println("GOOS: ", runtime.GOOS)
	fmt.Println("GOROOT: ", runtime.GOROOT())
	fmt.Println("NumCPU: ", runtime.NumCPU())
	fmt.Println("GOMAXPROCS: ", runtime.GOMAXPROCS(8)) // 返回上一次设置值

	wg := &sync.WaitGroup{}
	wg.Add(2)
	go func() {
		defer func() { // 协程退出时执行此函数
			fmt.Println("go func eixt!!!")
			wg.Done()
		}()

		for { // 协程死循环,正常运行时此协程会一直运行直到程序退出
			time.Sleep(time.Second * 2)
			runtime.Goexit() // 主动退出协程
		}
	}()

	go func() {
		for i := 0; i < 5; i++ {
			runtime.Gosched() // 让出协程时间片
		}
		wg.Done()
	}()

	fmt.Println("NumGoroutine: ", runtime.NumGoroutine())

	runtime.GC() // 主动调用垃圾回收机制
	wg.Wait()
}

输出结果
GOOS: windows
GOROOT: C:\Program Files\Go
NumCPU: 12
GOMAXPROCS: 12
NumGoroutine: 3
go func eixt!!!

  • GC(Garbage Collection 的缩写)表示一种自动的存储器管理机制。当一个计算机上的动态存储器不再需要时,就应该将其释放,让出存储器,这种存储器资源管理,称为垃圾回收。
  • Go 语言中提供 GC 机制,可以自动管理程序内存,释放不再使用的内存,帮助开发者管理内存,尽量使其避免一些常见的内存错误。Go 语言的GC由早起的标记-清除(mark and sweep)算法到后来的三色标记 + 混合写屏障算法,优化 GC 工作时尽量减少对主程序的影响。
  • GC 的触发方式一般有三种,第一种是内存分配达到当前内存分配的阈值(动态变化,一般下一次会设置为当前垃圾集的2倍时会再次调用 GC)时自动触发;第二种是定时(一般为 2 分钟)自动触发;第三种事手动调用触发(runtime.GC()函数)。
  • GC 调优:读程序代码进行内存优化,尽量减少额外内存开销,调整 GOGC(Go 为了保证使用 GC 的简洁性,只提供了一个参数 GOGC,减少 GOGC 对 CPU 的占用)。实际开发过程中,一般都会对程序内存优化,所以大多数情况下不需要关注 GC 相关。

起始

宇树机器人丝滑的二人转动作背后,MPC到底是什么? 了解MPC,让我们闻“机”起舞。 阅读详情

相关推荐

Ubuntu20.04配置PX4环境启动或编译jmavsim 出现的错误及解决办法

错误1:ant命令未找到

Iamsonice的博客 8856

Golang 中的并发限制与超时控制

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_33852020的博客 403

对称加密和非对称加密、公钥和私钥、单向认证和双向认证、数字签名、数字证书、根证书

文章目录1. 什么是公钥和私钥?2. 加密算法2.1 对称加密2.2 非对称加密2.2.1 非对称加密的重要性质2.2.2 非对称加密算法应用2.2.2.1 应用1 加密通信2.2.2.1.1 单向认证和双向认证2.2.2.1 应用2 数字签名3. 数字签名3.1 数字签名用法演示3.2 数字签名存在问题4. 数字证书(Digital Certificate)4.1 创建数字证书4.1.1 举例,CA如何给我们签发一个有效证书: 1. 什么是公钥和私钥? 摘自《什么是公钥和私钥?》 公钥(Public Ke

m0_45406092的博客 1万+

Golang 并发限制和超时限制

   我们先来看一个简单的并发实例: package main import ( "fmt" "time" ) func run(task_id, sleeptime int, ch chan string) { time.Sleep(time.Duration(sleeptime) * time.Second) ch &lt;- fmt.Sprintf(...

冷月醉雪的博客 3154

山东大学软件测试实验2——系统功能测试

​ 给出一个被测试的 Web 应用系统(SUT)。http://211.87.232.162:8080/注:友情链接不用延伸测试。针对被测试的 Web 应用系统进行功能测试,发现其存在的缺陷。 ​

m0_74033391的博客 2409

从仿真到流片:手把手教你优化65nm CMOS电荷泵的电流匹配与稳定性

本文详细介绍了在65nm CMOS工艺下优化电荷泵电流匹配与稳定性的工程实践指南。通过蒙特卡洛仿真、运放反馈结构版图技巧和全差分结构设计,实现优于1%的电流匹配精度和±2%以内的电流稳定性,特别针对current mismatch问题提供了有效的解决方案。

weixin_42522320的博客 417

100天精通Golang(基础入门篇)——第13天:深入解析Go语言中的字符串(string)及常用函数应用

本篇教程将深入解析Go语言中的字符串类型及其常用函数的应用。我们将学习字符串的创建和初始化方法,并介绍常见的字符串操作函数,如字符串连接、长度计算、字符访问等。此外,还将探讨一些常用的字符串函数,包括字符串切割、查找和替换。通过本篇教程,您将更好地理解和运用Go语言中的字符串操作。

猫头虎技术团队:授渔优于赠鱼,兴趣引领智慧,探索之乐尤显珍贵。商务合作+:Libin9iOak ,万粉变现+:优快云WF,猫头虎承诺每年免费为100名C站创作者做账号流量诊断服务!全网搜:猫头虎技术团队,点击文章底部名片或直接私信我一切皆可谈,快找虎哥! 5万+

BIM模型轻量化避坑指南:Revit导出GLTF常见问题及解决方案

本文深入探讨了BIM模型轻量化过程中,从Revit导出GLTF格式时遇到的常见问题与解决方案。针对文件体积膨胀、材质丢失、属性信息缺失等核心痛点,提供了从数据预处理、Draco几何压缩、纹理优化到Web端性能调优的全流程实战指南,帮助工程师高效实现BIM模型在WebGL环境中的流畅展示与交互。

wdx012345的博客 171

Go语言学习基础刷题记(二)(Golang roadmap)2021-07-13

1. 下面的代码输出什么? func main() { fmt.Println(~2) } 编译错误: invalid character U+007E '~' 很多语言都是采用 ~ 作为按位取反运算符,Go 里面采用的是 ^ 。按位取反之后返回一个每个 bit 位都取反的数,对于有符号的整数来说,是按照补码进行取反操作的(快速计算方法:对数 a 取反,结果为 -(a+1) ),对于无符号整数来说就是按位取反。例如: func main() { var a int8 = 3

weixin_44336181的博客 531

AUTOSAR BswM配置实战:手把手教你用Vector Davinci Configurator搭建第一个模式管理规则

本文详细介绍了如何使用Vector Davinci Configurator工具配置AUTOSAR BswM模块,实现车身控制ECU的模式管理规则。通过实战案例,从环境准备、核心概念梳理到具体配置步骤,包括ModeRequestPort、ModeCondition、Logical Expression和Rules的构建,帮助工程师快速掌握BswM配置技巧,提升汽车电子软件开发效率。

weixin_28678831的博客 352

SAP PFCG 中的单一角色与复合角色

在 SAP 系统的安全与授权管理中,事务码 PFCG(角色维护)扮演着关键角色。理解单一角色(Single Role)和复合角色(Composite Role)的概念,对于有效地管理用户权限至关重要。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 1477

golang2021变量基础(13)

  Go语言的词法元素包括5种,分别是标识符(identifier)、关键字(keyword)、操作符(operator)、分隔符(delimiter)、字面量(literal),它们是组成Go语言代码和程序的最基本单位。   本节我们主要来介绍一下Go语言中的关键字和标识符。   关键字即是被Go语言赋予了特...

计算机毕业论文源码,学生个人网页制作html源码。贴近用户做网络推广和互联网优化。 239

手把手教你学Simulink--基础储能管理场景实例:基于Simulink的锂电池健康状态(SOH)在线评估仿真

本文详细介绍了基于Simulink的锂电池健康状态(SOH)在线评估仿真方法。主要内容包括:1)SOH定义与量化指标(容量SOH、内阻SOH及综合SOH);2)基于Thevenin模型参数辨识和递推最小二乘法(RLS)的在线评估原理;3)100次充放电循环的仿真场景设置;4)Simulink建模步骤(主电路搭建、核心模块实现及信号流连接);5)仿真结果分析,显示SOH评估误差<2%,响应时间45秒,满足在线评估需求;6)提出多参数融合评估、深度学习辅助辨识等优化方向。该方法为储能系统寿命管理提供了有效

MHD0815的博客 182

【Go语言基础【13】】函数、闭包、方法

【Go语言基础【13】】函数、闭包、方法

hiliang521的博客 1420

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

13Go语言基础之接口

本文是李文周的博客go语言学习之路的目录提要版,方便复习查看使用,原文参见文章连接: https://www.liwenzhou.com/posts/Go/go_menu/ 接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。 接口总结 接口类型 在Go语言中接口(interface)是一种类型,一种抽象的类型。 interface是一组method...

weixin_42426385的博客 217

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

go语言基础13——并发★★★

并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因。 Go语言中的并发编程 并发与并行 并发:同一时刻只执行一个任务,一个时间段内不断切换执行多个任务。 并行:同一时刻同时执行多个任务。 Go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。 Go语言还提供c

一蓑烟雨任平生 797

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

13. Go语言基础之函数

前言 函数是组织好的、可重复使用的、用于执行指定任务的代码块。 Go语言中支持函数、匿名函数和闭包 13.1 函数 13.1.1 函数定义 Go语言中定义函数使用func关键字,具体格式如下: func 函数名(参数)(返回值){ 函数体 } 其中: 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。 返回值:返回值由返回值变量和其变量类型组成,也可以

小六 261

非零基础自学Golang 第13章 并发与通道 13.1 概述

非零基础自学Golang 第13章 并发与通道 13.1 概述

代码改变世界 406

一、Go基础知识13、list详解

Go语言,list和slice的区别,list的基本用法

风不归的乐园 1357

Go基础13-理解Go语言代码块与作用域

Go语言中的代码块是包裹在一对大括号内部的声明和语句,且代码块支持嵌套。如果一对大括号之间没有任何语句,那么称这个代码块为空代码块。代码块是代码执行流流转的基本单元,代码执行流总是从一个代码块跳到另一个代码块。// 这里是显式代码块,包裹在函数的函数体内// ...for {// 这里是显式代码块,包裹在for循环体内// 该代码块也是嵌套在函数体显式代码块内部的代码块// ...if true {// 这里是显式代码块,包裹在if语句的true分支内。

小海聊智造 488

掌握Go语言:探索Go语言中的循环奇妙世界,从基础到实战(13)

通过深入探索Go语言中的循环结构,我们不仅理解了循环的基本用法,还学会了如何利用循环实现实际场景下的功能。掌握循环,意味着能够更加高效地处理重复性任务,为Go语言编程之旅增添了无限可能。

欢迎来到技术蜜糖罐的IT角落! 2143

非零基础自学Golang 第13章 并发与通道 13.4 select & 13.5 小结

非零基础自学Golang 第13章 并发与通道 13.4 select & 13.5 小结

代码改变世界 646

Go基础编程 - 13 -单元测试

不测试的开发不是好程序员。

979

13.Go语言基础之error接口

Go 语言中的错误处理与其他语言不太一样,它把错误当成一种值来处理,更强调判断错误、处理错误,而不是一股脑的 catch 捕获异常。

鹿晓鹏的博客 323

13golang基础-slice 切片的4种声明定义方式

golang学习笔记-slice 切片的4种声明定义方式

FISH 2087

go基础-13.线程安全

现在有两个协程,同时触发,一个协程对一个全局变量进行100完成++操作,另一个对全局变量—的操作。希望大家见到这个错误,就能知道,这个就是map的线程安全错误。如果我们在一个协程函数下,读写map就会引发一个错误。根本原因是CPU的调度方法为抢占式执行,随机调度。那么我们能不能通过给操作加锁来解决这个问题呢。那么,两个协程结束,最后的值应该是0才对。但是你会发现,这个输出的结果完全无法预测。其实看它源码,它的内部也是用了同步锁的。我们不能在并发模式下读写map。

枫枫知道的个人博客 452
上一篇: Golang基础-12
下一篇: c++11新特性
无夜_
博客等级 码龄7年 2916粉丝 51原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值