导航菜单

输入输出与文件操作

io 包

io.Reader 与 io.Writer 接口

io.Readerio.Writer 是 Go 中最核心的两个接口。io.Reader 要求实现 Read(p []byte) (n int, err error) 方法,io.Writer 要求实现 Write(p []byte) (n int, err error) 方法。它们构成了 Go I/O 系统的基础。

Reader 与 Writer 接口

package main

import (
    "fmt"
    "io"
    "os"
    "strings"
)

// io.Reader 接口
type Reader interface {
    Read(p []byte) (n int, err error)
}

// io.Writer 接口
type Writer interface {
    Write(p []byte) (n int, err error)
}

// 任何实现了 Read 方法的类型都可以作为 Reader
func main() {
    // strings.Reader — 从字符串读取
    r := strings.NewReader("Hello, Go!")
    buf := make([]byte, 5)

    // Read 每次最多读取 len(buf) 字节
    n, err := r.Read(buf)
    fmt.Printf("读取 %d 字节: %s, err: %v\n", n, buf[:n], err)
    // 读取 5 字节: Hello, err: <nil>

    n, err = r.Read(buf)
    fmt.Printf("读取 %d 字节: %s, err: %v\n", n, buf[:n], err)
    // 读取 5 字节: , Go!, err: <nil>

    n, err = r.Read(buf)
    fmt.Printf("读取 %d 字节: %s, err: %v\n", n, buf[:n], err)
    // 读取 0 字节: , err: EOF

    // os.Stdout 实现了 io.Writer
    os.Stdout.Write([]byte("写入标准输出\n"))
}

实现自定义 Reader

package main

import (
    "fmt"
    "io"
)

// UpperReader 将读取的数据转为大写
type UpperReader struct {
    src io.Reader
}

func NewUpperReader(src io.Reader) *UpperReader {
    return &UpperReader{src: src}
}

func (r *UpperReader) Read(p []byte) (int, error) {
    n, err := r.src.Read(p)
    for i := 0; i < n; i++ {
        if p[i] >= 'a' && p[i] <= 'z' {
            p[i] -= 32
        }
    }
    return n, err
}

func main() {
    r := NewUpperReader(strings.NewReader("hello world"))
    buf := make([]byte, 20)
    n, _ := r.Read(buf)
    fmt.Println(string(buf[:n]))  // HELLO WORLD
}

io.Copy

package main

import (
    "fmt"
    "io"
    "os"
    "strings"
)

func main() {
    // io.Copy — 从 Reader 拷贝到 Writer
    src := strings.NewReader("Hello from source!")
    io.Copy(os.Stdout, src)  // Hello from source!

    // 实际应用:文件拷贝
    srcFile, _ := os.Open("input.txt")
    defer srcFile.Close()

    dstFile, _ := os.Create("output.txt")
    defer dstFile.Close()

    written, err := io.Copy(dstFile, srcFile)
    fmt.Printf("拷贝了 %d 字节\n", written)
}

io.TeeReader

io.TeeReader

io.TeeReader(r, w) 返回一个 Reader,它从 r 读取数据的同时,将数据写入 w。类似于 Unix 的 tee 命令。

package main

import (
    "bytes"
    "fmt"
    "io"
    "os"
    "strings"
)

func main() {
    src := strings.NewReader("Hello, TeeReader!")

    // 同时写入 bytes.Buffer
    var buf bytes.Buffer
    tee := io.TeeReader(src, &buf)

    // 从 tee 读取会同时写入 buf
    io.Copy(os.Stdout, tee)
    fmt.Println()  // Hello, TeeReader!

    fmt.Println("Buffer 中:", buf.String())
    // Buffer 中: Hello, TeeReader!
}

io.MultiWriter

package main

import (
    "bytes"
    "fmt"
    "io"
    "os"
    "strings"
)

func main() {
    var buf1, buf2 bytes.Buffer

    // MultiWriter — 同时写入多个 Writer
    writer := io.MultiWriter(&buf1, &buf2, os.Stdout)

    io.Copy(writer, strings.NewReader("multi-destination!"))
    // multi-destination!(打印到标准输出)

    fmt.Println("\nbuf1:", buf1.String())  // multi-destination!
    fmt.Println("buf2:", buf2.String())   // multi-destination!
}

io.Pipe

package main

import (
    "fmt"
    "io"
)

