# Day07 标准库time与并发编程

# 本节关键词

  • 时间对象、时间戳与时间字符串

# 标准库time

# 时间对象 time.Time

func timeDemo() {
	now := time.Now()                     // 当前时间对象
	fmt.Printf("current time: %v\n", now) // time.Time类型结构体

	year := now.Year()     // 2022
	month := now.Month()   // November
	day := now.Day()       // 29
	hour := now.Hour()     // 17
	minute := now.Minute() // 21
	second := now.Second() // 18
	fmt.Println(year, month, day, hour, minute, second)
}

# 时区 time.FixedZone time.LoadLocation

func timezoneDemo() {
	// 中国没有夏令时,使用一个固定的8小时的UTC时差。
	// 对于很多其他国家需要考虑夏令时。
	secondsEastOfUTC := int((8 * time.Hour).Seconds())
	// FixedZone 返回始终使用给定区域名称和偏移量(UTC 以东秒)的 Location。
	beijing := time.FixedZone("Beijing Time", secondsEastOfUTC)

	// 如果当前系统有时区数据库,则可以加载一个位置得到对应的时区
	// 例如,加载纽约所在的时区
	newYork, err := time.LoadLocation("America/New_York") // UTC-05:00
	if err != nil {
		fmt.Println("load America/New_York location failed", err)
		return
	}
	fmt.Println(beijing, newYork)

	// 加载上海所在的时区
	// shanghai, err := time.LoadLocation("Asia/Shanghai") // UTC+08:00
	// 加载东京所在的时区
	// tokyo, err := time.LoadLocation("Asia/Tokyo") // UTC+09:00
	// fmt.Println(shanghai, tokyo)

	// 创建时间对象需要指定位置。常用的位置是 time.Local(当地时间) 和 time.UTC(UTC时间)。
	timeInUTC := time.Date(2022, 02, 02, 02, 10, 10, 0, time.UTC)
	timeInLocal := time.Date(2022, 02, 02, 10, 10, 10, 0, time.Local) // 系统本地时间

	sameTimeInBeijing := time.Date(2022, 02, 02, 10, 10, 10, 0, beijing)
	sameTimeInNewYork := time.Date(2022, 02, 02, 10, 10, 10, 0, newYork)
	fmt.Println(timeInLocal)
	fmt.Println(timeInUTC)
	fmt.Println(sameTimeInBeijing)
	fmt.Println(sameTimeInNewYork)

	// 北京时间(东八区)比UTC早8小时,所以上面两个时间看似差了8小时,但表示的是同一个时间
	timesAreEqual := timeInUTC.Equal(sameTimeInBeijing)
	fmt.Println(timesAreEqual)

	// 纽约(西五区)比UTC晚5小时,所以上面两个时间看似差了5小时,但表示的是同一个时间
	timesAreEqual = timeInUTC.Equal(sameTimeInNewYork)
	fmt.Println(timesAreEqual)
}

# 时间戳 timeObj.Unix()

Unix Time是自1970年1月1日 00:00:00 UTC 至当前时间经过的总秒数

func timestampDemo() {
	now := time.Now()        // 当前时间对象
	timestamp := now.Unix()  // 秒级时间戳,int64
	milli := now.UnixMilli() // 毫秒级 Go.17+
	micro := now.UnixMicro() // 微秒级 Go.17+
	nano := now.UnixNano()   // 纳秒

	fmt.Println(timestamp, milli, micro, nano)
}

time 包还提供了一系列将 int64 类型的时间戳转换为时间对象的方法

func timestamp2Time() {
	// 获取北京时间所在的东八区时区对象
	secondsOfEastOfUTC := int((8 * time.Hour).Seconds())
	beijing := time.FixedZone("Beijing Time", secondsOfEastOfUTC)

	// 北京时间 2022-02-22 22:22:22.000000022 +0800 CST
	t := time.Date(2022, 02, 22, 22, 22, 22, 22, beijing)

	var (
		sec  = t.Unix()
		msec = t.UnixMilli()
		usec = t.UnixMicro()
	)

	// 将秒级时间戳转为时间对象(第二个参数为不足1秒的纳秒数)
	timeObj := time.Unix(sec, 22)
	fmt.Println(timeObj)           // 2022-02-22 22:22:22.000000022 +0800 CST
	timeObj = time.UnixMilli(msec) // 毫秒级时间戳转为时间对象
	fmt.Println(timeObj)           // 2022-02-22 22:22:22 +0800 CST
	timeObj = time.UnixMicro(usec) // 微秒级时间戳转为时间对象
	fmt.Println(timeObj)           // 2022-02-22 22:22:22 +0800 CST
}

