输入输出与文件操作
io 包
io.Reader 和 io.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"))
}Read 返回 EOF
当没有更多数据可读时,Read 返回 io.EOF 错误。这是正常情况,不是真正的错误。应在循环中检查 err != nil 但区分 io.EOF 与其他错误。
实现自定义 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(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>
}io.Pipe 的同步特性
io.Pipe 是同步管道——写入操作会阻塞直到读取端消费数据。适合在 goroutine 之间传递数据流。如果需要缓冲管道,考虑使用 os.Pipe 或 channel。
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)
}RemoveAll 不可恢复
os.RemoveAll 会递归删除目录及其所有内容,不可恢复。使用前务必确认路径,或在正式代码中加入安全检查。
文件权限与信息
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")) // (空)
}Setenv 的作用范围
os.Setenv 仅影响当前进程及其之后创建的子进程。不会修改系统环境变量或父进程的环境变量。
bufio 包
bufio 包实现了带缓冲的 I/O 操作。它包装了 io.Reader 和 io.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 默认行长度限制
bufio.Scanner 默认的最大 token 大小为 64KB。如果单行超过这个限制,Scan() 会返回 false,Err() 返回 bufio.ErrTooLong。可以通过 scanner.Buffer() 调整:
scanner := bufio.NewScanner(file)
scanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024) // 最大 1MB自定义 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("缓冲写入完成")
}别忘了 Flush
bufio.Writer 的数据先写入内存缓冲区,必须调用 Flush() 才能确保数据写入底层 Writer。使用 defer writer.Flush() 是好习惯。
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 包
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
})
}WalkDir 优于 Walk
Go 1.16+ 推荐使用 filepath.WalkDir 替代 filepath.Walk。WalkDir 使用 os.DirEntry 而非 os.FileInfo,不需要对每个目录条目调用 os.Lstat,性能更好。
练习题
练习 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 信号,确保进程不会在超时后继续运行。
