IO
获取文件信息 file
计算机中的文件时存储在外部介质(通常是磁盘)上的数据集合,文件分为文本文件和二进制文件。
file类是在os包中的,封装了底层的文件描述符和相关信息,同时封装了Read和Write的实现。
获取文件信息
创建目录、创建文件
IO读、写
文件复制
断点续传
bufio
package main
import (
"fmt"
"os"
)
/*
type FileInfo interface {
Name() string // base name of the file
Size() int64 // length in bytes for regular files; system-dependent for others
Mode() FileMode // file mode bits
ModTime() time.Time // modification time
IsDir() bool // abbreviation for Mode().IsDir()
// 获取更加详细的文件信息
Sys() any // underlying data source (can return nil)
}
*/
func main() {
// 获取某个文件的状态
// func Stat(name string) (FileInfo, error)
fileinfo, err := os.Stat("D:\\Environment\\GoWorks\\src\\xuego\\lesson06\\Demo06.go")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(fileinfo.Name()) // Demo06.go
fmt.Println(fileinfo.IsDir()) // false
fmt.Println(fileinfo.ModTime()) // 2023-02-26 14:56:36.4989813 +0800 CST
fmt.Println(fileinfo.Size()) // 439
fmt.Println(fileinfo.Mode()) // -rw-rw-rw-
}
科普:权限
r:可读 w:可写 x:可执行
- --- --- ---
type owner group others
- rw- rw- rw-
d rw- rw- rw-
如果没有那个权限就用 - 代替
八进制表示权限
r 004
w 002
x 001
- 000
- rwx rwx rwx
- 4+2+1 4+2+1 4+2+1
chomd 7 7 7
chomd 6 6 6
创建文件、目录
通过代码创建文件
路径:
相对路径
相对当前目录的路径
./ 当前目录
../ 上一级目录
绝对路径
从盘符开始的路径
创建目录
mkdir 路径+权限 创建单个目录
mkdirAll 路径+权限 创建层级目录
删除目录
remove 删除单个空文件夹
removeAll 强制删除目录
package main
import (
"fmt"
"os"
)
func main() {
// 删除文件夹
// 通过Remove方法只能删除单个空的文件夹
// func Remove(name string) error
err := os.Remove("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\file2")
if err != nil {
// The directory is not empty.
fmt.Println(err)
}
fmt.Println("单个空文件夹删除成功")
// 如果存在多层不为空的文件夹, 可用RemoveAll方法,此方法会删除指定目录下的所有文件,慎用
// 类似于 linux命令 rm -rf ./
// func RemoveAll(path string) error
err2 := os.RemoveAll("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\file2")
if err2 != nil {
fmt.Println(err2)
return
}
fmt.Println("文件夹删除成功")
}
// 创建文件夹(存在就无法创建,不存在就创建)
func m1() {
// ModePerm: 0777
// func Mkdir(name string, perm FileMode) error
err := os.Mkdir("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\file1", os.ModePerm)
if err != nil {
// Cannot create a file when that file already exists.
fmt.Println(err)
return
}
fmt.Println("文件夹创建完毕")
}
// 创建层级文件夹
func m2() {
// func MkdirAll(path string, perm FileMode) error
err := os.MkdirAll("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\file2\\aa\\bb\\cc", os.ModePerm)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("层级文件夹创建完毕")
}
创建文件
文件,就是一个File结构体对象,由相应的方法和属性。
create 方法,如果文件存在就返回File对象,如果不存在就创建并返回File对象。
package main
import (
"fmt"
"os"
)
func main() {
// 返回的file结构体对象就是需要的文件
// func Create(name string) (*File, error)
file1, err := os.Create("a.go")
if err != nil {
fmt.Println(err)
return
}
// &{0xc000004a00}
fmt.Println(file1)
// 删除文件
os.Remove("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\a.go")
}
IO读
与文件建立连接
package main
import (
"fmt"
"os"
)
func main() {
file1, err := os.Open("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\a.txt")
if err != nil {
fmt.Println(err)
return
}
// &{0xc000004a00}
fmt.Println(file1)
// 打开文件时,选定权限,可读可写的方式打开
// OpenFile(文件名, 打开方式:可读、可写等, 权限)
// func OpenFile(name string, flag int, perm FileMode) (*File, error)
file2, err2 := os.OpenFile("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\a.txt", os.O_RDONLY|os.O_WRONLY, os.ModePerm)
if err2 != nil {
fmt.Println(err2)
return
}
// &{0xc0000cca00}
fmt.Println(file2)
}
读取文件
file.Read([]byte),将file中的数据读取到 []byte中,返回值n和error, n代表读取到的字节数,error如果是EOF错误,就代表文件读取完毕了!Read方法一直被调用,就代表光标移动。
package main
import (
"fmt"
"os"
)
// 读取文件数据
func main() {
// 建立连接
// a.txt内容:abcd
file, _ := os.Open("a.txt")
// 关闭连接(习惯使用defer)
defer file.Close()
// 1、创建一个容器,缓冲区,接收读取的数据
bs := make([]byte, 2, 1024)
// 2、读取到缓冲区中
// func (f *File) Read(b []byte) (n int, err error)
n, err := file.Read(bs)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(n) // 2
// 获取读取到的字符串
fmt.Println(string(bs)) // ab
n, err = file.Read(bs)
if err != nil {
// 如果光标读取到文件末尾,就会返回EOF
fmt.Println(err)
return
}
fmt.Println(n) // 2
fmt.Println(string(bs)) // cd
n, err = file.Read(bs)
if err != nil {
// 如果光标读取到文件末尾,就会返回EOF
fmt.Println(err)
return
}
fmt.Println(n)
fmt.Println(string(bs))
}
IO写
建立连接(需要使用openFile方法并设置相应的权限,可读可写可扩充)
关闭连接
写入文件的方法wirte/writeString
package main
import (
"fmt"
"os"
)
// 读取文件数据
func main() {
// 建立连接
// 权限添加:如果需要追加内容就得加O_APPEND权限,如果不加就是从头开始覆盖
file, _ := os.OpenFile("a.txt", os.O_WRONLY|os.O_RDONLY|os.O_APPEND, os.ModePerm)
// 关闭连接
defer file.Close()
// 操作
bs := []byte{65, 66, 67, 68, 69}
n, err := file.Write(bs)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(n) // 5
n, err = file.WriteString("我爱Go语言")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(n) //14
}
文件复制
自己手写copy方法
package main
import (
"fmt"
"io"
"os"
)
func main() {
srcSource := "D:\\Environment\\GoWorks\\src\\xuego\\IMG_20200604_205838.jpg"
destSource := "D:\\Environment\\GoWorks\\src\\xuego\\copy.jpg"
copy(srcSource, destSource, 1024)
}
// 文件复制
func copy(srcSource, destSource string, bufferSize int) {
srcFile, err := os.Open(srcSource)
if err != nil {
fmt.Println("Open错误:", err)
}
destFile, err := os.OpenFile(destSource, os.O_WRONLY|os.O_CREATE, os.ModePerm)
if err != nil {
fmt.Println("OpenFile错误:", err)
}
// 关闭
defer srcFile.Close()
defer destFile.Close()
buf := make([]byte, bufferSize)
for {
// 读取目标文件内容到缓冲区
n, err := srcFile.Read(buf)
if n == 0 || err == io.EOF {
fmt.Println("文件复制完毕")
break
} else if err != nil {
fmt.Println("读取错误:", err)
return // 出现错误,终止函数
}
// 将缓冲区的内容写到目标文件
_, err = destFile.Write(buf[:n])
if err != nil {
fmt.Println("写出错误:", err)
}
}
}
io包提供的copy方法
package main
import (
"fmt"
"io"
"os"
)
func main() {
srcSource := "D:\\Environment\\GoWorks\\src\\xuego\\IMG_20200604_205838.jpg"
destSource := "D:\\Environment\\GoWorks\\src\\xuego\\copy1.jpg"
copy(srcSource, destSource)
}
// 文件复制
func copy(srcSource, destSource string) {
srcFile, err := os.Open(srcSource)
if err != nil {
fmt.Println("Open错误:", err)
}
destFile, err := os.OpenFile(destSource, os.O_WRONLY|os.O_CREATE, os.ModePerm)
if err != nil {
fmt.Println("OpenFile错误:", err)
}
// 关闭
defer srcFile.Close()
defer destFile.Close()
n, err := io.Copy(destFile, srcFile)
if err != nil {
fmt.Println("文件复制错误:", err)
}
fmt.Println("复制文件的字节大小: ", n)
}
os包提供的ReadFile、WriteFile方法
package main
import (
"fmt"
"os"
)
func main() {
srcSource := "D:\\Environment\\GoWorks\\src\\xuego\\IMG_20200604_205838.jpg"
destSource := "D:\\Environment\\GoWorks\\src\\xuego\\copy2.jpg"
copy(srcSource, destSource)
}
// 文件复制
func copy(srcSource, destSource string) {
fileBuf, _ := os.ReadFile(srcSource)
err := os.WriteFile(destSource, fileBuf, 0777)
if err != nil {
fmt.Println("文件复制错误:", err)
} else {
fmt.Println("文件复制成功")
}
}
Seeker 接口
type Seeker interface {
// 1、offset 偏移量
// 2、whence 设置当前光标的位置
Seek(offset int64, whence int) (int64, error)
}
// Seek whence values.
const (
SeekStart = 0 // seek relative to the origin of the file
SeekCurrent = 1 // seek relative to the current offset
SeekEnd = 2 // seek relative to the end
)
package main
import (
"fmt"
"io"
"os"
)
func main() {
file, _ := os.OpenFile("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\a.txt", os.O_RDWR, os.ModePerm)
defer file.Close()
// io.SeekStart 相对于文件开头,偏移2
file.Seek(2, io.SeekStart)
buf := []byte{0}
file.Read(buf)
fmt.Println(string(buf))
// 相对于当前位置
file.Seek(3, io.SeekCurrent)
file.Read(buf)
fmt.Println(string(buf))
// 在结尾追加内容
file.Seek(0, io.SeekEnd)
file.WriteString("Go语言")
}
断点续传
package main
import (
"fmt"
"io"
"os"
"strconv"
)
// 断点续传
func main() {
// 源文件地址
srcFile := "D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\IMG_20200604_205838.jpg"
// 目标文件地址
destFile := "D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\copy.jpg"
// 临时文件地址
tempFile := "D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\temp.txt"
// 源文件
file1, _ := os.Open(srcFile)
// 目标文件
file2, _ := os.OpenFile(destFile, os.O_CREATE|os.O_RDWR, os.ModePerm)
// 临时文件
file3, _ := os.OpenFile(tempFile, os.O_CREATE|os.O_RDWR, os.ModePerm)
// 文件关闭
defer file1.Close()
defer file2.Close()
// 临时文件光标重置到开头
file3.Seek(0, io.SeekStart)
buf := make([]byte, 1024, 1024)
// 读取临时文件内容到buf
n, _ := file3.Read(buf)
counStr := string(buf[:n])
count, _ := strconv.ParseInt(counStr, 10, 64)
fmt.Println("temp.txt中记录的值为:", count)
// 源文件、目标文件的光标移动到临时文件末尾(也就是断点处)
file1.Seek(count, io.SeekStart)
file2.Seek(count, io.SeekStart)
bufData := make([]byte, 1024, 1024)
total := int(count)
for {
// 读取目标文件内容到bufData
readNum, err := file1.Read(bufData)
if err == io.EOF {
fmt.Println("文件传输完毕了")
file3.Close()
// 删除临时文件
os.Remove(tempFile)
break
}
// 将buf内容写入到目标文件
writeNum, err := file2.Write(bufData[:readNum])
// 将写入数据记录到total中,也就是传输进度
total = total + writeNum
// 重置临时文件光标到开头
file3.Seek(0, io.SeekStart)
// 写入传输数据到临时文件
file3.WriteString(strconv.Itoa(total))
// 模拟断电
//if total > 5000 {
// panic("断电了")
//}
}
}
遍历文件夹
package main
import (
"fmt"
"log"
"os"
)
func main() {
dir := "D:\\Environment\\GoWorks\\src\\xuego\\lesson01"
tree(dir, 0)
}
func tree(dir string, level int) {
// 体现层级
tabString := "|--"
for i := 0; i < level; i++ {
tabString = "| " + tabString
}
// 获取目录信息[]DirEntry
// func ReadDir(name string) ([]DirEntry, error)
fileInfos, err := os.ReadDir(dir)
if err != nil {
log.Println(err)
return
}
// 遍历目录信息,获取子文件信息
for _, file := range fileInfos {
fileName := dir + "\\" + file.Name()
fmt.Println(tabString + file.Name())
// 如果子文件是文件夹,继续遍历
if file.IsDir() {
tree(fileName, level+1)
}
}
}
bufio
bufio Go语言自带的IO操作包,使用此包可大幅提升文件的读写效率。
如果频繁地访问本地磁盘文件,io包的操作效率较低。
bufio包,提供了一个缓冲区,读写都先在缓冲区中,然后再一次性读写到文件中,从而降低对本地磁盘的访问次数。
读出
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.Open("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\Demo01.go")
if err != nil {
fmt.Println(err)
}
defer file.Close()
// 读取文件内容到Reader中
// func NewReader(rd io.Reader) *Reader
bufioReader := bufio.NewReader(file)
buf := make([]byte, 1024)
n, err := bufioReader.Read(buf)
fmt.Println("读取到了多少个字节:", n)
fmt.Println("读取到的内容:", string(buf[:n]))
// 读取键盘的输入,输入实际是流
inputReader := bufio.NewReader(os.Stdin)
// delim 到何位置结束读取
// func (b *Reader) ReadString(delim byte) (string, error)
readString, _ := inputReader.ReadString('\n')
fmt.Println("输出键盘输入的信息:", readString)
}
写入
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, _ := os.OpenFile("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\a.txt",
os.O_RDWR|os.O_CREATE|os.O_APPEND, os.ModePerm)
defer file.Close()
// 获取写入文件的Writer
// func NewWriter(w io.Writer) *Writer
fileWriter := bufio.NewWriter(file)
// 字符串写入到文件中
// func (b *Writer) WriteString(s string) (int, error)
writeNum, _ := fileWriter.WriteString("我爱Go语言")
fmt.Println("writeNum: ", writeNum)
// 写入文件需要手动刷新一下
fileWriter.Flush()
}
Goroutine
进程、线程、协程
程序:指令和数据的有序集合,静态的概念,本身没有任何含义。
进程(Process),线程(Thread), 协程(Coroutine, 也叫轻量级线程)
进程
进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序",它是CPU资源分配和调度的独立单位。
进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;
数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。 进程的局限是创建、撤销和切换的开销比较大。
线程
线程是在进程之后发展出来的概念。线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、 程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。
线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,缺点是线程没有自己的系统资源,同一进程的各线程可以共享进程所拥有的系统资源,如果把进程比作一个车间,那么线程就好比是车间里面的工人。不过对于某些独占性资源存在锁机制,处理不当可能会产生”死锁"。
协程Goroutine
协程是一种用户态的轻量级线程,又称微线程,英文名Coroutine,协程的调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。
就好比是启动了一个函数,单次执行完毕它。不影响我们main线程的执行。
子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。
与传统的系统级线程和进程相比,协程的最大优势在于其"轻量级”,可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常最多也不能超过1万的。这也是协程也叫轻量级线程的原因。
Goroutine
Go语言对于并发的实现是靠协程,Groutine
Goroutine是Go语言特有的名词,区别于进程Process、线程Thread。
Goroutine是其它函数或方法同时运行的函数或方法。Goroutine可以被认为是轻量级的线程。与线程相比,创建Goroutine的成本很小,就是一段代码,一个函数入口,在堆上为其分配的一个堆栈(初始大小为4K,会随着程序的执行自动增长删除)。因为非常廉价,所以Go可以轻松并发运行数千个Goroutine。
使用Goroutine,只需要在函数或方法前面添加一个go 关键字即可。
package main
import "fmt"
func main() {
// goroutine: 和普通方法的调用完全不同,它是并发执行的,快速交替
go hello()
for i := 0; i < 100; i++ {
fmt.Println("main - ", i)
}
}
func hello() {
for i := 0; i < 100; i++ {
fmt.Println("hello - ", i)
}
}
Goroutine的执行规则:
当新的Goroutine开始时, Goroutine调用立即返回。与函数不同,go不等待Goroutine执行结束;
当Goroutine调用,并且Goroutine的任何返回值被忽略之后,go立即执行到下一行代码;
main的Goroutine应该为其他的Goroutines执行。如果main的Goroutine终止了,程序将被终止,而其他Goroutine将不会运行。
主Goroutine main
封装main函数的goroutine称为主goroutine。
主goroutine所做的事情并不是执行main函数那么简单。它首先要做的是:设定每一个goroutine所能申请的栈空间的最大尺寸。在32位的计算机系统中此最大尺寸为250MB,而在64位的计算机系统中此尺寸为1GB。如果有某个goroutine的栈空间尺寸大于这个限制,那么运行时系统就会引发一个栈溢出(stack overflow)的运行时恐慌。随后,这个go程序的运行也会终止。
此后,主goroutine会 进行一系列的初始化工作,涉及的工作内容大致如下:
创建一个特殊的defer语句,用于在主goroutine退出时做必要的善后处理。因为主goroutine也可能非正常的结束;
启动专用于在后台清扫内存垃圾的goroutine,并设置GC可用的标识;
执行main包中所引用包下的init函数;
执行main函数;
执行完main函数后,它还会检查主goroutine是否引发了运行时恐慌,并进行必要的处理。
程序运行完毕后,主goroutine会结束自己以及当前进程的运行。
runtime包
获取系统信息runtime、让出时间片、终止goroutine
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// 终止程序 runtime.Goexit()
go func() {
fmt.Println("start")
test()
fmt.Println("end")
}()
time.Sleep(time.Second * 3)
}
func test() {
defer fmt.Println("test defer")
// 终止当前的goroutine
runtime.Goexit()
fmt.Println("test")
}
func runtime1() {
// 获取GOROOT目录
fmt.Println("GOROOT path: ", runtime.GOROOT()) // D:\Environment\Go
// 获取操作系统信息
fmt.Println("OS System: ", runtime.GOOS) // windows
// 获取CPU数量
fmt.Println("CPU num: ", runtime.NumCPU()) // 8
}
func runtime2() {
// goroutine是竞争cpu的
go func() {
for i := 0; i < 5; i++ {
fmt.Println("goroutine-", i)
}
}()
for i := 0; i < 5; i++ {
// Gosched 礼让,让出时间片,让其他的goroutine先执行
// 由于cpu的调度是随机的,所以并非绝对的礼让
runtime.Gosched()
fmt.Println("main-", i)
}
}
临界资源安全问题
临界资源:指并发环境中多个进程、线程、协程共享的资源
在并发编程中对临界资源的处理不当,往往会导致数据不一致的问题。
package main
import (
"fmt"
"time"
)
func main() {
// 临界资源
a := 1
go func() {
// 临界资源a被修改为2
a = 2
fmt.Println("goroutine a:", a)
}()
a = 3
time.Sleep(3 * time.Second)
fmt.Println("mian a:", a)
}
售票问题
并发本身并不复杂,但是因为有了资源竞争的问题,就使得我们开发出好的并发程序变得复杂起来,因为会引起很多莫名其妙的问题。
如果多个goroutine在访问同一个数据资源的时候,其中一个线程修改了数据,那么这个数值就被修改了,对于其他的goroutine来讲,这个数值可能是不对的。
package main
import (
"fmt"
"time"
)
// 定义全局变量 票库存为10张
var ticket int = 10
func main() {
// 单线程不存在问题,多线程资源争抢就出现了问题
go saleTickets("张三")
go saleTickets("李四")
go saleTickets("王五")
go saleTickets("赵六")
time.Sleep(time.Second * 5)
}
// 售票函数
func saleTickets(name string) {
for {
if ticket > 0 {
time.Sleep(time.Millisecond * 1)
fmt.Println(name, "剩余票的数量为:", ticket)
ticket--
} else {
fmt.Println("票已售完")
break
}
}
}
发现结果和预想的不同,多线程加入之后,原先单线程的逻辑出现了问题。出现了临界资源安全问题。
sync包 - 锁
要想解决临界资源安全的问题,很多编程语言的解决方案都是同步。通过上锁的方式,某一时间段,只能允许一个goroutine来访问这个共享数据,当前goroutine访问完毕, 解锁后,其他的goroutine才 能来访问。
我们可以借助于sync包下的锁操作。 synchronization
但是实际上,在Go的并发编程中有一句很经典的话:不要以共享内存的方式去通信:锁,而要以通信的方式去共享内存。
在Go语言中并不鼓励用锁保护共享状态的方式,在不同的Goroutine中分享信息(以共享内存的方式去通信)。而是鼓励通过channeI将共享状态或共享状态的变化在各个Goroutine之间传递(以通信的方式去共享内存),这样同样能像用锁一样保证在同一的时间只有一个Goroutine访问共享状态。
当然,在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多。
package main
import (
"fmt"
"sync"
"time"
)
// 定义全局变量 票库存为10张
var ticket int = 10
// 定义一个锁 Mutex
var mutex sync.Mutex
func main() {
// 单线程不存在问题,多线程资源争抢就出现了问题
go saleTickets("张三")
go saleTickets("李四")
go saleTickets("王五")
go saleTickets("赵六")
time.Sleep(time.Second * 5)
}
// 售票函数
func saleTickets(name string) {
for {
// 在拿到共享资源之前上锁
mutex.Lock()
if ticket > 0 {
time.Sleep(time.Millisecond * 1)
fmt.Println(name, "剩余票的数量为:", ticket)
ticket--
} else {
// 票售完后解锁
mutex.Unlock()
fmt.Println("票已售完")
break
}
// 操作完后解锁
mutex.Unlock()
}
}
同步等待组
package main
import (
"fmt"
"sync"
)
// 同步等待组
var wg sync.WaitGroup
func main() {
// 判断有几个线程
wg.Add(2)
go test1()
go test2()
wg.Wait() // 等待 wg 归零后才会继续向下执行
//time.Sleep(time.Second * 3)
}
func test1() {
for i := 0; i < 10; i++ {
fmt.Println("test1-", i)
}
wg.Done() // 告知当前goroutine已结束
}
func test2() {
defer wg.Done() // 告知当前goroutine已结束
for i := 0; i < 10; i++ {
fmt.Println("test2-", i)
}
}
package main
import (
"fmt"
"sync"
"time"
)
// 定义全局变量 票库存为10张
var ticket int = 10
// 锁
var mutex sync.Mutex
// 同步等待组
var wg sync.WaitGroup
func main() {
// 添加4个线程
wg.Add(4)
// 单线程不存在问题,多线程资源争抢就出现了问题
go saleTickets("张三")
go saleTickets("李四")
go saleTickets("王五")
go saleTickets("赵六")
// 等待线程结束
wg.Wait()
}
// 售票函数
func saleTickets(name string) {
defer wg.Done()
for {
mutex.Lock()
if ticket > 0 {
time.Sleep(time.Millisecond * 1)
fmt.Println(name, "剩余票的数量为:", ticket)
ticket--
} else {
mutex.Unlock()
fmt.Println("票已售完")
break
}
mutex.Unlock()
}
}
channel
通道的概念及定义
Go语言不建议使用锁机制解决多线程问题,建议使用通道。
通道,类似于水管,数据可以从一端流到另一端。一个goroutine需要将一些信息告诉另外一个goroutine ,就直接将数据信息放入chan通道即可。通道必须作用在两个及两个以上的Goroutine。
不要通过共享内存来通信,而应该通过通信共享内存。
package main
import (
"fmt"
"time"
)
// 定义通道chan
func main() {
// 定义一个 bool 通道
var ch chan bool
ch = make(chan bool)
// 在一个Goroutine往通道中存入数据
go func() {
// 模拟阻塞等待
time.Sleep(time.Second * 3)
ch <- true
}()
// 另一个Goroutine可以从通道中取数据(线程之间的通信)
// 阻塞等待ch拿到值
data := <-ch
fmt.Println("data:", data)
}
一个通道发送和接收数据,默认是阻塞的。
当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个Goroutine从该通道读取数据。
相对地,当从通道读取数据时,读取被阻塞,直到一个Goroutine将数据写入该通道。
本身channel就是同步的, 意味着同一时间,只能有一个goroutine来操作。
最后,通道是goroutine之间的连接,所有通道的发送和接收必须处在不同的goroutine中。
这些通道的特性是帮助Goroutines有效地进行通信,而无需像使用其他编程语言中非常常见的显式锁或条件变量。
死锁
如果创建了chan,没有 Goroutine 来使用了,则会出现死锁。
使用通道时要考虑的一个重要因素是死锁。如果Goroutine在一 个通道 上发送数据,那么预计其他的Goroutine应该接收数据。如果这种情况不发生,那么程序将在运行时出现死锁。
类似地,如果Goroutine 正在等待从通道接收数据,那么必须有另一个Goroutine将会在该通道上写入数据,否则程序将会死锁。
package main
// 死锁的产生
func main() {
ch := make(chan int)
// 如果通道中存入数据,没有其它Goroutine消耗数据
// 则会产生死锁,fatal error: all goroutines are asleep - deadlock!
// 相反亦然
ch <- 1
}
死锁的产生:
单线程的使用,没有其他的goroutine消费
两个chan,互相需要对方的数据,但是由于判断,拿不到对方的数据。
sync 锁产生的死锁。
通道关闭
通过ok的状态判断通道是否关闭
package main
import (
"fmt"
"time"
)
// 关闭通道
func main() {
ch1 := make(chan int)
// 存入数据
go test(ch1)
// 读取数据
for {
time.Sleep(time.Second)
// ok 判断通道的状态是否关闭,如果false 表示关闭
data, ok := <-ch1
if !ok {
fmt.Println("读取完毕:", ok) // false
break
}
fmt.Println("ch1 data: ", data)
}
}
func test(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
}
// 告诉接收方已经结束,不会再有数据放入通道了
close(ch)
}
通过for range循环遍历通道中的数据,简化开发
package main
import (
"fmt"
"time"
)
// 关闭通道
func main() {
ch1 := make(chan int)
// 存入数据
go test(ch1)
// 读取数据
for data := range ch1 {
time.Sleep(time.Second)
fmt.Println(data)
}
fmt.Println("end")
}
// 通道可以进行参数传递
func test(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
}
// 告诉接收方已经结束,不会再有数据放入通道了
close(ch)
}
缓冲通道
非缓冲通道
只能存一个数据,发送和接收都是阻塞的,一个发送对应一个接收。
缓冲通道
通道带有缓冲区,发送方发送的数据直到缓冲区填满为止,才会被阻塞,接收方也是,只有缓冲区清空,才会被阻塞。
package main
import (
"fmt"
"strconv"
"time"
)
// 缓冲通道
func main() {
// 非缓冲通道
ch1 := make(chan int)
fmt.Println(cap(ch1), len(ch1)) // 0 0
// 缓冲通道
ch2 := make(chan string, 5)
fmt.Println(cap(ch2), len(ch2)) // 5 0
ch2 <- "1"
ch2 <- "2"
ch2 <- "3"
ch2 <- "4"
ch2 <- "5"
fmt.Println(cap(ch2), len(ch2)) // 5 5
// 如果通道满了,还没有goroutine取数据,会死锁
// fatal error: all goroutines are asleep - deadlock!
// ch2 <- "6"
fmt.Println("--------------------")
ch3 := make(chan string, 4)
go test(ch3)
for s := range ch3 {
time.Sleep(time.Second)
fmt.Println("main中读取的数据:", s)
}
fmt.Println("main-end")
}
func test(ch chan string) {
for i := 0; i < 10; i++ {
ch <- "test - " + strconv.Itoa(i)
fmt.Println("子goroutine存入数据:test-", i)
}
// 关闭通道
close(ch)
}
缓冲通道,可以定义缓冲区的数量。
如果缓冲区没有满,可以继续存放,如果满了,也会阻塞等待。
如果缓冲区空的,读取也会等待,如果缓冲区中有多个数据,依次按照先进先出的规则进行读取。
如果缓冲区满了,同时有两个线程在读或者写,这个时候和普通的通道一样。一进一出。
单向通道
package main
import "fmt"
// 定向通道
func main() {
// 双向通道
ch1 := make(chan int, 1)
ch1 <- 100
data := <-ch1
fmt.Println(data) // 100
// 单向通道
ch2 := make(chan<- int, 1) // 只能写数据,不能读
ch2 <- 200
// data := <- ch2 Invalid operation: <- ch2 (receive from the send-only type chan<- int)
// ch3 := make(<-chan int, 1) // 只能读数据,不能写
// Invalid operation: ch3 <- 300 (send to the receive-only type <-chan int)
// ch3 <- 300
}
package main
import (
"fmt"
"time"
)
// 定向通道
func main() {
// 单向通道使用场景
// 指定函数读写数据,防止通道滥用
ch := make(chan int, 1)
go writeOnly(ch)
go readOnly(ch)
time.Sleep(time.Second)
}
// 指定函数,只能往通道中写数据,不能读数据
func writeOnly(ch chan<- int) {
ch <- 100
}
// 指定函数,只能读取数据,不能写数据
func readOnly(ch <-chan int) {
data := <-ch
fmt.Println(data)
}
Select
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
go func() {
time.Sleep(time.Second * 2)
ch1 <- 100
}()
go func() {
time.Sleep(time.Second * 2)
ch2 <- 200
}()
// select 只是在通道中使用,case表达式获取通道结果,就会输出,从而抛弃其它的
select {
case num1 := <-ch1:
fmt.Println(num1)
case num2 := <-ch2:
fmt.Println(num2)
default:
fmt.pringtln("default")
}
}
每一个case都必须是一个通道的操作;
所有chan操作都要有结果(通道表达式都必须会被求值);
如果任意的通道拿到了结果,他就会立即执行该case,其它的都会被忽略。
如果有多个case,select会随机选取一个运行,其它就不会运行;
如果存在default语句,没有拿到相应的值,就会执行该语句;如果不存在;阻塞等待select直到拿到某个通道的结果。
Timer定时器
通道的应用场景一
可以控制程序在某个时间执行
package main
import (
"fmt"
"time"
)
// 定时器
func main() {
// 创建一个定时器 NewTimer
timer := time.NewTimer(time.Second * 3)
// 当前时间
// 2023-07-05 22:56:08.6040883 +0800 CST m=+0.018072201
fmt.Println(time.Now())
// time.C 时间通道,通道中存放着定时器对应的时间
timeChan := timer.C
// 2023-07-05 22:56:11.6210377 +0800 CST m=+3.035021601
fmt.Println(<-timeChan)
timer2 := time.NewTimer(time.Second * 5)
// 手动停止定时器
timer2.Stop()
}
package main
import (
"fmt"
"time"
)
// 定时器
func main() {
// 通道中放入当前时间之后的某个时间
timeChan := time.After(time.Second * 3)
// 2023-07-05 23:07:33.0042737 +0800 CST m=+0.015017801
fmt.Println(time.Now())
chanTime := <-timeChan
// 2023-07-05 23:07:33.0042737 +0800 CST m=+0.015017801
fmt.Println(chanTime)
// 在3s以后执行发邮件
time.AfterFunc(time.Second*3, mail)
// 等待上面结束
time.Sleep(time.Second * 4)
}
func mail() {
fmt.Println("发邮件")
}