# 时间间隔 time.Duration

time.Duration是time包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位

time.Duration表示一段时间间隔,可表示的最长时间段大约290年

const (
	Nanosecond  Duration = 1
	Microsecond          = 1000 * Nanosecond
	Millisecond          = 1000 * Microsecond
	Second               = 1000 * Millisecond
	Minute               = 60 * Second
	Hour                 = 60 * Minute
)

func durationDemo() {
	// 需要转换一下
	num := 1
	time.Sleep(time.Duration(num) * time.Second)

	// Add 时间对象加上时间间隔
	now := time.Now()
	later := now.Add(time.Hour) // 当前时间加1小时后的时间
	fmt.Println(later)

	// Sub 求两个时间之间的差值
	ret := later.Sub(now)
	fmt.Println(ret)

	// Before 判断 now 在 later 之前
	fmt.Println(now.Before(later))

	// After 判断 now 在 later 之后
	fmt.Println(now.After(later))
	
	// Equal 判断 now == later 会考虑时区的影响
	fmt.Println(now.Equal(later))
}

例如:time.Duration表示1纳秒,time.Second表示1秒

# 定时器 time.Tick(时间间隔)

func tickDemo() {
	ticker := time.Tick(time.Second)
	for i := range ticker {
		fmt.Println(i) //每秒都会执行的任务
	}
}

# 时间格式化 timeObj.Format

时间对象格式化为字符串,格式规则2006-01-02 15:04:05.000(记忆口诀为2006 1 2 3 4 5)

func formatDemo() {
	now := time.Now()
	// 格式化的模板为 2006-01-02 15:04:05

	// 24小时制
	fmt.Println(now.Format("2006-01-02 15:04:05"))
	fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan"))
	
	// 12小时制
	fmt.Println(now.Format("2006-01-02 03:04:05.000 PM"))

	// 小数点后写0,因为有3个0所以格式化输出的结果也保留3位小数
	fmt.Println(now.Format("2006/01/02 15:04:05.000")) // 2022/02/02 00:10:42.960
	// 小数点后写9,会省略末尾可能出现的0
	fmt.Println(now.Format("2006/01/02 15:04:05.999")) // 2022/02/02 00:10:42.96

	// 只格式化时分秒部分
	fmt.Println(now.Format("15:04:05"))
	// 只格式化日期部分
	fmt.Println(now.Format("2006.01.02"))
}

# 解析格式化字符串为时间对象 time.Parse time.ParseInLocation

其中time.Parse在解析时不需要额外指定时区信息

// parseDemo 指定时区解析时间
func parseDemo() {
	// 在没有时区指示符的情况下,time.Parse 返回UTC时间
	timeObj, err := time.Parse("2006/01/02 15:04:05", "2022/10/10 10:10:10")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(timeObj) // 2022-10-10 10:10:10 +0000 UTC

	// 在有时区指示符的情况下,time.Parse 返回对应时区的时间表示
	// RFC3339     = "2006-01-02T15:04:05Z07:00"
	timeObj, err = time.Parse(time.RFC3339, "2022-10-10T10:10:10+08:00")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(timeObj) // 2022-10-10 10:10:10 +0800 CST
}

time.ParseInLocation函数需要在解析时额外指定时区信息

// parseDemo 解析时间
func parseDemo() {
	now := time.Now()
	fmt.Println(now)
	// 加载时区
	loc, err := time.LoadLocation("Asia/Shanghai")
	if err != nil {
		fmt.Println(err)
		return
	}
	// 按照指定时区和指定格式解析字符串时间
	timeObj, err := time.ParseInLocation("2006/01/02 15:04:05", "2022/10/10 10:10:10", loc)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(timeObj)
}

