Go基础:输入与输出格式化详解
文章目录
- 一、Go语言输入与输出概述
- 二、输出格式化详解
- 2.1 常用输出函数
- 2.2 格式化动词(Verbs)
- 2.3 案例
- 三、输入格式化详解
- 3.1 常用输入函数
- 3.2 案例
- 四、Go语言 `bufio` 详解
- 4.1 为什么需要 `bufio`?(核心思想)
- 4.2 `bufio.Reader`:带缓冲的读取器
- 4.3 `bufio.Writer`:带缓冲的写入器
- 4.4 `bufio.Scanner`:更高级的读取工具
- 4.5 使用对比
一、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 stringAge int
}
func main() {// 基本输出fmt.Print("Hello, ")fmt.Println("World!")// 格式化输出name := "Alice"age := 25fmt.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 stringvar age intfmt.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. 创建 Scannerscanner := bufio.NewScanner(file)lineCount := 0wordCount := 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 程序。