func main() {
    pr, pw := io.Pipe()

    // goroutine 1 写入管道
    go func() {
        pw.Write([]byte("通过管道传输的数据"))
        pw.Close()  // 写完必须关闭,否则读取端会阻塞
    }()

    // goroutine 2 读取管道
    buf := make([]byte, 1024)
    n, err := pr.Read(buf)
    fmt.Printf("读取 %d 字节: %s, err: %v\n", n, string(buf[:n]), err)
    // 读取 28 字节: 通过管道传输的数据, err: <nil>
}

os 包

os 包

os 包提供了平台无关的操作系统功能接口,包括文件操作、环境变量、进程管理等。它是 Go 中最基础的标准库包之一。

文件操作

package main

import (
    "fmt"
    "os"
)

func main() {
    // Create — 创建文件(如果文件已存在则截断)
    f, err := os.Create("test.txt")
    if err != nil {
        panic(err)
    }
    f.WriteString("Hello, file!\n")
    f.Close()

    // Open — 以只读方式打开文件
    f2, err := os.Open("test.txt")
    if err != nil {
        panic(err)
    }
    defer f2.Close()

    buf := make([]byte, 100)
    n, _ := f2.Read(buf)
    fmt.Println(string(buf[:n]))  // Hello, file!

    // OpenFile — 更精细的控制(权限 + 模式)
    // O_RDONLY / O_WRONLY / O_RDWR / O_CREATE / O_TRUNC / O_APPEND
    f3, _ := os.OpenFile("test.txt", os.O_RDWR|os.O_APPEND, 0644)
    f3.WriteString("追加的内容\n")
    f3.Close()

    // ReadFile — 一次性读取整个文件(适合小文件)
    content, err := os.ReadFile("test.txt")
    if err != nil {
        panic(err)
    }
    fmt.Println(string(content))

    // WriteFile — 一次性写入整个文件
    err = os.WriteFile("output.txt", []byte("写入的内容"), 0644)
    if err != nil {
        panic(err)
    }

    // Remove — 删除文件
    os.Remove("output.txt")

    // Rename — 重命名/移动文件
    os.Rename("test.txt", "renamed.txt")
    os.Remove("renamed.txt")  // 清理
}

目录操作

package main

import (
    "fmt"
    "os"
)

func main() {
    // Mkdir — 创建单个目录
    os.Mkdir("testdir", 0755)

    // MkdirAll — 递归创建目录(包括所有父目录)
    os.MkdirAll("a/b/c/d", 0755)

    // ReadDir — 读取目录内容
    entries, _ := os.ReadDir("a")
    for _, entry := range entries {
        fmt.Printf("名称: %s, 是否目录: %v\n", entry.Name(), entry.IsDir())
    }

    // Remove — 删除空目录
    os.Remove("testdir")

    // RemoveAll — 递归删除(谨慎使用!)
    os.RemoveAll("a")

    // Getwd / Chdir — 获取/切换工作目录
    dir, _ := os.Getwd()
    fmt.Println("当前目录:", dir)
}

文件权限与信息

package main

import (
    "fmt"
    "os"
)

func main() {
    // Chmod — 修改文件权限
    os.WriteFile("secret.txt", []byte("secret data"), 0644)
    os.Chmod("secret.txt", 0600)  // 仅所有者可读写

    // Stat — 获取文件信息
    info, _ := os.Stat("secret.txt")
    fmt.Println("文件名:", info.Name())
    fmt.Println("大小:", info.Size(), "字节")
    fmt.Println("是否目录:", info.IsDir())
    fmt.Println("权限:", info.Mode())
    fmt.Println("修改时间:", info.ModTime())
}

环境变量

package main

import (
    "fmt"
    "os"
)

func main() {
    // Getenv — 获取单个环境变量(不存在返回空字符串)
    home := os.Getenv("HOME")
    fmt.Println("HOME:", home)

    // Setenv — 设置环境变量(仅对当前进程及其子进程生效)
    os.Setenv("MY_APP_ENV", "development")
    fmt.Println("MY_APP_ENV:", os.Getenv("MY_APP_ENV"))

    // LookupEnv — 获取环境变量并判断是否存在
    val, exists := os.LookupEnv("NONEXISTENT_VAR")
    fmt.Printf("值: %q, 是否存在: %v\n", val, exists)
    // 值: "", 是否存在: false

    // Environ — 获取所有环境变量
    envs := os.Environ()
    fmt.Printf("共有 %d 个环境变量\n", len(envs))
    for _, env := range envs[:3] {
        fmt.Println(env)
    }

    // Unsetenv — 删除环境变量
    os.Setenv("TEMP_VAR", "temp")
    os.Unsetenv("TEMP_VAR")
    fmt.Println("TEMP_VAR:", os.Getenv("TEMP_VAR"))  // (空)
}