# 并发概念

  • 串行、并发(同一时间段)与并行(同一时刻)
  • 进程、线程与协程(轻量级、用户态)
  • 部分并发模型
    • 线程&锁模型
    • CSP模型

Go语言中的并发程序主要是通过基于CSP(communicating sequential processes)的goroutine和channel来实现,当然也支持使用传统的多线程共享内存的并发方式

# Goroutine

  • Goroutine 是 Go 语言支持并发的核心

    • goroutine 以很小的栈开启,一般2KB,动态调整大小,可创建数万个;
    • goroutine 是由Go运行时(runtime)负责调度;
    • goroutine 是Go程序中最基本的并发执行单元;
  • go 关键字

go f()  // 创建一个新的 goroutine 运行函数f

go func(){
	// 支持匿名函数,注意最后的()和闭包时的作用域
}()

一个 goroutine 必定对应一个函数/方法,可以创建多个 goroutine 去执行相同的函数/方法。

  • 启动单个goroutine
func hello() {
	fmt.Println("hello")
}

func main() {
	go hello()
	fmt.Println("你好")

	// main goroutine 结束,会同时结束创建的hello goroutine
	// 笨方法,稍微等一下hello goroutine 执行完
	time.Sleep(time.Second)   
}

当并发不关心并发操作的结果或者有其它方式收集并发操作的结果时,sync.WaitGroup是实现等待一组并发操作完成的好方法

package main

import (
	"fmt"
	"sync"
)

// 声明全局等待组变量
var wg sync.WaitGroup

func hello() {
	fmt.Println("hello")
	wg.Done() // 告知当前goroutine完成
}

func main() {
	wg.Add(1) // 登记1个goroutine
	go hello()
	fmt.Println("你好")
	wg.Wait() // 阻塞等待登记的goroutine完成
}
  • 启动多个goroutine
package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func hello(i int) {
	defer wg.Done() // goroutine结束就登记-1
	fmt.Println("hello", i)
}
func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1) // 启动一个goroutine就登记+1
		go hello(i)
	}
	wg.Wait() // 等待所有登记的goroutine都结束
}
  • GPM调度模型

# Channel

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

Go语言采用的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信

如果说 goroutine 是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

  • channel类型
var 变量名称 chan 元素类型

chan:是关键字
元素类型:是指通道中传递元素的类型

var ch1 chan int   // 声明一个传递整型的通道
var ch2 chan bool  // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道
  • channel零值

未初始化的通道类型变量其默认零值是nil

var ch chan int
fmt.Println(ch) // <nil>
  • 初始化channel

声明的通道类型变量需要使用内置的make函数初始化之后才能使用

make(chan 元素类型, [缓冲大小])

ch4 := make(chan int)
ch5 := make(chan bool, 1)  // 声明一个缓冲区大小为1的通道
  • channel操作

通道共有发送(send)、接收(receive)和关闭(close)三种操作。而发送和接收操作都使用<-符号

ch := make(chan int)

// 发送:将一个值发送到通道中
ch <- 10 // 把10发送到ch中

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

// 关闭
close(ch)

关闭后的通道有以下特点:
1. 对一个关闭的通道再发送值就会导致 panic;
2. 对一个关闭的通道进行接收会一直获取值直到通道为空;
3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值;
4. 关闭一个已经关闭的通道会导致 panic
  • 无缓冲的通道

无缓冲的通道又称为阻塞的通道

func main() {
	ch := make(chan int)
	ch <- 10
	fmt.Println("发送成功")
}

上面这段代码能够通过编译,但是执行的时候会出现以下错误
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
        /Users/nining/learning-go/channel/main.go:7 +0x34

ch := make(chan int) 创建的是无缓冲的通道,无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段

func recv(c chan int) {
	fmt.Println("开始接收")
	time.Sleep(time.Second * 3)
	ret := <-c
	fmt.Println("接收成功", ret)
}

func main() {
	ch := make(chan int)
	go recv(ch) // 创建一个 goroutine 从通道接收值

	fmt.Println("开始发送")
	ch <- 10
	fmt.Println("发送成功")
}

