Go基础:输入与输出格式化详解 - 详解
文章目录
一、Go语言输入与输出概述
Go语言中,输入与输出(I/O)操作主要通过 fmt 包和 bufio 包实现。fmt 包提供了基本的格式化输入输出功能,而 bufio 包则用于更高效的缓冲读写操作。
二、输出格式化详解
2.1 常用输出函数
Go语言中,fmt 包提供了以下常用输出函数:
fmt.Print(a ...interface{}):打印参数,不添加换行。fmt.Println(a ...interface{}):打印参数,自动添加空格和换行。fmt.Printf(format string, a ...interface{}):按格式化字符串打印。fmt.Sprintf(format string, a ...interface{}):返回格式化后的字符串,不打印。fmt.Fprintf(w io.Writer, format string, a ...interface{}):将格式化内容写入io.Writer。
2.2 格式化动词(Verbs)
格式化动词用于控制输出格式,常用动词包括:
| 动词 | 说明 |
|---|---|
%v | 值的默认格式 |
%+v | 类似 %v,但输出结构体时会添加字段名 |
%#v | 值的 Go 语法表示 |
%T | 值的类型 |
%% | 百分号 |
%d | 十进制整数 |
%b | 二进制整数 |
%x | 十六进制整数(小写) |
%f | 浮点数 |
%s | 字符串 |
%p | 指针地址 |
2.3 案例
package main
import (
"fmt"
"os"
)
type Person struct {
Name string
Age int
}
func main() {
// 基本输出
fmt.Print("Hello, ")
fmt.Println("World!")
// 格式化输出
name := "Alice"
age := 25
fmt.Printf("Name: %s, Age: %d\n", name, age)
// 结构体格式化
person := Person{Name: "Bob", Age: 30}
fmt.Printf("Person: %v\n", person) // 输出: Person: {Bob 30}
fmt.Printf("Person: %+v\n", person) // 输出: Person: {Name:Bob Age:30}
fmt.Printf("Person: %#v\n", person) // 输出: Person: main.Person{Name:"Bob", Age:30}
// 返回格式化字符串
formattedStr := fmt.Sprintf("Name: %s, Age: %d", name, age)
fmt.Println("Formatted string:", formattedStr)
// 写入文件
file, err := os.Create("output.txt")
if err != nil {
fmt.Println("Error creating file:", err)
return
}
defer file.Close()
fmt.Fprintf(file, "This is written to a file. Name: %s, Age: %d\n", name, age)
fmt.Println("Data written to file.")
}
三、输入格式化详解
3.1 常用输入函数
Go语言中,fmt 包提供了以下常用输入函数:
fmt.Scan(a ...interface{}):从标准输入读取数据,按空格分隔。fmt.Scanln(a ...interface{}):类似Scan,但读取到换行符时停止。fmt.Scanf(format string, a ...interface{}):按格式化字符串读取输入。
3.2 案例
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
// 使用 fmt.Scan 读取输入
var name string
var age int
fmt.Print("Enter your name and age (separated by space): ")
fmt.Scan(&name, &age)
fmt.Printf("Name: %s, Age: %d\n", name, age)
// 使用 fmt.Scanln 读取输入
fmt.Print("Enter your name: ")
fmt.Scanln(&name)
fmt.Printf("Name: %s\n", name)
// 使用 fmt.Scanf 读取格式化输入
fmt.Print("Enter your name and age (format: Name Age): ")
fmt.Scanf("%s %d", &name, &age)
fmt.Printf("Name: %s, Age: %d\n", name, age)
// 使用 bufio 读取整行输入
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter a sentence: ")
sentence, _ := reader.ReadString('\n')
sentence = strings.TrimSpace(sentence)
fmt.Printf("You entered: %s\n", sentence)
}
代码说明
- 输出部分:
Print和Println用于简单输出,后者会自动换行。Printf支持格式化动词,可以灵活控制输出格式。Sprintf返回格式化后的字符串,不直接打印。Fprintf将格式化内容写入io.Writer,如文件。
- 输入部分:
Scan和Scanln用于读取标准输入,按空格或换行符分隔。Scanf支持格式化输入,适用于特定格式的数据读取。bufio.NewReader提供更高效的缓冲读取功能,适合读取整行输入。
四、Go语言 bufio 详解
bufio 是 “buffered I/O” 的缩写,它通过在 I/O 操作中引入缓冲区来提升性能。这个包包装了 io.Reader 和 io.Writer 对象,创建了新的、带缓冲的 Reader 和 Writer 实例,提供了额外的功能。
4.1 为什么需要 bufio?(核心思想)
想象一下没有缓冲区的 I/O 操作,就像用一个非常小的勺子(比如一次只能装一个字节)从一个大水桶(文件或网络连接)里取水。每次取水都需要一个完整的“伸手-舀水-收回”的动作,这个过程的开销很大。bufio 的作用就是引入一个“水杯”(缓冲区)。现在你的操作变成了:
- 用勺子从水桶里快速舀满一杯水(一次性读取一大块数据到缓冲区)。
- 需要用水时,直接从水杯里倒(从缓冲区读取数据给程序)。
这个过程大大减少了“伸手-舀水-收回”的次数,从而显著提高了 I/O 效率,尤其是在处理大量小数据块或网络通信时。
核心优势:
- 减少系统调用:将多次小的读/写操作合并为少数几次大的操作,减少了昂贵的系统调用次数。
- 提供高级功能:除了性能提升,
bufio还提供了许多便捷的方法,如按行读取、按单词读取等。
4.2 bufio.Reader:带缓冲的读取器
bufio.Reader 是一个结构体,它包装了一个 io.Reader 接口。
1、创建 bufio.Reader:使用 bufio.NewReader 函数创建
func NewReader(rd io.Reader) *Reader
示例: 从标准输入创建一个缓冲读取器。
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// os.Stdin 实现了 io.Reader 接口
reader := bufio.NewReader(os.Stdin)
fmt.Printf("创建了一个缓冲区大小为 %d 字节的 Reader\n", reader.Size())
}
2、核心方法:bufio.Reader 提供了一系列强大的读取方法。
a) ReadString(delim byte) - 按分隔符读取
这是最常用的方法之一。它会读取数据,直到遇到指定的分隔符 delim,然后返回包含分隔符在内的字符串。
func (b *Reader) ReadString(delim byte) (string, error)
示例: 逐行读取用户输入,直到用户输入 “exit”。
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
fmt.Println("请输入内容(输入 'exit' 退出):")
for {
fmt.Print("> ")
// 读取直到遇到换行符 '\n'
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("读取错误:", err)
return
}
// 去除字符串两端的空白字符(包括换行符)
input = strings.TrimSpace(input)
if input == "exit" {
fmt.Println("程序退出。")
break
}
fmt.Println("你输入了:", input)
}
}
b) ReadBytes(delim byte) - 按分隔符读取字节切片
与 ReadString 类似,但返回的是 []byte 类型,在处理二进制数据或需要更高性能时非常有用。
func (b *Reader) ReadBytes(delim byte) ([]byte, error)
c) ReadLine() - 读取一行(不包含行尾符)
这是一个底层方法,它会读取一行,但不包含行尾的 \r 或 \n。它返回三个值:line (内容), isPrefix (是否过长), err (错误)。通常我们更常用 ReadString('\n'),因为它更简单直观。
d) ReadSlice(delim byte) - 读取到分隔符的切片
这个方法返回的是对缓冲区内部数据的引用,而不是拷贝。这意味着下一次读取操作可能会覆盖这次返回的数据。它性能更高,但使用起来需要更小心。
e) Peek(n int) - 预览数据Peek 返回缓冲区接下来的 n 个字节,但不会移动读取指针。也就是说,下次调用 Read 或其他读取方法时,仍然会从这次 Peek 的位置开始读。非常适合用于“偷看”一下接下来的内容是什么。
func (b *Reader) Peek(n int) ([]byte, error)
f) Read(p []byte) - 实现 io.Reader 接口bufio.Reader 本身也实现了 io.Reader 接口。当你调用它的 Read 方法时,它会尝试从自己的缓冲区中填满你提供的字节切片 p。如果缓冲区空了,它会从底层的 io.Reader 中读取数据来填满缓冲区,然后再满足你的请求。
4.3 bufio.Writer:带缓冲的写入器
bufio.Writer 包装了一个 io.Writer 接口,将写入操作先存入内存缓冲区,当缓冲区满了或被显式刷新时,才一次性写入底层的 io.Writer。
创建 bufio.Writer:使用 bufio.NewWriter 函数创建:
func NewWriter(w io.Writer) *Writer
示例: 创建一个写入标准输出的缓冲写入器。
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// os.Stdout 实现了 io.Writer 接口
writer := bufio.NewWriter(os.Stdout)
fmt.Printf("创建了一个缓冲区大小为 %d 字节的 Writer\n", writer.Size())
}
核心方法
a) Write(p []byte) - 写入字节切片
这是最核心的写入方法。它将数据写入缓冲区。如果缓冲区有足够空间,数据就暂存于此;如果空间不足,它会先将缓冲区的内容刷到底层 Writer,然后再写入新数据。
b) WriteString(s string) - 写入字符串WriteString 是一个便捷方法,内部也是调用 Write。
c) Flush() - 强制刷新
这是 bufio.Writer最重要的方法。它会将缓冲区中所有(无论是否已满)的数据都写入到底层的 io.Writer 中。
为什么 Flush 如此重要?
因为如果不调用 Flush(),当程序结束时,缓冲区里可能还有未写入的数据,这些数据就会丢失!
示例: 向文件写入多行内容。
package main
import (
"bufio"
"fmt"
"log"
"os"
)
func main() {
// 1. 创建文件
file, err := os.Create("output.txt")
if err != nil {
log.Fatal(err)
}
// 确保文件在函数结束时关闭
defer file.Close()
// 2. 创建缓冲写入器
writer := bufio.NewWriter(file)
// 3. 写入数据(此时数据在内存缓冲区中)
fmt.Fprintln(writer, "这是第一行。")
fmt.Fprintln(writer, "这是第二行。")
writer.WriteString("这是第三行,使用 WriteString。\n")
// 4. **必须刷新缓冲区,将数据写入文件**
err = writer.Flush()
if err != nil {
log.Fatal("刷新缓冲区失败:", err)
}
fmt.Println("数据已成功写入 output.txt")
}
在这个例子中,如果注释掉 writer.Flush(),output.txt 文件将是空的。
4.4 bufio.Scanner:更高级的读取工具
从 Go 1.1 开始,bufio 包引入了 Scanner 类型,它提供了一个更简单、更健壮的方式来读取数据,特别是按“token”(标记)分割数据。核心思想:Scanner 的核心是 SplitFunc 类型的函数,它定义了如何将输入流分割成一个个标记。默认的分割函数是 ScanLines,即按行分割。
使用步骤
- 创建 Scanner:
bufio.NewScanner(r io.Reader) - (可选)设置分割函数:
scanner.Split(customSplitFunc) - 循环扫描: 使用
scanner.Scan()作为循环条件,它会尝试读取下一个标记。 - 获取标记: 在循环体内,使用
scanner.Text()(返回string) 或scanner.Bytes()(返回[]byte) 获取当前标记的内容。 - 检查错误: 循环结束后,使用
scanner.Err()检查是否发生了错误。
案例1:逐行读取文件
使用 Scanner 逐行读取文件比 Reader.ReadString 更简洁,且能更好地处理行尾符。
package main
import (
"bufio"
"fmt"
"log"
"os"
"strings"
)
func main() {
// 1. 打开文件
file, err := os.Open("my_file.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// 2. 创建 Scanner
scanner := bufio.NewScanner(file)
lineCount := 0
wordCount := 0
// 3. 循环扫描
for scanner.Scan() {
line := scanner.Text()
lineCount++
// 简单地按空格分割来计算单词数
words := strings.Fields(line)
wordCount += len(words)
fmt.Printf("行 %d: %s\n", lineCount, line)
}
// 4. 检查错误
if err := scanner.Err(); err != nil {
log.Fatal("扫描时出错:", err)
}
fmt.Printf("\n扫描完成。共 %d 行,%d 个单词。\n", lineCount, wordCount)
}
案例2:自定义分割函数Scanner 的强大之处在于可以自定义分割逻辑。例如,我们可以按单词读取。
示例: 使用 ScanWords 分割函数按单词读取。
// ... (前面的代码相同)
scanner := bufio.NewScanner(strings.NewReader("Hello world, this is a test."))
// 设置分割函数为按单词分割
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil {
// ...
}
输出:
Hello
world,
this
is
a
test.
4.5 使用对比
| 类型 | 适用场景 | 优点 | 注意事项 |
|---|---|---|---|
bufio.Reader | 需要精细控制读取过程,如读取固定字节数、预览数据、读取到特定分隔符。 | 功能强大,控制力强,性能高。 | API 相对底层,使用时需注意错误处理。 |
bufio.Writer | 需要频繁进行小块写入,以减少系统调用,提高性能。 | 显著提升写入性能。 | 必须记得调用 Flush(),否则数据可能丢失。 |
bufio.Scanner | 需要方便地按行、按单词等“标记”来处理文本流。 | API 简洁易用,代码可读性高,错误处理方便。 | 功能相对固定,不如 Reader 灵活。不适合读取二进制流。 |
Go语言的输入与输出格式化功能非常强大,通过 fmt 包和 bufio 包,可以满足各种输入输出需求。掌握这些函数和格式化动词,有助于编写清晰、易读的 Go 程序。
浙公网安备 33010602011771号