文章目录
- 【C++转GO】文件操作+协程和管道
-
- 文件操作
- 协程和管道
-
- Go协程(Goroutine)
- 协程同步
- Go的锁机制详解
- 管道(Channel)
-
- 基本语法
- Channel类型
- Channel的方向
- [for range遍历channel的注意事项](#for range遍历channel的注意事项)
-
- 正确的使用方式
- [for range的工作原理](#for range的工作原理)
- Select语句
- 常见的协程模式
-
- [1. 生产者-消费者模式](#1. 生产者-消费者模式)
- [2. 工作池模式](#2. 工作池模式)
- [3. 超时控制](#3. 超时控制)
【C++转GO】文件操作+协程和管道
文件操作
主要是讲几个函数
go
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main() {
// 演示文件读取和写入操作
// ========== 1. os.Open - 打开文件用于读取 ==========
// 参数:filename (string) - 要打开的文件路径
file, err := os.Open("example.txt")
if err != nil {
fmt.Printf("打开文件失败: %v\n", err)
return
}
defer file.Close() // Close() 关闭文件,释放资源
// ========== 2. bufio.NewReader - 创建带缓冲的读取器 ==========
// 参数:r (io.Reader) - 实现了Reader接口的对象(如文件)
reader := bufio.NewReader(file)
// ========== 3. ReadString - 读取字符串直到指定分隔符 ==========
// 参数:delim (byte) - 分隔符,常用 '\n' 表示读取一行
line1, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
fmt.Printf("读取失败: %v\n", err)
return
}
fmt.Printf("第一行: %s", line1)
// ========== 4. io.EOF - 文件结束错误,表示已读到文件末尾 ==========
// 当 ReadString 返回 io.EOF 时,说明文件已经读取完毕
line2, err := reader.ReadString('\n')
if err == io.EOF {
fmt.Println("已到达文件末尾")
} else if err != nil {
fmt.Printf("读取失败: %v\n", err)
return
} else {
fmt.Printf("第二行: %s", line2)
}
// ========== 5. os.ReadFile - 一次性读取整个文件 ==========
// 参数:filename (string) - 要读取的文件路径
// 返回:[]byte - 文件内容,error - 错误信息
content, err := os.ReadFile("example.txt")
if err != nil {
fmt.Printf("读取文件失败: %v\n", err)
return
}
fmt.Printf("\n文件全部内容:\n%s\n", string(content))
// ========== 6. os.OpenFile - 打开文件,支持多种模式 ==========
// 参数:
// name (string) - 文件路径
// flag (int) - 打开模式:os.O_CREATE(创建), os.O_WRONLY(只写), os.O_APPEND(追加), os.O_TRUNC(清空) 等
// perm (os.FileMode) - 文件权限,如 0644 表示 rw-r--r--
outputFile, err := os.OpenFile("output.txt", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
fmt.Printf("打开文件失败: %v\n", err)
return
}
defer outputFile.Close()
// ========== 7. bufio.NewWriter - 创建带缓冲的写入器 ==========
// 参数:w (io.Writer) - 实现了Writer接口的对象(如文件)
writer := bufio.NewWriter(outputFile)
// ========== 8. WriteString - 写入字符串 ==========
// 参数:s (string) - 要写入的字符串
// 返回:n (int) - 写入的字节数,err (error) - 错误信息
n, err := writer.WriteString("Hello, World!\n")
if err != nil {
fmt.Printf("写入失败: %v\n", err)
return
}
fmt.Printf("已写入 %d 字节\n", n)
// 继续写入
writer.WriteString("这是第二行内容\n")
writer.WriteString("使用缓冲写入器可以提高性能\n")
// ========== 9. Flush() - 将缓冲区数据刷新到文件 ==========
// 无参数,将缓冲区的所有数据强制写入底层文件
// 如果不调用 Flush(),数据可能还在缓冲区中,不会写入文件
err = writer.Flush()
if err != nil {
fmt.Printf("刷新缓冲区失败: %v\n", err)
return
}
fmt.Println("数据已成功写入文件")
}
协程和管道
Go协程(Goroutine)
Go协程是Go语言中的轻量级线程,由Go运行时管理。与传统线程相比,协程更加轻量,创建和销毁的开销更小。
我们要知道,切换线程需要更换寄存器的内容,需要进行现场保护和CPU调度,需要由用户态到内核态的转换。
协程比线程更加轻量级,因为协程只运行在用户态,表现上只是不同的函数相互转换,不是线程来回转换。另外,C++20也更新了协程,不同的是C++20的协程是无栈协程,相比GO的有栈协程更快。但是C++的协程还不够成熟,用起来不如GO方便
基本语法
go
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
for i := 0; i < 5; i++ {
fmt.Printf("Hello %s: %d\n", name, i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
// 使用 go 关键字启动协程
go sayHello("协程1")
go sayHello("协程2")
// 主协程执行
sayHello("主协程")
// 等待一段时间,让协程执行完毕
time.Sleep(1 * time.Second)
}
// 除了等待一段时间外,可以使用Wait:
var wg sync.WaitGroup
func main(){
for i := 1; i <= 5; i++ {
wg.Add(1)
go func(n int) {
fmt.Println(n)
wg.Done() // 协程执行完毕后减一
}(i) // 这个协程调用匿名函数,使用外部的变量还有个小巧思:如果不使用传参,而是使用闭包会怎么样。讲解在下文
}
wg.Wait() // 等讲到0就不阻塞了
}
小巧思------闭包陷阱
go
func main(){
for i := 1; i < 5; i++ {
go func(){
fmt.Println(i)
}()
}
}
如果使用闭包,协程获取i不通过传参,而是通过捕获,那么当协程执行的时候,i的值可能已经是5了,所以打印结果与预期不符
协程特点
- 轻量级:协程比线程占用更少的内存和CPU资源
- 并发执行:多个协程可以并发执行
- 自动调度:由Go运行时自动调度,无需手动管理
- 通信:通过channel进行协程间通信
协程同步
sync.WaitGroup
用于等待一组协程完成。
go
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 协程结束时调用
fmt.Printf("Worker %d 开始工作\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d 完成工作\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 增加计数
go worker(i, &wg)
}
wg.Wait() // 等待所有协程完成
fmt.Println("所有工作完成")
}
context.Context
用于控制协程的取消和超时。
go
package main
import (
"context"
"fmt"
"time"
)
func longRunningTask(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("任务 %d 被取消: %v\n", id, ctx.Err())
return
default:
fmt.Printf("任务 %d 正在运行\n", id)
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
go longRunningTask(ctx, 1)
go longRunningTask(ctx, 2)
time.Sleep(3 * time.Second)
}
Go的锁机制详解
Go的锁是锁定操作系统线程,不是协程!
理解Go的并发模型
- 协程(Goroutine):用户空间的轻量级线程,由Go运行时管理
- 操作系统线程:内核级线程,Go运行时会创建和管理
- M:N调度:M个协程映射到N个操作系统线程
锁的工作原理
当协程获取锁时:
- 协程尝试获取锁
- 如果获取失败,当前操作系统线程被阻塞
- Go运行时可能会将其他协程调度到其他线程继续执行
- 当锁释放时,被阻塞的线程被唤醒
sync.Mutex 示例
go
package main
import (
"fmt"
"sync"
"time"
)
var (
counter int
mutex sync.Mutex
)
func increment(id int) {
for i := 0; i < 100; i++ {
mutex.Lock() // 锁定:阻塞当前操作系统线程
counter++
mutex.Unlock() // 解锁:唤醒等待的线程
}
fmt.Printf("协程 %d 完成\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
increment(id)
}(i)
}
wg.Wait()
fmt.Printf("最终计数: %d\n", counter) // 应该输出 300
}
读写锁 sync.RWMutex
go
var rwMutex sync.RWMutex
// 读操作可以并发
func readData() {
rwMutex.RLock() // 读锁:允许多个读操作并发
// 读取数据...
rwMutex.RUnlock()
}
// 写操作独占
func writeData() {
rwMutex.Lock() // 写锁:阻塞所有读写操作
// 写入数据...
rwMutex.Unlock()
}
管道(Channel)
Channel是Go中协程间的通信机制,可以在协程间安全地传递数据。
基本语法
go
package main
import "fmt"
func main() {
// 创建无缓冲channel
ch := make(chan int)
// 启动协程发送数据
go func() {
ch <- 42 // 发送数据到channel
}()
// 主协程接收数据
value := <-ch // 从channel接收数据
fmt.Printf("接收到的值: %d\n", value)
}
Channel类型
-
无缓冲channel :
make(chan T)- 发送和接收操作会阻塞,直到另一方准备好
- 同步通信
-
有缓冲channel :
make(chan T, capacity)- 可以存储指定数量的数据
- 发送操作在缓冲区满时阻塞
- 接收操作在缓冲区空时阻塞
go
package main
import "fmt"
func main() {
// 无缓冲channel
ch1 := make(chan string)
// 有缓冲channel,容量为2
ch2 := make(chan string, 2)
// 向有缓冲channel发送数据
ch2 <- "消息1"
ch2 <- "消息2"
fmt.Printf("缓冲区长度: %d, 容量: %d\n", len(ch2), cap(ch2))
// 接收数据
fmt.Println(<-ch2)
fmt.Println(<-ch2)
}
Channel的方向
go
package main
import "fmt"
// 只发送channel
func sender(ch chan<- string) {
ch <- "Hello from sender"
}
// 只接收channel
func receiver(ch <-chan string) {
msg := <-ch
fmt.Println("Received:", msg)
}
func main() {
ch := make(chan string)
go sender(ch)
receiver(ch)
}
for range遍历channel的注意事项
在单线程情况下使用for range遍历channel时,如果在close前启动遍历,可能会导致死锁!
go
// ❌ 死锁示例:单线程中close在for range之后
func main() {
ch := make(chan int)
// 这个for range会一直阻塞,等待数据或channel关闭
for value := range ch {
fmt.Println(value)
}
// 这行代码永远不会执行,因为上面的for range阻塞了main协程
close(ch)
}
为什么会死锁?
for range ch会一直阻塞,直到channel被关闭- 在单线程中,没有其他协程能执行
close(ch) - main协程被永远阻塞 → 死锁
正确的使用方式
方式1:在另一个协程中发送数据和关闭
go
func main() {
ch := make(chan int)
// 在协程中发送数据并关闭channel
go func() {
for i := 1; i <= 3; i++ {
ch <- i
}
close(ch) // 发送完毕后关闭
}()
// 主协程中遍历(协程安全)
for value := range ch {
fmt.Println(value)
}
}
方式2:使用select和超时避免死锁
go
func main() {
ch := make(chan int)
go func() {
time.Sleep(2 * time.Second)
ch <- 42
close(ch)
}()
// 使用select避免无限等待
for {
select {
case value, ok := <-ch:
if !ok {
fmt.Println("channel已关闭")
return
}
fmt.Println("收到:", value)
case <-time.After(5 * time.Second):
fmt.Println("超时退出")
return
}
}
}
方式3:预先知道数据量
go
func main() {
ch := make(chan int, 3) // 有缓冲channel
// 发送数据
ch <- 1
ch <- 2
ch <- 3
close(ch)
// 现在可以安全地遍历
for value := range ch {
fmt.Println(value)
}
}
for range的工作原理
go
// for range ch 等价于:
for {
value, ok := <-ch
if !ok { // channel被关闭
break
}
// 使用value
}
关键点:
for range会在channel关闭时自动退出- 在单线程中,如果没有其他协程关闭channel,就会死锁
- 解决方案:使用协程发送数据,或使用select避免无限等待
Select语句
select语句语法上类似于switch,但用于channel操作。可以同时监听多个channel。就是多路复用,C++也用epoll。
go
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
// 协程1
go func() {
time.Sleep(1 * time.Second)
ch1 <- "来自协程1的消息"
}()
// 协程2
go func() {
time.Sleep(2 * time.Second)
ch2 <- "来自协程2的消息"
}()
// 使用select监听多个channel
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println("收到ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("收到ch2:", msg2)
case <-time.After(3 * time.Second):
fmt.Println("超时")
}
}
}
常见的协程模式
1. 生产者-消费者模式
go
package main
import (
"fmt"
"time"
)
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
fmt.Printf("生产了: %d\n", i)
time.Sleep(100 * time.Millisecond)
}
close(ch) // 关闭channel
}
func consumer(ch <-chan int) {
for value := range ch { // 使用range遍历channel
fmt.Printf("消费了: %d\n", value)
time.Sleep(200 * time.Millisecond)
}
}
func main() {
ch := make(chan int, 3) // 缓冲区大小为3
go producer(ch)
go consumer(ch)
time.Sleep(3 * time.Second)
}
2. 工作池模式
go
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d 开始处理任务 %d\n", id, job)
time.Sleep(time.Second) // 模拟工作
results <- job * 2
fmt.Printf("Worker %d 完成任务 %d\n", id, job)
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
// 启动3个worker
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 发送5个任务
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
// 收集结果
for r := 1; r <= 5; r++ {
result := <-results
fmt.Printf("结果: %d\n", result)
}
}
3. 超时控制
go
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch <- "完成"
}()
select {
case result := <-ch:
fmt.Println("收到结果:", result)
case <-time.After(1 * time.Second):
fmt.Println("操作超时")
}
}