Go 文件处理教程
📖 关于本教程本教程覆盖 Go 文件 I/O 的方方面面:读写文件、缓冲 IO、目录遍历、压缩解压、JSON 序列化、日志系统、系统命令、正则表达式等,配合大量实战代码和注释讲解。
1. 写文件及相对路径问题
1.1 写文件基础
go
package main
import (
"fmt"
"os"
)
func main() {
// ==================== 方式1:os.WriteFile(最简单)====================
// 一次性写入整个文件,适合小文件
data := []byte("Hello, Go!\n第二行内容\n")
err := os.WriteFile("output.txt", data, 0644)
if err != nil {
fmt.Println("写入失败:", err)
return
}
// ==================== 方式2:os.Create + Write ====================
// 更灵活,可以分多次写入
file, err := os.Create("output2.txt") // 创建或截断文件
if err != nil {
fmt.Println("创建失败:", err)
return
}
defer file.Close() // 务必关闭文件
file.WriteString("第一行\n")
file.WriteString("第二行\n")
file.Write([]byte("第三行(字节方式)\n"))
// ==================== 方式3:os.OpenFile(最灵活)====================
// 可以控制打开模式:追加、只写、创建等
f, err := os.OpenFile("output3.txt",
os.O_WRONLY|os.O_CREATE|os.O_APPEND, // 只写 | 不存在则创建 | 追加模式
0644,
)
if err != nil {
fmt.Println("打开失败:", err)
return
}
defer f.Close()
fmt.Fprintln(f, "追加的内容") // 用 fmt 写入文件
fmt.Fprintf(f, "格式化写入: %d\n", 42)
}常用文件打开标志:
text
标志 含义
──────────────────────────────
os.O_RDONLY 只读
os.O_WRONLY 只写
os.O_RDWR 读写
os.O_CREATE 不存在则创建
os.O_APPEND 追加模式
os.O_TRUNC 打开时清空文件
os.O_EXCL 和 O_CREATE 一起用,文件已存在则报错文件权限说明:
text
0644 = rw-r--r--
- 所有者:读+写
- 同组:读
- 其他:读
0755 = rwxr-xr-x(可执行文件/目录用)
0600 = rw-------(私密文件,如密钥)1.2 相对路径问题
❌ Go 的相对路径是相对于「运行命令时所在的目录」,不是源码文件所在目录!
text
项目结构:
myproject/
├── cmd/
│ └── server/
│ └── main.go
├── configs/
│ └── config.yaml
└── data/
└── output.txtgo
// cmd/server/main.go
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
)
func main() {
// ==================== 相对路径的坑 ====================
// 这个路径相对于你执行命令的目录,不是 main.go 所在目录!
// 如果在项目根目录运行:go run ./cmd/server
// 则 "configs/config.yaml" 相对于项目根目录 ✅
//
// 如果 cd 到 cmd/server/ 运行:go run .
// 则 "configs/config.yaml" 相对于 cmd/server/ ❌ 找不到!
// 查看当前工作目录
wd, _ := os.Getwd()
fmt.Println("当前工作目录:", wd)
// ==================== 解决方案1:基于可执行文件位置 ====================
execPath, _ := os.Executable()
execDir := filepath.Dir(execPath)
configPath := filepath.Join(execDir, "configs", "config.yaml")
fmt.Println("基于可执行文件:", configPath)
// ==================== 解决方案2:基于源码文件位置(开发期)====================
_, filename, _, _ := runtime.Caller(0)
sourceDir := filepath.Dir(filename)
configPath2 := filepath.Join(sourceDir, "..", "..", "configs", "config.yaml")
fmt.Println("基于源码文件:", configPath2)
// ==================== 解决方案3:环境变量或命令行参数(推荐)====================
configDir := os.Getenv("CONFIG_DIR")
if configDir == "" {
configDir = "configs" // 默认值
}
configPath3 := filepath.Join(configDir, "config.yaml")
fmt.Println("基于环境变量:", configPath3)
// ==================== 解决方案4:嵌入文件(Go 1.16+)====================
// 编译时直接把文件打包进二进制,彻底避免路径问题
// 见下方 embed 示例
}go
// ==================== embed:嵌入文件(终极方案)====================
package main
import (
"embed"
"fmt"
)
//go:embed configs/config.yaml
var configData []byte
//go:embed templates/*
var templateFS embed.FS
func main() {
// 配置文件已经编译进二进制,不存在路径问题
fmt.Println(string(configData))
// 读取嵌入的目录
data, _ := templateFS.ReadFile("templates/index.html")
fmt.Println(string(data))
}2. 读文件
go
package main
import (
"fmt"
"io"
"os"
)
func main() {
// ==================== 方式1:os.ReadFile(最简单)====================
// 一次性读取整个文件到内存,适合小文件
data, err := os.ReadFile("example.txt")
if err != nil {
fmt.Println("读取失败:", err)
return
}
fmt.Println(string(data))
// ==================== 方式2:os.Open + Read ====================
// 手动控制读取过程
file, err := os.Open("example.txt") // 只读打开
if err != nil {
fmt.Println("打开失败:", err)
return
}
defer file.Close()
// 固定缓冲区读取
buf := make([]byte, 1024)
for {
n, err := file.Read(buf)
if n > 0 {
fmt.Print(string(buf[:n]))
}
if err == io.EOF {
break // 读完了
}
if err != nil {
fmt.Println("读取错误:", err)
break
}
}
// ==================== 方式3:io.ReadAll ====================
// 从任意 Reader 读取全部内容
file2, _ := os.Open("example.txt")
defer file2.Close()
all, err := io.ReadAll(file2)
if err != nil {
fmt.Println("ReadAll 失败:", err)
return
}
fmt.Println(string(all))
// ==================== 读取文件信息 ====================
info, err := os.Stat("example.txt")
if err != nil {
if os.IsNotExist(err) {
fmt.Println("文件不存在")
}
return
}
fmt.Printf("文件名: %s\n", info.Name())
fmt.Printf("大小: %d 字节\n", info.Size())
fmt.Printf("权限: %s\n", info.Mode())
fmt.Printf("修改时间: %s\n", info.ModTime())
fmt.Printf("是否是目录: %v\n", info.IsDir())
}3. 用 bufio 读写文件
3.1 bufio 读文件
go
package main
import (
"bufio"
"fmt"
"io"
"os"
"strings"
)
func main() {
file, err := os.Open("example.txt")
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
// ==================== 按行读取(最常用)====================
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text() // 获取当前行(不含换行符)
fmt.Printf("%3d: %s\n", lineNum, line)
}
if err := scanner.Err(); err != nil {
fmt.Println("扫描错误:", err)
}
// ⚠️ Scanner 默认最大行长度 64KB,超长行会报错
// 解决:设置更大的缓冲区
file.Seek(0, 0) // 重置文件指针到开头
scanner2 := bufio.NewScanner(file)
scanner2.Buffer(make([]byte, 1024*1024), 1024*1024) // 1MB 缓冲区
for scanner2.Scan() {
_ = scanner2.Text()
}
// ==================== 按单词读取 ====================
file.Seek(0, 0)
scanner3 := bufio.NewScanner(file)
scanner3.Split(bufio.ScanWords) // 按单词分割(空格/换行分隔)
for scanner3.Scan() {
fmt.Printf("单词: %q\n", scanner3.Text())
}
// ==================== 按字节读取 ====================
file.Seek(0, 0)
scanner4 := bufio.NewScanner(file)
scanner4.Split(bufio.ScanBytes)
for scanner4.Scan() {
fmt.Printf("%x ", scanner4.Bytes()[0])
}
fmt.Println()
// ==================== 按 rune 读取(支持中文)====================
file.Seek(0, 0)
scanner5 := bufio.NewScanner(file)
scanner5.Split(bufio.ScanRunes)
for scanner5.Scan() {
fmt.Printf("%s", scanner5.Text())
}
// ==================== 自定义分隔符 ====================
// 按 "---" 分割
input := "part1---part2---part3"
scanner6 := bufio.NewScanner(strings.NewReader(input))
scanner6.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := strings.Index(string(data), "---"); i >= 0 {
return i + 3, data[:i], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil // 需要更多数据
})
for scanner6.Scan() {
fmt.Printf("段落: %q\n", scanner6.Text())
}
// 段落: "part1"
// 段落: "part2"
// 段落: "part3"
// ==================== bufio.Reader 逐行读取(替代方案)====================
file.Seek(0, 0)
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n') // 读到换行符为止
if len(line) > 0 {
line = strings.TrimRight(line, "\r\n")
fmt.Println("行:", line)
}
if err == io.EOF {
break
}
if err != nil {
fmt.Println("错误:", err)
break
}
}
}3.2 bufio 写文件
go
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.Create("buffered_output.txt")
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
// 创建带缓冲的 Writer
// 默认缓冲区 4096 字节,数据先写入缓冲区,满了才刷到磁盘
writer := bufio.NewWriter(file)
// 也可以指定缓冲区大小
// writer := bufio.NewWriterSize(file, 8192) // 8KB 缓冲
// 写入数据(先到缓冲区,还没落盘)
writer.WriteString("第一行\n")
writer.WriteString("第二行\n")
fmt.Println("缓冲区中未刷新的字节:", writer.Buffered()) // > 0
fmt.Println("缓冲区剩余可用空间:", writer.Available())
// 更多写入
for i := 0; i < 100; i++ {
fmt.Fprintf(writer, "行 %d: 这是一些数据内容\n", i)
}
// ⚠️ 必须调用 Flush,否则缓冲区中的数据会丢失!
err = writer.Flush()
if err != nil {
fmt.Println("Flush 失败:", err)
}
fmt.Println("写入完成")
}⚠️ bufio.Writer 的关键注意事项 Flush() 必须调用!如果忘记调用,缓冲区中的数据不会写入文件。最佳实践是用 defer:先 defer file.Close(),再 defer writer.Flush()(defer 是后进先出,Flush 会在 Close 之前执行)。
4. 练习:自行实现带缓冲的 FileWriter
💡 要求实现一个 BufferedFileWriter,内部维护一个字节缓冲区,写入时先存缓冲区,缓冲区满了自动刷新到文件。支持 Write、WriteString、Flush、Close 方法。
go
package main
import (
"fmt"
"os"
)
// BufferedFileWriter 带缓冲的文件写入器
type BufferedFileWriter struct {
file *os.File
buf []byte // 缓冲区
size int // 缓冲区容量
offset int // 当前已写入缓冲区的字节数
}
// NewBufferedFileWriter 创建带缓冲的文件写入器
// path: 文件路径
// bufSize: 缓冲区大小(字节)
func NewBufferedFileWriter(path string, bufSize int) (*BufferedFileWriter, error) {
if bufSize <= 0 {
bufSize = 4096 // 默认 4KB
}
file, err := os.Create(path)
if err != nil {
return nil, fmt.Errorf("create file: %w", err)
}
return &BufferedFileWriter{
file: file,
buf: make([]byte, bufSize),
size: bufSize,
offset: 0,
}, nil
}
// Write 实现 io.Writer 接口
func (w *BufferedFileWriter) Write(p []byte) (int, error) {
totalWritten := 0
remaining := p
for len(remaining) > 0 {
// 计算缓冲区剩余空间
space := w.size - w.offset
if len(remaining) < space {
// 剩余数据能全部放进缓冲区
copy(w.buf[w.offset:], remaining)
w.offset += len(remaining)
totalWritten += len(remaining)
remaining = nil
} else {
// 先填满缓冲区
copy(w.buf[w.offset:], remaining[:space])
w.offset = w.size
totalWritten += space
remaining = remaining[space:]
// 缓冲区满了,刷新到文件
if err := w.Flush(); err != nil {
return totalWritten, err
}
}
}
return totalWritten, nil
}
// WriteString 写入字符串
func (w *BufferedFileWriter) WriteString(s string) (int, error) {
return w.Write([]byte(s))
}
// Flush 将缓冲区内容刷新到文件
func (w *BufferedFileWriter) Flush() error {
if w.offset == 0 {
return nil // 缓冲区为空,无需刷新
}
// 将缓冲区数据写入文件
_, err := w.file.Write(w.buf[:w.offset])
if err != nil {
return fmt.Errorf("flush to file: %w", err)
}
w.offset = 0 // 重置偏移量
return nil
}
// Buffered 返回缓冲区中未刷新的字节数
func (w *BufferedFileWriter) Buffered() int {
return w.offset
}
// Available 返回缓冲区剩余可用空间
func (w *BufferedFileWriter) Available() int {
return w.size - w.offset
}
// Close 刷新缓冲区并关闭文件
func (w *BufferedFileWriter) Close() error {
// 先刷新剩余数据
if err := w.Flush(); err != nil {
w.file.Close() // 即使 Flush 失败也要关闭文件
return err
}
return w.file.Close()
}
func main() {
// 创建带缓冲的写入器(缓冲区 64 字节,方便观察效果)
writer, err := NewBufferedFileWriter("custom_buffered.txt", 64)
if err != nil {
fmt.Println("创建失败:", err)
return
}
defer writer.Close()
// 多次小写入(都在缓冲区中)
for i := 0; i < 10; i++ {
n, err := writer.WriteString(fmt.Sprintf("行 %02d: Hello, Buffered Writer!\n", i))
if err != nil {
fmt.Println("写入错误:", err)
return
}
fmt.Printf("写入 %d 字节, 缓冲: %d, 可用: %d\n",
n, writer.Buffered(), writer.Available())
}
// Close 会自动 Flush
fmt.Println("写入完成")
// 验证:读回来看看
data, _ := os.ReadFile("custom_buffered.txt")
fmt.Printf("\n文件内容(%d 字节):\n%s", len(data), string(data))
}5. 文件管理和目录遍历
5.1 文件和目录操作
go
package main
import (
"fmt"
"os"
"path/filepath"
)
func main() {
// ==================== 创建目录 ====================
os.Mkdir("testdir", 0755) // 创建单层目录
os.MkdirAll("a/b/c/d", 0755) // 递归创建多层目录
// ==================== 创建临时文件/目录 ====================
tmpFile, _ := os.CreateTemp("", "prefix-*.txt") // 系统临时目录
fmt.Println("临时文件:", tmpFile.Name())
tmpFile.Close()
defer os.Remove(tmpFile.Name())
tmpDir, _ := os.MkdirTemp("", "myapp-")
fmt.Println("临时目录:", tmpDir)
defer os.RemoveAll(tmpDir)
// ==================== 重命名 / 移动 ====================
os.WriteFile("old.txt", []byte("hello"), 0644)
os.Rename("old.txt", "new.txt") // 重命名
os.Rename("new.txt", "testdir/new.txt") // 移动
// ==================== 复制文件(Go 没有内置 Copy)====================
copyFile("testdir/new.txt", "testdir/copy.txt")
// ==================== 删除 ====================
os.Remove("testdir/copy.txt") // 删除单个文件
os.RemoveAll("a") // 递归删除目录及内容
// ==================== 路径操作 ====================
path := "/home/user/docs/report.tar.gz"
fmt.Println("Dir:", filepath.Dir(path)) // /home/user/docs
fmt.Println("Base:", filepath.Base(path)) // report.tar.gz
fmt.Println("Ext:", filepath.Ext(path)) // .gz
fmt.Println("Clean:", filepath.Clean("a/../b/./c")) // b/c
// 拼接路径(自动处理分隔符)
joined := filepath.Join("home", "user", "docs", "file.txt")
fmt.Println("Join:", joined) // home/user/docs/file.txt
// 绝对路径
abs, _ := filepath.Abs("testdir")
fmt.Println("Abs:", abs)
// 相对路径
rel, _ := filepath.Rel("/home/user", "/home/user/docs/file.txt")
fmt.Println("Rel:", rel) // docs/file.txt
// 匹配模式
match, _ := filepath.Match("*.go", "main.go")
fmt.Println("Match:", match) // true
}
// 复制文件的实现
func copyFile(src, dst string) error {
data, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dst, data, 0644)
}5.2 目录遍历
go
package main
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)
func main() {
root := "."
// ==================== 方式1:filepath.Walk(经典方式)====================
fmt.Println("=== filepath.Walk ===")
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 跳过隐藏目录
if info.IsDir() && strings.HasPrefix(info.Name(), ".") && path != "." {
return filepath.SkipDir
}
indent := strings.Repeat(" ", strings.Count(path, string(os.PathSeparator)))
if info.IsDir() {
fmt.Printf("%s📁 %s/\n", indent, info.Name())
} else {
fmt.Printf("%s📄 %s (%d bytes)\n", indent, info.Name(), info.Size())
}
return nil
})
// ==================== 方式2:filepath.WalkDir(Go 1.16+,更快)====================
// WalkDir 比 Walk 快,因为它不需要在每个文件上调用 Stat
fmt.Println("\n=== filepath.WalkDir ===")
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
fmt.Printf("[DIR] %s\n", path)
} else {
fmt.Printf("[FILE] %s\n", path)
}
return nil
})
// ==================== 方式3:os.ReadDir(只读一层)====================
fmt.Println("\n=== os.ReadDir ===")
entries, _ := os.ReadDir(root)
for _, entry := range entries {
info, _ := entry.Info()
if entry.IsDir() {
fmt.Printf("📁 %-20s\n", entry.Name())
} else {
fmt.Printf("📄 %-20s %d bytes\n", entry.Name(), info.Size())
}
}
// ==================== 方式4:filepath.Glob(模式匹配)====================
fmt.Println("\n=== Glob ===")
goFiles, _ := filepath.Glob("*.go")
fmt.Println("Go 文件:", goFiles)
allGoFiles, _ := filepath.Glob("**/*.go") // ⚠️ ** 在 Glob 中不递归!
fmt.Println("Go 文件:", allGoFiles)
// ==================== 实用:递归查找特定文件 ====================
fmt.Println("\n=== 查找所有 .go 文件 ===")
var goFileList []string
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// 跳过 vendor 和 .git
if d.IsDir() && (d.Name() == "vendor" || d.Name() == ".git") {
return filepath.SkipDir
}
if !d.IsDir() && filepath.Ext(path) == ".go" {
goFileList = append(goFileList, path)
}
return nil
})
for _, f := range goFileList {
fmt.Println(" ", f)
}
}6. 文件练习:图像的分割与合并
💡 题目将一个大文件(如图片)按指定大小分割成多个小文件,然后再将这些小文件合并还原成原始文件。这是断点续传、分片上传等功能的基础。
go
package main
import (
"crypto/md5"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
)
// SplitFile 将文件分割成多个小块
// chunkSize 单位为字节
func SplitFile(srcPath string, chunkSize int64) ([]string, error) {
// 打开源文件
src, err := os.Open(srcPath)
if err != nil {
return nil, fmt.Errorf("open source: %w", err)
}
defer src.Close()
// 获取文件信息
info, err := src.Stat()
if err != nil {
return nil, fmt.Errorf("stat: %w", err)
}
// 创建输出目录
outDir := srcPath + "_parts"
os.MkdirAll(outDir, 0755)
// 计算分块数
totalChunks := (info.Size() + chunkSize - 1) / chunkSize
fmt.Printf("文件大小: %d 字节, 块大小: %d, 总块数: %d\n",
info.Size(), chunkSize, totalChunks)
var parts []string
buf := make([]byte, chunkSize)
for i := int64(0); i < totalChunks; i++ {
// 读取一块
n, err := src.Read(buf)
if err != nil && err != io.EOF {
return nil, fmt.Errorf("read chunk %d: %w", i, err)
}
if n == 0 {
break
}
// 写入分块文件
partName := filepath.Join(outDir, fmt.Sprintf("part_%04d", i))
if err := os.WriteFile(partName, buf[:n], 0644); err != nil {
return nil, fmt.Errorf("write chunk %d: %w", i, err)
}
parts = append(parts, partName)
fmt.Printf(" 写入分块 %d: %d 字节\n", i, n)
}
return parts, nil
}
// MergeFiles 将多个分块文件合并为一个文件
func MergeFiles(parts []string, dstPath string) error {
// 按文件名排序,确保顺序正确
sort.Strings(parts)
// 创建目标文件
dst, err := os.Create(dstPath)
if err != nil {
return fmt.Errorf("create dest: %w", err)
}
defer dst.Close()
// 逐块写入
for i, partPath := range parts {
part, err := os.Open(partPath)
if err != nil {
return fmt.Errorf("open part %d: %w", i, err)
}
n, err := io.Copy(dst, part) // 高效拷贝,不会全部读入内存
part.Close()
if err != nil {
return fmt.Errorf("copy part %d: %w", i, err)
}
fmt.Printf(" 合并分块 %d: %d 字节\n", i, n)
}
return nil
}
// fileMD5 计算文件的 MD5 哈希(用于验证完整性)
func fileMD5(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := md5.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
func main() {
// 创建一个测试文件(模拟图片)
testFile := "test_image.bin"
createTestFile(testFile, 1024*1024) // 1MB 测试文件
// 计算原始文件 MD5
origMD5, _ := fileMD5(testFile)
fmt.Printf("原始文件 MD5: %s\n\n", origMD5)
// 分割:每块 256KB
fmt.Println("=== 分割文件 ===")
parts, err := SplitFile(testFile, 256*1024)
if err != nil {
fmt.Println("分割失败:", err)
return
}
fmt.Printf("分割完成,共 %d 个分块\n\n", len(parts))
// 合并
fmt.Println("=== 合并文件 ===")
mergedFile := "merged_image.bin"
if err := MergeFiles(parts, mergedFile); err != nil {
fmt.Println("合并失败:", err)
return
}
// 验证
mergedMD5, _ := fileMD5(mergedFile)
fmt.Printf("\n合并文件 MD5: %s\n", mergedMD5)
if origMD5 == mergedMD5 {
fmt.Println("✅ 校验通过:文件完全一致")
} else {
fmt.Println("❌ 校验失败:文件不一致")
}
// 清理
os.Remove(testFile)
os.Remove(mergedFile)
os.RemoveAll(testFile + "_parts")
}
// 创建测试文件
func createTestFile(path string, size int) {
f, _ := os.Create(path)
defer f.Close()
// 写入可辨识的模式数据
for i := 0; i < size; i++ {
f.Write([]byte{byte(i % 256)})
}
}
// 实用函数:在目录中查找所有分块文件
func findParts(dir string) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var parts []string
for _, e := range entries {
if !e.IsDir() && strings.HasPrefix(e.Name(), "part_") {
parts = append(parts, filepath.Join(dir, e.Name()))
}
}
sort.Strings(parts)
return parts, nil
}7. 高级 IO 功能
go
package main
import (
"bytes"
"fmt"
"io"
"os"
"strings"
)
func main() {
// ==================== io.Copy:高效拷贝 ====================
// 内部使用 32KB 缓冲区,不会把整个文件读入内存
src, _ := os.Open("source.txt")
dst, _ := os.Create("dest.txt")
n, err := io.Copy(dst, src) // dst 是 Writer,src 是 Reader
fmt.Printf("拷贝了 %d 字节, err: %v\n", n, err)
src.Close()
dst.Close()
// ==================== io.CopyN:只拷贝 N 个字节 ====================
src2, _ := os.Open("source.txt")
dst2, _ := os.Create("first_100.txt")
io.CopyN(dst2, src2, 100) // 只拷贝前 100 字节
src2.Close()
dst2.Close()
// ==================== io.TeeReader:读取时同时写到另一个地方 ====================
// 类似 Linux 的 tee 命令
original := strings.NewReader("Hello, TeeReader!")
var buf bytes.Buffer
tee := io.TeeReader(original, &buf) // 读 tee 时,数据同时写入 buf
data, _ := io.ReadAll(tee)
fmt.Println("读到:", string(data)) // Hello, TeeReader!
fmt.Println("缓冲:", buf.String()) // Hello, TeeReader!(同时有一份)
// ==================== io.MultiReader:串联多个 Reader ====================
r1 := strings.NewReader("Part 1 ")
r2 := strings.NewReader("Part 2 ")
r3 := strings.NewReader("Part 3")
multi := io.MultiReader(r1, r2, r3) // 依次读取三个 Reader
all, _ := io.ReadAll(multi)
fmt.Println("Multi:", string(all)) // Part 1 Part 2 Part 3
// ==================== io.MultiWriter:同时写到多个地方 ====================
var buf1, buf2 bytes.Buffer
multiW := io.MultiWriter(&buf1, &buf2, os.Stdout)
fmt.Fprintln(multiW, "写到三个地方") // stdout + buf1 + buf2 都有
// ==================== io.LimitReader:限制读取量 ====================
bigReader := strings.NewReader("abcdefghijklmnopqrstuvwxyz")
limited := io.LimitReader(bigReader, 5)
result, _ := io.ReadAll(limited)
fmt.Println("Limited:", string(result)) // abcde
// ==================== io.SectionReader:读取文件的某一段 ====================
file, _ := os.Open("source.txt")
defer file.Close()
section := io.NewSectionReader(file, 10, 20) // 从偏移 10 开始,读 20 字节
sectionData, _ := io.ReadAll(section)
fmt.Println("Section:", string(sectionData))
// ==================== io.Pipe:内存管道 ====================
// 一个 goroutine 写,另一个 goroutine 读,同步通信
pr, pw := io.Pipe()
go func() {
defer pw.Close()
pw.Write([]byte("通过管道传输的数据"))
}()
pipeData, _ := io.ReadAll(pr)
fmt.Println("Pipe:", string(pipeData))
// ==================== bytes.Buffer:内存中的 Reader/Writer ====================
var b bytes.Buffer
b.WriteString("hello ")
b.WriteString("world")
fmt.Println("Buffer:", b.String()) // hello world
fmt.Println("Len:", b.Len()) // 11
// Buffer 同时实现了 io.Reader 和 io.Writer
readBack := make([]byte, 5)
b.Read(readBack)
fmt.Println("Read:", string(readBack)) // hello
}8. 文件的压缩和解压,Reader 和 Writer 接口
8.1 Reader 和 Writer 接口
go
// Go 的 I/O 设计哲学:一切皆是 Reader/Writer 流
//
// type Reader interface {
// Read(p []byte) (n int, err error)
// }
//
// type Writer interface {
// Write(p []byte) (n int, err error)
// }
//
// 实现了这两个接口的类型可以无缝组合:
// 文件、网络连接、压缩流、加密流、缓冲流...
// 它们像乐高积木一样拼接在一起
//
// 例如写压缩文件的流水线:
// 数据 → gzip.Writer → bufio.Writer → os.File
//
// 读压缩文件的流水线:
// os.File → gzip.Reader → bufio.Reader → 数据8.2 gzip 压缩和解压
go
package main
import (
"compress/gzip"
"fmt"
"io"
"os"
"strings"
)
// 压缩文件
func gzipCompress(srcPath, dstPath string) error {
// 打开源文件
src, err := os.Open(srcPath)
if err != nil {
return err
}
defer src.Close()
// 创建目标文件
dst, err := os.Create(dstPath)
if err != nil {
return err
}
defer dst.Close()
// 创建 gzip Writer(写入 dst 的数据会被自动压缩)
gw, err := gzip.NewWriterLevel(dst, gzip.BestCompression)
if err != nil {
return err
}
defer gw.Close()
// 设置 gzip header(可选)
gw.Name = srcPath
gw.Comment = "compressed by Go"
// 将源文件内容写入 gzip Writer
// io.Copy 内部循环 Read→Write,不会一次性读入内存
_, err = io.Copy(gw, src)
return err
}
// 解压文件
func gzipDecompress(srcPath, dstPath string) error {
// 打开压缩文件
src, err := os.Open(srcPath)
if err != nil {
return err
}
defer src.Close()
// 创建 gzip Reader(从 src 读出的数据会被自动解压)
gr, err := gzip.NewReader(src)
if err != nil {
return err
}
defer gr.Close()
fmt.Printf("原始文件名: %s\n", gr.Name)
fmt.Printf("注释: %s\n", gr.Comment)
// 创建目标文件
dst, err := os.Create(dstPath)
if err != nil {
return err
}
defer dst.Close()
// 将解压后的数据写入目标文件
_, err = io.Copy(dst, gr)
return err
}
func main() {
// 创建测试文件
testData := []byte("Hello, Gzip! " + strings.Repeat("Go is awesome! ", 1000))
os.WriteFile("test.txt", testData, 0644)
// 压缩
err := gzipCompress("test.txt", "test.txt.gz")
if err != nil {
fmt.Println("压缩失败:", err)
return
}
// 比较大小
origInfo, _ := os.Stat("test.txt")
compInfo, _ := os.Stat("test.txt.gz")
fmt.Printf("原始: %d 字节\n", origInfo.Size())
fmt.Printf("压缩: %d 字节\n", compInfo.Size())
fmt.Printf("压缩率: %.1f%%\n",
(1-float64(compInfo.Size())/float64(origInfo.Size()))*100)
// 解压
err = gzipDecompress("test.txt.gz", "test_restored.txt")
if err != nil {
fmt.Println("解压失败:", err)
return
}
fmt.Println("✅ 压缩解压完成")
}8.3 tar.gz 打包和解包
go
package main
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
)
// 创建 tar.gz 压缩包
func createTarGz(srcDir, dstPath string) error {
dst, err := os.Create(dstPath)
if err != nil {
return err
}
defer dst.Close()
// 流水线:tar.Writer → gzip.Writer → os.File
gw := gzip.NewWriter(dst)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
// 遍历目录,逐个添加文件
return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 创建 tar header
header, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
// 使用相对路径
header.Name, _ = filepath.Rel(srcDir, path)
// 写入 header
if err := tw.WriteHeader(header); err != nil {
return err
}
// 如果是文件(非目录),写入内容
if !info.IsDir() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(tw, file)
return err
}
return nil
})
}
// 解压 tar.gz
func extractTarGz(srcPath, dstDir string) error {
src, err := os.Open(srcPath)
if err != nil {
return err
}
defer src.Close()
// 流水线:os.File → gzip.Reader → tar.Reader
gr, err := gzip.NewReader(src)
if err != nil {
return err
}
defer gr.Close()
tr := tar.NewReader(gr)
// 逐个读取 tar 中的文件
for {
header, err := tr.Next()
if err == io.EOF {
break // 读完了
}
if err != nil {
return err
}
targetPath := filepath.Join(dstDir, header.Name)
fmt.Printf(" 解压: %s\n", targetPath)
switch header.Typeflag {
case tar.TypeDir:
os.MkdirAll(targetPath, 0755)
case tar.TypeReg:
os.MkdirAll(filepath.Dir(targetPath), 0755)
outFile, err := os.Create(targetPath)
if err != nil {
return err
}
io.Copy(outFile, tr)
outFile.Close()
}
}
return nil
}
func main() {
// 创建 tar.gz
err := createTarGz("./testdir", "archive.tar.gz")
fmt.Println("打包:", err)
// 解压 tar.gz
err = extractTarGz("archive.tar.gz", "./extracted")
fmt.Println("解压:", err)
}9. JSON 序列化的若干问题
9.1 基本序列化/反序列化
go
package main
import (
"encoding/json"
"fmt"
"time"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"` // 空值时省略
Password string `json:"-"` // 永远不序列化
CreatedAt time.Time `json:"created_at"`
}
func main() {
// ==================== 序列化(struct → JSON)====================
user := User{
Name: "Alice",
Age: 25,
Email: "alice@example.com",
Password: "secret123",
CreatedAt: time.Now(),
}
// 紧凑 JSON
data, _ := json.Marshal(user)
fmt.Println(string(data))
// {"name":"Alice","age":25,"email":"alice@example.com","created_at":"2024-01-01T12:00:00Z"}
// 注意:Password 没有出现(因为 json:"-")
// 格式化 JSON
pretty, _ := json.MarshalIndent(user, "", " ")
fmt.Println(string(pretty))
// ==================== 反序列化(JSON → struct)====================
jsonStr := `{"name":"Bob","age":30,"email":"bob@test.com"}`
var user2 User
err := json.Unmarshal([]byte(jsonStr), &user2)
if err != nil {
fmt.Println("解析失败:", err)
return
}
fmt.Printf("%+v\n", user2)
}9.2 常见问题
go
package main
import (
"encoding/json"
"fmt"
"strings"
"time"
)
func main() {
// ==================== 问题1:数字精度丢失 ====================
// JSON 中数字默认解析为 float64,大整数会丢失精度
jsonStr := `{"id": 1234567890123456789}`
// ❌ 精度丢失
var m map[string]interface{}
json.Unmarshal([]byte(jsonStr), &m)
fmt.Printf("float64: %.0f\n", m["id"].(float64))
// 1234567890123456768(最后几位不对!)
// ✅ 用 json.Number 保持精度
decoder := json.NewDecoder(strings.NewReader(jsonStr))
decoder.UseNumber()
var m2 map[string]interface{}
decoder.Decode(&m2)
num := m2["id"].(json.Number)
fmt.Println("json.Number:", num.String()) // 精确值
intVal, _ := num.Int64()
fmt.Println("Int64:", intVal)
// ✅ 或者直接定义为 string tag
type Order struct {
ID int64 `json:"id,string"` // 前端传字符串 "123",自动转 int64
}
// ==================== 问题2:嵌套结构体的 omitempty ====================
type Address struct {
City string `json:"city"`
}
type Person struct {
Name string `json:"name"`
Address *Address `json:"address,omitempty"` // 指针类型才能被 omitempty 省略
}
p1 := Person{Name: "Alice"}
data1, _ := json.Marshal(p1)
fmt.Println(string(data1)) // {"name":"Alice"}(address 被省略)
p2 := Person{Name: "Bob", Address: &Address{City: "Beijing"}}
data2, _ := json.Marshal(p2)
fmt.Println(string(data2)) // {"name":"Bob","address":{"city":"Beijing"}}
// ⚠️ 注意:非指针结构体的 omitempty 不会省略空结构体
type Person2 struct {
Name string `json:"name"`
Address Address `json:"address,omitempty"` // 值类型
}
p3 := Person2{Name: "Charlie"}
data3, _ := json.Marshal(p3)
fmt.Println(string(data3)) // {"name":"Charlie","address":{"city":""}}
// Address 没被省略!因为空结构体不是零值 nil
// 问题3 见下方独立代码块
// ==================== 问题4:未知字段处理 ====================
type Config struct {
Name string `json:"name"`
// 其他未知字段会被忽略(默认行为)
}
// 如果要禁止未知字段:
decoder2 := json.NewDecoder(strings.NewReader(`{"name":"app","unknown":123}`))
decoder2.DisallowUnknownFields()
var cfg Config
err := decoder2.Decode(&cfg)
fmt.Println("未知字段:", err) // json: unknown field "unknown"
// ==================== 问题5:解析到 map(动态结构)====================
dynamicJSON := `{"name":"Alice","scores":[90,85,92],"meta":{"role":"admin"}}`
var result map[string]interface{}
json.Unmarshal([]byte(dynamicJSON), &result)
// 类型断言访问嵌套数据
name := result["name"].(string)
scores := result["scores"].([]interface{})
meta := result["meta"].(map[string]interface{})
fmt.Println(name, scores, meta["role"])
}9.3 自定义 JSON 序列化(问题3)
go
package main
import (
"encoding/json"
"fmt"
"time"
)
// 自定义时间戳类型
type Timestamp struct {
time.Time
}
// 实现 json.Marshaler 接口:序列化为 Unix 时间戳
func (t Timestamp) MarshalJSON() ([]byte, error) {
return json.Marshal(t.Unix())
}
// 实现 json.Unmarshaler 接口:从 Unix 时间戳反序列化
func (t *Timestamp) UnmarshalJSON(data []byte) error {
var stamp int64
if err := json.Unmarshal(data, &stamp); err != nil {
return err
}
t.Time = time.Unix(stamp, 0)
return nil
}
type Event struct {
Name string `json:"name"`
At Timestamp `json:"at"`
}
func main() {
event := Event{
Name: "Go 发布会",
At: Timestamp{time.Now()},
}
data, _ := json.Marshal(event)
fmt.Println(string(data))
// {"name":"Go 发布会","at":1704067200}
var event2 Event
json.Unmarshal(data, &event2)
fmt.Println(event2.At.Format("2006-01-02 15:04:05"))
}9.4 流式 JSON(Encoder/Decoder)
go
package main
import (
"encoding/json"
"fmt"
"os"
"strings"
)
type LogEntry struct {
Level string `json:"level"`
Message string `json:"message"`
}
func main() {
// ==================== 写 JSON 流(每行一个 JSON 对象)====================
file, _ := os.Create("logs.jsonl")
encoder := json.NewEncoder(file)
logs := []LogEntry{
{"INFO", "server started"},
{"WARN", "high memory usage"},
{"ERROR", "connection timeout"},
}
for _, log := range logs {
encoder.Encode(log) // 每次 Encode 自动追加换行符
}
file.Close()
// ==================== 读 JSON 流 ====================
file2, _ := os.Open("logs.jsonl")
defer file2.Close()
decoder := json.NewDecoder(file2)
for decoder.More() { // 还有数据吗?
var entry LogEntry
if err := decoder.Decode(&entry); err != nil {
break
}
fmt.Printf("[%s] %s\n", entry.Level, entry.Message)
}
// ==================== 从字符串读写 ====================
var buf strings.Builder
enc := json.NewEncoder(&buf)
enc.SetIndent("", " ") // 格式化输出
enc.Encode(map[string]int{"a": 1, "b": 2})
fmt.Println(buf.String())
}10. log 和 slog
10.1 标准 log 包
go
package main
import (
"log"
"os"
)
func main() {
// ==================== 基本用法 ====================
log.Println("普通日志") // 2024/01/01 12:00:00 普通日志
log.Printf("格式化: %s=%d\n", "x", 42)
// log.Fatal:打印日志后调用 os.Exit(1)
// log.Fatal("致命错误") // 会退出程序!
// log.Panic:打印日志后调用 panic
// log.Panic("崩溃") // 会 panic!
// ==================== 配置日志格式 ====================
// 设置前缀
log.SetPrefix("[MyApp] ")
// 设置输出格式
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
// 常用 flag:
// log.Ldate 日期: 2024/01/01
// log.Ltime 时间: 12:00:00
// log.Lmicroseconds 微秒: 12:00:00.123456
// log.Llongfile 完整路径: /home/user/main.go:10
// log.Lshortfile 短路径: main.go:10
// log.LUTC 使用 UTC 时间
// log.Lmsgprefix 前缀放在消息前而非时间前
log.Println("带格式的日志")
// [MyApp] 2024/01/01 12:00:00 main.go:25: 带格式的日志
// ==================== 输出到文件 ====================
file, err := os.OpenFile("app.log",
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Fatal(err)
}
defer file.Close()
log.SetOutput(file) // 日志写入文件而非 stderr
log.Println("这条日志写入文件")
// ==================== 自定义 Logger ====================
// 创建独立的 Logger 实例(不影响全局 log)
errorLog := log.New(os.Stderr, "[ERROR] ",
log.Ldate|log.Ltime|log.Lshortfile)
infoLog := log.New(os.Stdout, "[INFO] ",
log.Ldate|log.Ltime)
errorLog.Println("这是错误日志")
infoLog.Println("这是信息日志")
// 同时输出到文件和终端
// multiWriter := io.MultiWriter(os.Stdout, file)
// log.SetOutput(multiWriter)
}10.2 slog 结构化日志(Go 1.21+)
go
package main
import (
"context"
"log/slog"
"os"
)
func main() {
// ==================== 基本用法 ====================
slog.Info("服务启动",
"port", 8080,
"env", "production",
)
// 输出: 2024/01/01 12:00:00 INFO 服务启动 port=8080 env=production
slog.Warn("内存使用率高", "usage", 85.5)
slog.Error("连接失败", "err", "timeout", "host", "db.example.com")
// ==================== JSON 格式输出 ====================
jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug, // 设置最低日志级别
})
jsonLogger := slog.New(jsonHandler)
jsonLogger.Info("用户登录",
"user_id", 12345,
"ip", "192.168.1.1",
)
// {"time":"2024-01-01T12:00:00Z","level":"INFO","msg":"用户登录","user_id":12345,"ip":"192.168.1.1"}
// ==================== 文本格式输出 ====================
textHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
textLogger := slog.New(textHandler)
textLogger.Info("请求处理", "method", "GET", "path", "/api/users", "latency", "23ms")
// ==================== 设置为默认 Logger ====================
slog.SetDefault(jsonLogger)
slog.Info("这条用 JSON 格式") // 全局生效
// ==================== 结构化字段(类型安全)====================
slog.Info("订单创建",
slog.String("order_id", "ORD-001"),
slog.Int("amount", 9900),
slog.Bool("paid", false),
slog.Group("customer",
slog.String("name", "Alice"),
slog.String("email", "alice@test.com"),
),
)
// ==================== 带固定字段的子 Logger ====================
// 适用于请求级别的日志:每条日志自动带上 request_id
reqLogger := jsonLogger.With(
"request_id", "req-abc-123",
"user_id", 42,
)
reqLogger.Info("处理开始")
reqLogger.Info("查询数据库")
reqLogger.Error("处理失败", "err", "not found")
// 每条日志都自动带上 request_id 和 user_id
// ==================== 带 Context ====================
ctx := context.Background()
slog.InfoContext(ctx, "带上下文的日志", "key", "value")
// ==================== 输出到文件 ====================
file, _ := os.OpenFile("structured.log",
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
defer file.Close()
fileLogger := slog.New(slog.NewJSONHandler(file, nil))
fileLogger.Info("写入文件", "action", "save")
}11. 标准输入和标准输出
go
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
// ==================== fmt.Scan 系列(简单输入)====================
fmt.Print("请输入姓名: ")
var name string
fmt.Scan(&name) // 读到空格或换行停止
fmt.Println("你好,", name)
fmt.Print("请输入年龄和城市(空格分隔): ")
var age int
var city string
fmt.Scan(&age, &city) // 空格分隔的多个值
fmt.Printf("年龄: %d, 城市: %s\n", age, city)
// Scanf:格式化读取
fmt.Print("请输入日期 (yyyy-mm-dd): ")
var year, month, day int
fmt.Scanf("%d-%d-%d", &year, &month, &day)
fmt.Printf("日期: %d年%d月%d日\n", year, month, day)
// ==================== bufio.Scanner(读取整行,推荐)====================
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("请输入一整行内容: ")
if scanner.Scan() {
line := scanner.Text()
fmt.Println("你输入了:", line)
fmt.Println("长度:", len(line))
}
// ==================== 持续读取(交互式程序)====================
fmt.Println("\n交互模式(输入 quit 退出):")
scanner2 := bufio.NewScanner(os.Stdin)
for {
fmt.Print("> ")
if !scanner2.Scan() {
break
}
input := strings.TrimSpace(scanner2.Text())
if input == "quit" || input == "exit" {
fmt.Println("再见!")
break
}
if input == "" {
continue
}
// 处理命令
parts := strings.Fields(input)
switch parts[0] {
case "hello":
fmt.Println("Hello, World!")
case "echo":
fmt.Println(strings.Join(parts[1:], " "))
case "help":
fmt.Println("命令: hello, echo <text>, quit")
default:
fmt.Printf("未知命令: %s(输入 help 查看帮助)\n", parts[0])
}
}
// ==================== 标准输出的几种方式 ====================
fmt.Println("Println:自动换行")
fmt.Printf("Printf:格式化 %s %d\n", "hello", 42)
fmt.Print("Print:不换行")
fmt.Fprintln(os.Stdout, "\nFprintln:写入指定 Writer")
fmt.Fprintln(os.Stderr, "这条写到标准错误")
// 高效输出:bufio.Writer
writer := bufio.NewWriter(os.Stdout)
for i := 0; i < 100; i++ {
fmt.Fprintf(writer, "行 %d\n", i)
}
writer.Flush() // 别忘了
}12. 用 Go 执行系统命令
go
package main
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"strings"
"time"
)
func main() {
// ==================== 最简单:exec.Command + Output ====================
out, err := exec.Command("echo", "Hello from Go").Output()
if err != nil {
fmt.Println("执行失败:", err)
return
}
fmt.Println("输出:", string(out)) // Hello from Go
// ==================== 分别获取 stdout 和 stderr ====================
cmd := exec.Command("ls", "-la", "/tmp")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run() // 阻塞直到命令执行完
if err != nil {
fmt.Println("错误:", stderr.String())
return
}
fmt.Println(stdout.String())
// ==================== CombinedOutput:stdout + stderr 混合 ====================
combined, _ := exec.Command("go", "version").CombinedOutput()
fmt.Println(string(combined))
// ==================== 执行 Shell 命令(含管道、重定向等)====================
// exec.Command 不经过 shell,不支持管道
// 需要通过 sh -c 来执行
shellCmd := exec.Command("sh", "-c", "ps aux | grep go | head -5")
shellOut, _ := shellCmd.Output()
fmt.Println(string(shellOut))
// ==================== 管道传递数据给 stdin ====================
cmd2 := exec.Command("grep", "hello")
cmd2.Stdin = strings.NewReader("hello world\nfoo bar\nhello go\n")
var result bytes.Buffer
cmd2.Stdout = &result
cmd2.Run()
fmt.Println("grep 结果:", result.String())
// hello world
// hello go
// ==================== 设置环境变量和工作目录 ====================
cmd3 := exec.Command("sh", "-c", "echo $MY_VAR && pwd")
cmd3.Env = append(os.Environ(), "MY_VAR=custom_value")
cmd3.Dir = "/tmp" // 设置工作目录
out3, _ := cmd3.Output()
fmt.Println(string(out3))
// ==================== 超时控制 ====================
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
cmd4 := exec.CommandContext(ctx, "sleep", "10") // 10 秒的命令
err = cmd4.Run()
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("命令超时被终止")
}
// ==================== 实时流式输出 ====================
cmd5 := exec.Command("sh", "-c", "for i in 1 2 3 4 5; do echo $i; sleep 0.5; done")
cmd5.Stdout = os.Stdout // 直接输出到终端(实时)
cmd5.Stderr = os.Stderr
cmd5.Run()
// ==================== Start + Wait(非阻塞启动)====================
cmd6 := exec.Command("sleep", "2")
cmd6.Start() // 启动命令但不等待
fmt.Println("命令已启动,PID:", cmd6.Process.Pid)
fmt.Println("做其他事情...")
cmd6.Wait() // 等待命令完成
fmt.Println("命令执行完毕")
// ==================== 检查命令是否存在 ====================
path, err := exec.LookPath("git")
if err != nil {
fmt.Println("git 未安装")
} else {
fmt.Println("git 路径:", path)
}
}⚠️ 安全注意事项永远不要将用户输入直接拼接到 sh -c 的命令中,这会导致命令注入漏洞。应该使用 exec.Command("程序", "参数1", "参数2") 的方式,每个参数单独传递。
13. 正则表达式
go
package main
import (
"fmt"
"regexp"
"strings"
)
func main() {
// ==================== 基本匹配 ====================
// 推荐用 MustCompile(编译失败直接 panic,适用于写死的正则)
re := regexp.MustCompile(`\d+`) // 匹配数字
fmt.Println(re.MatchString("abc123")) // true
fmt.Println(re.MatchString("no digits")) // false
// Compile 返回 error(适用于动态正则)
re2, err := regexp.Compile(`[invalid`)
fmt.Println(re2, err) // nil, error parsing regexp: missing closing ]: ...
// ==================== 查找匹配 ====================
text := "价格:$29.99 和 $45.50 以及 $100"
priceRe := regexp.MustCompile(`\$\d+(\.\d{2})?`)
// 查找第一个匹配
first := priceRe.FindString(text)
fmt.Println("第一个:", first) // $29.99
// 查找所有匹配
all := priceRe.FindAllString(text, -1) // -1 表示全部
fmt.Println("所有:", all) // [$29.99 $45.50 $100]
// 限制数量
two := priceRe.FindAllString(text, 2)
fmt.Println("前两个:", two) // [$29.99 $45.50]
// ==================== 捕获组 ====================
emailRe := regexp.MustCompile(`(\w+)@(\w+)\.(\w+)`)
email := "alice@example.com"
// FindStringSubmatch 返回完整匹配 + 各捕获组
matches := emailRe.FindStringSubmatch(email)
fmt.Println("完整匹配:", matches[0]) // alice@example.com
fmt.Println("用户名:", matches[1]) // alice
fmt.Println("域名:", matches[2]) // example
fmt.Println("后缀:", matches[3]) // com
// 所有匹配的捕获组
text2 := "联系 alice@foo.com 或 bob@bar.org"
allMatches := emailRe.FindAllStringSubmatch(text2, -1)
for _, m := range allMatches {
fmt.Printf("邮箱: %s, 用户: %s\n", m[0], m[1])
}
// ==================== 命名捕获组 ====================
namedRe := regexp.MustCompile(
`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
date := "今天是 2024-01-15 星期一"
match := namedRe.FindStringSubmatch(date)
names := namedRe.SubexpNames()
for i, name := range names {
if i == 0 || name == "" {
continue
}
fmt.Printf("%s = %s\n", name, match[i])
}
// year = 2024
// month = 01
// day = 15
// ==================== 替换 ====================
re3 := regexp.MustCompile(`\b\w`)
// 每个单词首字母大写
result := re3.ReplaceAllStringFunc("hello world go", strings.ToUpper)
fmt.Println(result) // Hello World Go
// 带引用的替换
re4 := regexp.MustCompile(`(\w+)@(\w+)`)
result2 := re4.ReplaceAllString("alice@example", "$1 at $2")
fmt.Println(result2) // alice at example
// ==================== 分割 ====================
re5 := regexp.MustCompile(`[,;\s]+`) // 按逗号、分号、空白分割
parts := re5.Split("a, b; c d,,e", -1)
fmt.Println(parts) // [a b c d e]
// ==================== 实用示例 ====================
// 验证手机号
phoneRe := regexp.MustCompile(`^1[3-9]\d{9}$`)
fmt.Println("手机号:", phoneRe.MatchString("13812345678")) // true
fmt.Println("手机号:", phoneRe.MatchString("12345678")) // false
// 验证邮箱(简化版)
emailValidRe := regexp.MustCompile(
`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
fmt.Println("邮箱:", emailValidRe.MatchString("test@example.com")) // true
fmt.Println("邮箱:", emailValidRe.MatchString("invalid@")) // false
// 提取 URL
urlRe := regexp.MustCompile(`https?://[^\s<>"]+`)
html := `访问 https://go.dev 或 http://example.com/path?q=1 了解详情`
urls := urlRe.FindAllString(html, -1)
fmt.Println("URLs:", urls)
// 提取 HTML 标签内容
tagRe := regexp.MustCompile(`<h1>(.*?)</h1>`)
htmlStr := "<h1>Go 教程</h1><p>内容</p><h1>第二章</h1>"
titles := tagRe.FindAllStringSubmatch(htmlStr, -1)
for _, t := range titles {
fmt.Println("标题:", t[1])
}
// 清理多余空白
spaceRe := regexp.MustCompile(`\s+`)
cleaned := strings.TrimSpace(spaceRe.ReplaceAllString(" hello world go ", " "))
fmt.Println("清理后:", cleaned) // hello world go
}💡 Go 正则的特殊之处 Go 的 regexp 使用 RE2 语法,保证线性时间复杂度,不支持回溯、前瞻/后顾断言((?=...) (?<=...))和反向引用。这是为了安全和性能。如果需要这些功能,可以使用第三方库 github.com/dlclark/regexp2。
附录:常用 IO 模式速查
text
场景 推荐方式
──────────────────────────────────────────────────
小文件一次性读取 os.ReadFile
小文件一次性写入 os.WriteFile
大文件逐行读取 bufio.Scanner
大文件流式写入 bufio.Writer
文件拷贝 io.Copy
多 Reader 串联 io.MultiReader
同时写多个目标 io.MultiWriter
压缩/解压 compress/gzip + io.Copy
JSON 序列化 json.Marshal / json.NewEncoder
结构化日志 slog(Go 1.21+)
执行系统命令 exec.Command
正则匹配 regexp.MustCompile
路径拼接 filepath.Join
目录遍历 filepath.WalkDir