bufio 包

bufio 包

bufio 包实现了带缓冲的 I/O 操作。它包装了 io.Readerio.Writer,通过内部缓冲区减少系统调用的次数,显著提升读写性能。

Scanner — 按行读取

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

func main() {
    // 按行读取字符串
    src := strings.NewReader(`第一行
第二行
第三行`)

    scanner := bufio.NewScanner(src)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    if err := scanner.Err(); err != nil {
        fmt.Println("读取错误:", err)
    }

    // 读取文件(最常用的模式)
    file, err := os.Open("example.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    scanner2 := bufio.NewScanner(file)
    lineNum := 0
    for scanner2.Scan() {
        lineNum++
        fmt.Printf("第 %d 行: %s\n", lineNum, scanner2.Text())
    }
}

自定义 Scanner 分割方式

package main

import (
    "bufio"
    "fmt"
    "strings"
)

func main() {
    src := strings.NewReader("word1 word2\tword3\nword4  word5")

    // 默认按行分割
    scanner := bufio.NewScanner(src)

    // 自定义按单词分割
    scanner.Split(bufio.ScanWords)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }
    // word1
    // word2
    // word3
    // word4
    // word5

    // 自定义分割函数(按逗号分割)
    src2 := strings.NewReader("a,b,c,d")
    scanner2 := bufio.NewScanner(src2)
    scanner2.Split(ScanComma)
    for scanner2.Scan() {
        fmt.Println(scanner2.Text())
    }
}

// 自定义分割函数 — 按逗号分割
func ScanComma(data []byte, atEOF bool) (advance int, token []byte, err error) {
    for i := 0; i < len(data); i++ {
        if data[i] == ',' {
            return i + 1, data[:i], nil
        }
    }
    if atEOF && len(data) > 0 {
        return len(data), data, nil
    }
    return 0, nil, nil
}

NewReader 与 NewWriter

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    // NewReader — 带缓冲的读取
    file, _ := os.Open("example.txt")
    defer file.Close()

    reader := bufio.NewReader(file)

    // ReadString — 读取到指定分隔符
    line1, _ := reader.ReadString('\n')
    fmt.Print("第一行:", line1)

    // ReadBytes — 同 ReadString,返回 []byte
    // ReadLine — 读取一行(不包含换行符,已废弃,用 Scanner)

    // Peek — 查看缓冲区中的数据但不消费
    peek, _ := reader.Peek(5)
    fmt.Printf("接下来 5 字节: %q\n", peek)

    // NewWriter — 带缓冲的写入
    outFile, _ := os.Create("buffered.txt")
    defer outFile.Close()

    writer := bufio.NewWriter(outFile)

    // WriteString — 写入字符串(写入缓冲区,不是立即写文件)
    writer.WriteString("第一行\n")
    writer.WriteString("第二行\n")
    writer.WriteString("第三行\n")

    // Flush — 将缓冲区数据刷入底层 Writer
    writer.Flush()

    fmt.Println("缓冲写入完成")
}

os/exec 包

os/exec 包

os/exec 包用于执行外部命令。它可以运行系统命令、设置环境变量、捕获命令输出、连接管道等,是 Go 程序与操作系统交互的重要桥梁。

执行命令

package main

import (
    "fmt"
    "os/exec"
    "runtime"
)

func main() {
    // Command — 创建命令
    cmd := exec.Command("echo", "Hello from Go!")

    // Run — 执行命令并等待完成(不捕获输出)
    err := cmd.Run()
    if err != nil {
        fmt.Println("执行失败:", err)
    }

    // Output — 执行命令并捕获标准输出
    out, err := exec.Command("date").Output()
    if err != nil {
        fmt.Println("执行失败:", err)
    }
    fmt.Printf("当前日期: %s", out)

    // CombinedOutput — 捕获标准输出和标准错误
    out2, err := exec.Command("ls", "-la").CombinedOutput()
    fmt.Printf("结果: %s\n", out2)

    // 根据操作系统选择命令
    var cmdName string
    if runtime.GOOS == "windows" {
        cmdName = "cmd"
    } else {
        cmdName = "ls"
    }
    out3, _ := exec.Command(cmdName).Output()
    fmt.Println(string(out3))
}