使用无缓冲通道进行通信将导致发送和接收的 goroutine 同步化。因此,无缓冲通道也被称为同步通道

  • 有缓冲的通道
func main() {
	ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
	ch <- 10
	fmt.Println("发送成功")
}
  • 多返回值模式
value, ok := <- ch
1. value:从通道中取出的值,如果通道被关闭则返回对应类型的零值。
2. ok:通道ch关闭时返回 false,否则返回 true

func f2(ch chan int) {
	for {
		v, ok := <-ch
		if !ok {
			fmt.Println("通道已关闭")
			break
		}
		fmt.Printf("v:%#v ok:%#v\n", v, ok)
	}
}

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	close(ch)
	f2(ch)
}
  • for range接收值
func f3(ch chan int) {
	for v := range ch {
		fmt.Println(v)
	}
}

目前Go语言中并没有提供一个不对通道进行读取操作就能判断通道是否被关闭的方法

  • 单向通道

Go语言中提供了单向通道来处理这种需要限制通道只能进行某种操作的情况

<- chan int // 只接收通道,只能接收不能发送
chan <- int // 只发送通道,只能发送不能接收
  • 总结

# Select多路复用

在某些场景下我们可能需要同时从多个通道接收数据

Select 的使用方式类似于之前学到的 switch 语句,它也有一系列 case 分支和一个默认的分支。每个 case 分支会对应一个通道的通信(接收或发送)过程。select 会一直等待,直到其中的某个 case 的通信操作完成时,就会执行该 case 分支对应的语句。具体格式如下:

select {
case <-ch1:
	//...
case data := <-ch2:
	//...
case ch3 <- 10:
	//...
default:
	//默认操作
}

1.可处理一个或多个 channel 的发送/接收操作;
2. 如果多个 case 同时满足,select 会随机选择一个执行;
3, 对于没有 caseselect 会一直阻塞,可用于阻塞 main 函数,防止退出;
func main() {
	ch := make(chan int, 1)
	for i := 1; i <= 10; i++ {
		select {
		case x := <-ch:
			fmt.Println(x)
		case ch <- i:
		}
	}
}

# 课后作业

/*
使用 goroutine 和 channel 实现一个计算int64随机数和的程序,如随机数61345,计算其每个位数上数字和为19
1. 开启一个 goroutine 循环生成int64类型的随机数,发送到jobChan
2. 开启24个 goroutine 从jobChan中取出随机数计算各位数的和,将结果发送到resultChan
3. 主 goroutine 从resultChan中去结果并打印到终端
*/

package main

import (
	"fmt"
	"math/rand"
	"time"
)

// result 表示结果的结构体
type result struct {
	number int64 // 随机数
	sum    int64 // 计算得到的和
}

// genNumber 生成随机数int64
func genNumber() <-chan int64 {
	var jobChan = make(chan int64, 100)
	rand.Seed(time.Now().Unix())

	// 在后台一直执行
	go func() {
		for {
			// num := rand.Int63()
			num := rand.Intn(99999)
			select {
			case jobChan <- int64(num):
			default:
				time.Sleep(time.Microsecond)
			}
		}
	}()
	return jobChan
}

// sum 从jobChan中取出随机数计算各位数的和,将结果发送到resultChan
// 开启24个 goroutine来执行的函数,每个goroutine都可以循环取结果
// sum 函数的参数,要能接收闭包中的参数
func sum(jobChan <-chan int64, resultChan chan result) {
	for {
		select {
		case number := <-jobChan:
			r := result{
				number: number,
			}
			var sum int64
			for number > 0 {
				sum += number % 10
				number = number / 10
			}
			r.sum = sum
			resultChan <- r
		default:
			time.Sleep(time.Microsecond)
		}
	}
}

func main() {
	jobChan := genNumber()
	resultChan := make(chan result, 10)

	for i := 0; i < 24; i++ {
		go sum(jobChan, resultChan)
	}

	for {
		res := <-resultChan
		time.Sleep(time.Second)
		fmt.Printf("数字%d, 和%d\n", res.number, res.sum)
	}
}

上次更新: 1/10/2023, 12:41:52 PM