【C++转GO】文件操作+协程和管道

文章目录

【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个操作系统线程
锁的工作原理

当协程获取锁时:

  1. 协程尝试获取锁
  2. 如果获取失败,当前操作系统线程被阻塞
  3. Go运行时可能会将其他协程调度到其他线程继续执行
  4. 当锁释放时,被阻塞的线程被唤醒
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类型
  1. 无缓冲channelmake(chan T)

    • 发送和接收操作会阻塞,直到另一方准备好
    • 同步通信
  2. 有缓冲channelmake(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("操作超时")
    }
}
相关推荐
Halo_tjn2 小时前
Java IO流实现文件操作知识点
java·开发语言·windows·算法
历程里程碑2 小时前
滑动窗口解法:无重复字符最长子串
数据结构·c++·算法·leetcode·职场和发展·eclipse·哈希算法
FL16238631292 小时前
VTK源码编译时候选qt5路径
开发语言·qt
Felven2 小时前
C. Maximum Median
c语言·开发语言·算法
Wang's Blog3 小时前
Lua: 基于协程的生产者-消费者模型实现
开发语言·lua
星火开发设计3 小时前
广度优先搜索(BFS)详解及C++实现
数据结构·c++·算法··bfs·宽度优先·知识
jamesge20103 小时前
限流之漏桶算法
java·开发语言·算法
Dargon2883 小时前
Simulink的SIL软件在环测试
开发语言·matlab·simulink·mbd软件开发
csbysj20203 小时前
SVG 椭圆详解
开发语言