管道连接

package main

import (
    "fmt"
    "os/exec"
)

func main() {
    // 等效于 shell 中的: echo "hello" | tr a-z A-Z
    echoCmd := exec.Command("echo", "hello world")
    trCmd := exec.Command("tr", "a-z", "A-Z")

    // StdoutPipe — 获取命令的标准输出管道
    pipe, err := echoCmd.StdoutPipe()
    if err != nil {
        panic(err)
    }

    // 将 echo 的输出连接到 tr 的输入
    trCmd.Stdin = pipe

    // 启动 echo
    echoCmd.Start()

    // 捕获 tr 的输出
    result, _ := trCmd.Output()
    fmt.Println(string(result))  // HELLO WORLD
}

// 更复杂的管道链: cat file.txt | grep "pattern" | wc -l
func pipelineChain() {
    cat := exec.Command("cat", "file.txt")
    grep := exec.Command("grep", "pattern")
    wc := exec.Command("wc", "-l")

    // cat 的输出 → grep 的输入
    catPipe, _ := cat.StdoutPipe()
    grep.Stdin = catPipe

    // grep 的输出 → wc 的输入
    grepPipe, _ := grep.StdoutPipe()
    wc.Stdin = grepPipe

    cat.Start()
    grep.Start()
    output, _ := wc.Output()
    fmt.Println(string(output))
}

设置工作目录与环境变量

package main

import (
    "fmt"
    "os/exec"
)

func main() {
    cmd := exec.Command("sh", "-c", "echo $MY_VAR && pwd")

    // 设置环境变量
    cmd.Env = append(cmd.Env, "MY_VAR=hello_from_go")

    // 设置工作目录
    cmd.Dir = "/tmp"

    output, err := cmd.CombinedOutput()
    if err != nil {
        fmt.Println("错误:", err)
    }
    fmt.Println(string(output))
    // hello_from_go
    // /tmp
}

path/filepath 包

path/filepath 包

filepath 包提供了与平台无关的文件路径操作函数,自动处理不同操作系统(Windows 使用 \,Unix 使用 /)的路径分隔符差异。

路径操作

package main

import (
    "fmt"
    "path/filepath"
)

func main() {
    // Join — 拼接路径(自动处理分隔符)
    p1 := filepath.Join("dir", "subdir", "file.txt")
    fmt.Println(p1)  // dir/subdir/file.txt

    // 跨平台:在 Windows 上会使用 \
    // dir\subdir\file.txt

    // Split — 分割路径为 (目录, 文件名)
    dir, file := filepath.Split("dir/subdir/file.txt")
    fmt.Printf("目录: %q, 文件: %q\n", dir, file)
    // 目录: "dir/subdir/", 文件: "file.txt"

    // Ext — 获取文件扩展名
    fmt.Println(filepath.Ext("file.txt"))   // .txt
    fmt.Println(filepath.Ext("archive.tar.gz"))  // .gz
    fmt.Println(filepath.Ext("noext"))      // (空)

    // Dir — 获取目录部分
    fmt.Println(filepath.Dir("dir/subdir/file.txt"))  // dir/subdir
    fmt.Println(filepath.Dir("file.txt"))              // .

    // Base — 获取文件名部分(不含目录)
    fmt.Println(filepath.Base("dir/subdir/file.txt"))  // file.txt
    fmt.Println(filepath.Base("dir/subdir/"))          // subdir

    // Abs — 获取绝对路径
    abs, _ := filepath.Abs("relative/path")
    fmt.Println(abs)

    // Clean — 规范化路径
    fmt.Println(filepath.Clean("dir/../subdir/./file.txt"))  // subdir/file.txt
}

Glob 与 Walk

package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    // Glob — 匹配文件路径
    patterns, _ := filepath.Glob("*.go")
    fmt.Println(patterns)  // [main.go]

    patterns2, _ := filepath.Glob("dir/*.txt")
    fmt.Println(patterns2)

    // 递归匹配
    patterns3, _ := filepath.Glob("...")
    fmt.Println(patterns3)

    // Walk — 递归遍历目录
    filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if !info.IsDir() {
            fmt.Println(path)
        }
        return nil
    })

    // WalkDir — 更高效的遍历(Go 1.16+,不打开文件)
    filepath.WalkDir(".", func(path string, d os.DirEntry, err error) error {
        if err != nil {
            return err
        }
        if !d.IsDir() {
            info, _ := d.Info()
            fmt.Printf("%-40s %8d 字节\n", path, info.Size())
        }
        return nil
    })
}

练习题

练习 1:文件行计数器

编写一个程序,读取命令行指定的文件路径,统计文件的行数、单词数和字符数(类似 wc 命令)。

参考答案

解题思路:使用 os.Open 打开文件,bufio.Scanner 按行读取,使用 ScanWords 按单词统计。

代码

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Println("用法: go run main.go <文件路径>")
        os.Exit(1)
    }

    file, err := os.Open(os.Args[1])
    if err != nil {
        fmt.Fprintf(os.Stderr, "打开文件失败: %v\n", err)
        os.Exit(1)
    }
    defer file.Close()

    lines := 0
    words := 0
    chars := 0

    // 统计行数
    lineScanner := bufio.NewScanner(file)
    for lineScanner.Scan() {
        lines++
        chars += len(lineScanner.Text()) + 1 // +1 计入换行符
    }

    // 统计单词数
    file.Seek(0, 0) // 回到文件开头
    wordScanner := bufio.NewScanner(file)
    wordScanner.Split(bufio.ScanWords)
    for wordScanner.Scan() {
        words++
    }

    fmt.Printf("行数: %d, 单词数: %d, 字符数: %d, 文件: %s\n",
        lines, words, chars, os.Args[1])
}

关键点file.Seek(0, 0) 将文件指针重置到开头,以便用不同的 Scanner 重新遍历。

练习 2:目录文件分类器

编写一个函数 CategorizeFiles(root string) map[string][]string,递归遍历指定目录,按文件扩展名分类文件。

参考答案

解题思路:使用 filepath.WalkDir 遍历目录,filepath.Ext 获取扩展名,用 map 分类。

代码

package main

import (
    "fmt"
    "os"
    "path/filepath"
    "sort"
    "strings"
)

func CategorizeFiles(root string) map[string][]string {
    result := make(map[string][]string)

    filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
        if err != nil {
            return err // 跳过无法访问的目录
        }
        if d.IsDir() {
            return nil // 跳过目录
        }

        ext := strings.ToLower(filepath.Ext(path))
        if ext == "" {
            ext = "(无扩展名)"
        }

        // 使用相对路径
        relPath, _ := filepath.Rel(root, path)
        result[ext] = append(result[ext], relPath)
        return nil
    })

    // 对每个分类中的文件排序
    for ext := range result {
        sort.Strings(result[ext])
    }
    return result
}

func main() {
    categories := CategorizeFiles(".")

    // 获取所有扩展名并排序
    var exts []string
    for ext := range categories {
        exts = append(exts, ext)
    }
    sort.Strings(exts)

    for _, ext := range exts {
        files := categories[ext]
        fmt.Printf("【%s】(%d 个文件)\n", ext, len(files))
        for _, f := range files {
            fmt.Printf("  - %s\n", f)
        }
    }
}

关键点:使用 filepath.Rel 获取相对路径使输出更简洁,sort.Strings 保证输出顺序稳定。

练习 3:带超时的命令执行

编写一个函数 RunWithTimeout(name string, args []string, timeout time.Duration) ([]byte, error),在指定时间内执行命令,超时则终止命令并返回错误。

参考答案

解题思路:使用 exec.CommandContext 配合 context.WithTimeout,超时后自动终止命令。

代码

package main

import (
    "context"
    "fmt"
    "os/exec"
    "time"
)

func RunWithTimeout(name string, args []string, timeout time.Duration) ([]byte, error) {
    // 创建带超时的 context
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    cmd := exec.CommandContext(ctx, name, args...)
    output, err := cmd.CombinedOutput()

    if ctx.Err() == context.DeadlineExceeded {
        return output, fmt.Errorf("命令超时(%v): %s %v", timeout, name, args)
    }
    return output, err
}

func main() {
    // 正常执行(快速完成)
    out, err := RunWithTimeout("echo", []string{"hello"}, 5*time.Second)
    fmt.Printf("输出: %s错误: %v\n", out, err)

    // 模拟超时
    // sleep 10 秒,但超时设为 2 秒
    out2, err2 := RunWithTimeout("sleep", []string{"10"}, 2*time.Second)
    fmt.Printf("输出: %s错误: %v\n", out2, err2)
    // 错误: 命令超时(2s): sleep [10]
}

关键点exec.CommandContext 会在 context 取消时向进程发送 os.Kill 信号,确保进程不会在超时后继续运行。

搜索