Go的输入输出:从键盘敲字到屏幕显示,一文搞定
学任何语言,第一件事往往不是写算法,而是:怎么把东西打出来?怎么让用户输点东西进去?
Go的输入输出看似简单,却藏着不少实用的小技巧。今天这篇文章,我们用最直白的方式,把Go里的打印 和读取彻底讲清楚。
阅读时间 :10分钟
动手实践:每个例子都可以直接运行
一、输出:把话说给屏幕听
1.1 三大常用打印函数
Go的fmt包提供了三个层次的打印功能:
| 函数 | 作用 | 换行 | 格式化 |
|---|---|---|---|
fmt.Print() |
普通打印 | ❌ | ❌ |
fmt.Println() |
打印后换行 | ✅ | ❌ |
fmt.Printf() |
格式化打印 | ❌ | ✅ |
直接看代码:
go
package main
import "fmt"
func main() {
name := "张三"
age := 25
// Print:不换行
fmt.Print("Hello")
fmt.Print(" World")
// 输出:Hello World
// Println:自动换行,且自动在参数间加空格
fmt.Println("姓名:", name)
fmt.Println("年龄:", age)
// 输出:
// 姓名: 张三
// 年龄: 25
// Printf:格式化输出(类似C的printf)
fmt.Printf("我叫%s,今年%d岁\n", name, age)
// 输出:我叫张三,今年25岁
}
1.2 Printf 常用格式化占位符
| 占位符 | 说明 | 例子 |
|---|---|---|
%v |
默认格式(万能) | fmt.Printf("%v", person) |
%+v |
带字段名打印结构体 | {Name:张三 Age:25} |
%#v |
Go语法表示 | main.Person{Name:"张三", Age:25} |
%T |
打印类型 | string, int |
%d |
整数 | 42 |
%f |
浮点数 | 3.141590 |
%.2f |
保留两位小数 | 3.14 |
%s |
字符串 | "hello" |
%q |
带引号的字符串 | "\"hello\"" |
%t |
布尔值 | true |
%p |
指针地址 | 0xc0000140a0 |
实战小例子:
go
type User struct {
Name string
Age int
}
u := User{"李四", 30}
fmt.Printf("普通: %v\n", u) // 普通: {李四 30}
fmt.Printf("详细: %+v\n", u) // 详细: {Name:李四 Age:30}
fmt.Printf("Go语法: %#v\n", u) // Go语法: main.User{Name:"李四", Age:30}
fmt.Printf("类型: %T\n", u) // 类型: main.User
1.3 拼接字符串的几种方式
除了直接打印,有时你需要先拼好字符串:
go
// 方式1:Sprintf(返回字符串,不打印)
msg := fmt.Sprintf("Hello, %s", name)
fmt.Println(msg)
// 方式2:字符串拼接(适合少量)
msg2 := "Hello, " + name
// 方式3:strings.Builder(大量拼接时高效)
var builder strings.Builder
builder.WriteString("Hello, ")
builder.WriteString(name)
msg3 := builder.String()
二、输入:从键盘读取用户说了什么
2.1 fmt.Scan 系列(最常用)
go
var name string
var age int
fmt.Print("请输入姓名和年龄:")
n, err := fmt.Scan(&name, &age) // 注意传地址
// n 是成功读取的参数个数,err是错误
fmt.Printf("读取了%d个值,姓名:%s,年龄:%d\n", n, name, age)
运行示例:
请输入姓名和年龄:张三 25
读取了2个值,姓名:张三,年龄:25
注意:
Scan默认以空格 或换行分隔- 输入必须与变量数量和类型匹配,否则会出错
2.2 fmt.Scanln:读一整行(以换行为界)
go
var name string
var age int
fmt.Print("请输入姓名和年龄(空格分隔):")
fmt.Scanln(&name, &age) // 读到换行停止
fmt.Printf("姓名:%s,年龄:%d\n", name, age)
区别:
Scan会忽略开头的换行,且更灵活Scanln严格要求以换行结束,常见于交互式提示
2.3 fmt.Scanf:格式化输入
go
var name string
var age int
fmt.Print("请输入(格式:姓名,年龄):")
fmt.Scanf("%s,%d", &name, &age)
fmt.Printf("姓名:%s,年龄:%d\n", name, age)
运行示例:
请输入(格式:姓名,年龄):张三,25
姓名:张三,年龄:25
Scanf适合处理固定格式的输入,比如配置文件、日志解析。
2.4 读取整行字符串(包含空格)
这是最常见的需求:用户输入一行可能带空格的文字。
go
// 方法1:bufio.NewReader(推荐)
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
fmt.Print("请输入一段话:")
// 读到换行符为止
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("读取失败:", err)
return
}
// 注意:line 包含末尾的换行符,需要去掉
line = line[:len(line)-1] // 去除换行
// 或者用 strings.TrimSpace(line)
fmt.Printf("你输入的是:%s,长度:%d\n", line, len(line))
}
更简洁的方式(Go 1.16+):
go
import "fmt"
var input string
fmt.Print("请输入:")
fmt.Scanln(&input) // ❌ 遇到空格就停了
// 正确方式:用 bufio
三、实战场景:四种常见输入模式
场景1:读取一个整数,做加法
go
var a, b int
fmt.Print("请输入两个整数:")
fmt.Scan(&a, &b)
fmt.Printf("%d + %d = %d\n", a, b, a+b)
场景2:循环读取,直到输入"exit"
go
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("> ")
input, _ := reader.ReadString('\n')
input = input[:len(input)-1] // 去掉换行
if input == "exit" {
fmt.Println("再见!")
break
}
fmt.Printf("你说了:%s\n", input)
}
场景3:带提示的选择菜单
go
fmt.Println("请选择操作:")
fmt.Println("1. 查询余额")
fmt.Println("2. 取款")
fmt.Println("3. 退出")
var choice int
fmt.Print("请输入选项:")
fmt.Scan(&choice)
switch choice {
case 1:
fmt.Println("余额:1000元")
case 2:
fmt.Println("请输入金额...")
case 3:
fmt.Println("退出")
default:
fmt.Println("无效选项")
}
场景4:读取并处理带逗号的CSV格式输入
go
var name string
var age int
var score float64
fmt.Print("请输入:姓名,年龄,成绩(如:张三,25,89.5):")
fmt.Scanf("%s,%d,%f", &name, &age, &score)
fmt.Printf("姓名:%s,年龄:%d,成绩:%.1f\n", name, age, score)
四、错误处理:输入出错了怎么办?
Scan 系列函数会返回错误,实际项目应该处理它:
go
var age int
fmt.Print("请输入年龄:")
_, err := fmt.Scan(&age)
if err != nil {
fmt.Println("输入无效,请重新运行")
return
}
fmt.Printf("年龄:%d\n", age)
处理脏输入(清空缓冲区):
go
reader := bufio.NewReader(os.Stdin)
var age int
for {
fmt.Print("请输入年龄:")
_, err := fmt.Scan(&age)
if err == nil {
break
}
fmt.Println("输入错误,必须为整数")
reader.ReadString('\n') // 清空缓冲区
}
五、输出到文件:不只是屏幕
go
package main
import (
"fmt"
"os"
)
func main() {
// 创建或打开文件
file, err := os.Create("output.txt")
if err != nil {
fmt.Println("创建文件失败:", err)
return
}
defer file.Close() // 函数结束时关闭文件
// 写入内容
fmt.Fprintln(file, "Hello, 文件!")
fmt.Fprintf(file, "答案:%d\n", 42)
fmt.Println("写入成功")
}
注意:
fmt.Fprint系列函数可以把输出定向到任何实现了io.Writer接口的对象(文件、网络连接、缓冲区等)。
六、一张表总结:我该用哪个?
| 需求 | 推荐函数 |
|---|---|
| 普通打印,要换行 | fmt.Println() |
| 普通打印,不换行 | fmt.Print() |
| 需要格式化(数字、对齐等) | fmt.Printf() |
| 把格式化的字符串存起来,不打印 | fmt.Sprintf() |
| 读取多个值(空格分隔) | fmt.Scan() |
| 读取一整行(含空格) | bufio.NewReader().ReadString('\n') |
| 按指定格式读取 | fmt.Scanf() |
| 输出到文件 | fmt.Fprintln(file, ...) |
| 输出到日志 | log.Printf() |
建议
-
默认都用
fmt.Println和fmt.Scanln,简单直观,90%的场景够用。 -
需要读带空格的字符串时,放弃
fmt.Scan,直接上bufio,别挣扎。 -
养成检查错误的习惯 ,哪怕只是
if err != nil { fmt.Println(err) }。
掌握了这些内容,你已经可以写出完整的交互式命令行程序了。下一步可以尝试:一个简单的计算器、一个待办事项管理器、或者一个猜数字游戏。
bufio包完全指南:告别Scanf的种种"坑"
有多少次,你用
fmt.Scanf读字符串时发现它读到了空格就停了?有多少次,你想读一整行文本却发现自己只能读到第一个单词?
如果你遇到过这些问题,那么恭喜你------bufio正是你要找的解决方案。
核心目标:彻底掌握Go中的缓冲I/O,写出专业级别的输入处理代码
一、为什么需要bufio?一个生动的比喻
想象你去超市购物:
- 不使用缓冲 (
fmt.Scan):每次买一个东西就去收银台结一次账,买100件东西就要排队100次。 - 使用缓冲 (
bufio):推个购物车,把所有东西装满后一次性结账,只需要排队1次。
bufio做的就是这件事:它会在内存中维护一个缓冲区,批量读取数据,避免频繁的系统调用,大幅提升性能。
更重要的是,它提供了读取一整行 、按分隔符读取 、预读等强大功能。
二、bufio核心类型:一张图看懂
┌─────────────────┐
用户代码 │ bufio.Reader │
│ (带缓冲的读取器) │
└────────┬────────┘
│ 内部有缓冲区 (默认4096字节)
↓
┌─────────────────┐
操作系统/文件 │ 实际I/O对象 │
│ (os.File, net.Conn等)│
└─────────────────┘
三种核心类型:
bufio.Reader:带缓冲的读取器(最常用)bufio.Writer:带缓冲的写入器bufio.Scanner:更方便的文本扫描器(Go 1.1+)
三、bufio.Reader:读取的瑞士军刀
3.1 创建Reader
go
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 方式1:从标准输入读取
reader1 := bufio.NewReader(os.Stdin)
// 方式2:从文件读取
file, _ := os.Open("data.txt")
defer file.Close()
reader2 := bufio.NewReader(file)
// 方式3:从字符串读取(常用于测试)
import "strings"
reader3 := bufio.NewReader(strings.NewReader("hello world"))
// 方式4:自定义缓冲区大小(默认4096字节)
reader4 := bufio.NewReaderSize(os.Stdin, 8192) // 8KB缓冲
}
3.2 最常用的三个读取方法
| 方法 | 说明 | 适用场景 |
|---|---|---|
ReadString(delim byte) |
读到指定分隔符,返回字符串 | 读一行:ReadString('\n') |
ReadBytes(delim byte) |
同上,返回字节切片 | 处理二进制或原始数据 |
ReadLine() (已弃用) |
不推荐,用ReadString代替 |
- |
实战:读取一整行(最经典用法)
go
reader := bufio.NewReader(os.Stdin)
fmt.Print("请输入你的名字:")
name, err := reader.ReadString('\n')
if err != nil {
fmt.Println("读取失败:", err)
return
}
// 注意:name 包含末尾的换行符
name = name[:len(name)-1] // 去除换行
// 或者更健壮的方式:
// name = strings.TrimRight(name, "\r\n")
fmt.Printf("你好,%s!\n", name)
读取直到遇到特定字符:
go
// 读取直到遇到逗号
line, _ := reader.ReadString(',')
fmt.Println("读到逗号前的部分:", line)
// 读取直到遇到句号
sentence, _ := reader.ReadString('.')
3.3 读取固定大小:Read()
go
// 读取固定数量的字节到缓冲区
buffer := make([]byte, 10) // 准备10字节的缓冲区
n, err := reader.Read(buffer)
if err != nil {
fmt.Println("读取失败:", err)
return
}
fmt.Printf("读取了%d字节: %s\n", n, buffer[:n])
3.4 预读功能:Peek()
这是bufio的独门绝技:看一眼即将读到什么,但不移动读取位置。
go
reader := bufio.NewReader(strings.NewReader("Hello World"))
// 偷看前5个字符,但不消耗它们
data, err := reader.Peek(5)
if err != nil {
fmt.Println("Peek失败:", err)
return
}
fmt.Printf("Peek看到: %s\n", data) // 输出: Hello
// 实际读取
actual, _ := reader.ReadString(' ')
fmt.Printf("实际读取: %s\n", actual) // 输出: Hello (注意包含空格)
Peek的应用场景:
- 判断下一个字符是不是特定格式(如JSON开头是否为
{) - 实现"先看看再决定怎么读"的逻辑
3.5 丢弃指定字节:Discard()
go
// 跳过前10个字节不读
_, err := reader.Discard(10)
if err != nil {
fmt.Println("跳过失败:", err)
}
四、bufio.Writer:高效写入的利器
4.1 基本使用
go
// 创建Writer
writer := bufio.NewWriter(os.Stdout)
// 写入内容(注意:此时还在缓冲区,没有真正输出)
writer.WriteString("Hello, ")
writer.WriteString("World\n")
// 手动刷新:将缓冲区内容真正写入底层Writer
writer.Flush() // 现在才会显示在屏幕上
4.2 缓存写入的完整示例
go
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// 创建文件
file, err := os.Create("output.txt")
if err != nil {
fmt.Println("创建文件失败:", err)
return
}
defer file.Close()
// 创建带缓冲的Writer(缓冲区大小4KB)
writer := bufio.NewWriter(file)
// 写入100行数据(它们都会先进入缓冲区)
for i := 1; i <= 100; i++ {
fmt.Fprintf(writer, "第%d行: 这是一些测试数据\n", i)
}
// 最后统一刷新到文件(重要!)
writer.Flush()
fmt.Println("数据已写入文件")
}
4.3 Writer的核心方法
| 方法 | 作用 |
|---|---|
Write(p []byte) |
写入字节切片 |
WriteString(s string) |
写入字符串 |
WriteByte(c byte) |
写入单个字节 |
WriteRune(r rune) |
写入Unicode字符 |
Flush() |
将缓冲区内容真正写出 |
Available() |
查看缓冲区剩余字节数 |
Buffered() |
查看缓冲区中未写出的字节数 |
调试技巧:
go
writer := bufio.NewWriter(os.Stdout)
writer.WriteString("Hello")
fmt.Printf("缓冲区未刷新的大小: %d\n", writer.Buffered()) // 输出: 5
fmt.Printf("缓冲区可用空间: %d\n", writer.Available()) // 输出: 4091 (默认4096-5)
writer.Flush() // 现在真正输出了
五、bufio.Scanner:更现代、更优雅的读取方式
Scanner是Go 1.1后推荐的逐行读取工具,比Reader更简洁。
5.1 逐行读取文件
go
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
file, err := os.Open("data.txt")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
lineNum := 1
for scanner.Scan() { // 每次读取一行
line := scanner.Text() // 获取该行内容(不含换行符)
fmt.Printf("第%d行: %s\n", lineNum, line)
lineNum++
}
// 检查扫描过程中是否有错误
if err := scanner.Err(); err != nil {
fmt.Println("读取出错:", err)
}
}
5.2 自定义分隔符(不限于换行)
go
package main
import (
"bufio"
"fmt"
"strings"
)
func main() {
text := "apple,banana,orange,grape"
scanner := bufio.NewScanner(strings.NewReader(text))
// 设置分隔符为逗号
scanner.Split(bufio.ScanWords) // 按单词
// 更精确:自定义分隔函数
// scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
// // 自己实现按逗号分割
// })
// 使用更简单的方法:按逗号分割需要自己实现,这里展示标准分割
scanner = bufio.NewScanner(strings.NewReader(text))
scanner.Split(bufio.ScanWords) // 会按空格分割,不适合逗号
// 正确做法:按逗号分割(Go 1.15+)
scanner = bufio.NewScanner(strings.NewReader(text))
scanner.Split(bufio.ScanRunes) // 不适合
// 实际项目中常用 strings.Split 处理逗号分隔
}
内置的Split函数:
| Split函数 | 作用 |
|---|---|
ScanLines |
按行分割(默认) |
ScanWords |
按单词分割(空格分隔) |
ScanRunes |
按UTF-8字符分割 |
ScanBytes |
按字节分割 |
自定义Split示例(按逗号分割):
go
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
}
// 使用
scanner := bufio.NewScanner(strings.NewReader("a,b,c,d"))
scanner.Split(scanComma)
for scanner.Scan() {
fmt.Println(scanner.Text()) // 输出 a, b, c, d 每行一个
}
5.3 处理大文件:调整缓冲区大小
默认缓冲区只有64KB,对于超大行(如巨型JSON)会报错:
go
scanner := bufio.NewScanner(file)
// 设置更大的缓冲区(例如1MB)
buffer := make([]byte, 1024*1024)
scanner.Buffer(buffer, cap(buffer))
// 现在可以读取很长的行了
for scanner.Scan() {
// 处理...
}
六、实战案例:三个常用场景
场景1:统计文件行数、单词数、字符数
go
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
file, err := os.Open("input.txt")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
lines := 0
words := 0
chars := 0
for scanner.Scan() {
line := scanner.Text()
lines++
chars += len(line)
words += len(strings.Fields(line)) // 统计单词数
}
fmt.Printf("行数: %d, 单词数: %d, 字符数: %d\n", lines, words, chars)
}
场景2:带超时的用户输入
go
package main
import (
"bufio"
"fmt"
"os"
"time"
)
func main() {
// 创建一个channel来接收输入
inputChan := make(chan string)
// 启动goroutine等待输入
go func() {
reader := bufio.NewReader(os.Stdin)
fmt.Print("请在5秒内输入: ")
input, _ := reader.ReadString('\n')
inputChan <- input
}()
// 等待输入或超时
select {
case input := <-inputChan:
fmt.Printf("你输入了: %s", input)
case <-time.After(5 * time.Second):
fmt.Println("\n超时!你没有输入任何内容")
}
}
场景3:逐行读取并处理CSV文件(简化版)
go
func readCSV(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
// 跳过空行
if len(line) == 0 {
continue
}
// 按逗号分割(简单处理,实际CSV需要考虑引号内的逗号)
fields := strings.Split(line, ",")
if len(fields) < 3 {
fmt.Printf("第%d行格式错误\n", lineNum)
continue
}
fmt.Printf("第%d行: 姓名=%s, 年龄=%s, 城市=%s\n",
lineNum, fields[0], fields[1], fields[2])
}
return scanner.Err()
}
七、性能对比:bufio vs 普通读取
写一个简单的性能测试对比:
go
package main
import (
"bufio"
"fmt"
"os"
"time"
)
func readWithoutBuf(filename string) {
file, _ := os.Open(filename)
defer file.Close()
data := make([]byte, 1)
start := time.Now()
count := 0
for {
_, err := file.Read(data)
if err != nil {
break
}
count++
}
fmt.Printf("无缓冲: 读取%d次, 耗时%v\n", count, time.Since(start))
}
func readWithBuf(filename string) {
file, _ := os.Open(filename)
defer file.Close()
reader := bufio.NewReader(file)
start := time.Now()
count := 0
for {
_, err := reader.ReadByte()
if err != nil {
break
}
count++
}
fmt.Printf("带缓冲: 读取%d次, 耗时%v\n", count, time.Since(start))
}
结果 :带缓冲的版本通常快 5-50倍,数据越大差距越明显。
八、常见错误与坑
坑1:忘记Flush导致数据丢失
go
// ❌ 错误示例
writer := bufio.NewWriter(file)
writer.WriteString("important data")
// 程序结束了,但数据还在缓冲区,文件里没有!
// ✅ 正确示例
writer := bufio.NewWriter(file)
writer.WriteString("important data")
writer.Flush() // 必须调用
坑2:Reader读取后残留换行符
go
name, _ := reader.ReadString('\n')
fmt.Printf("'%s'", name) // 输出: '张三\n' ← 包含换行符
// 解决方法
name = strings.TrimRight(name, "\r\n")
坑3:Scanner无法处理超过64KB的行
go
// ❌ 会报错:bufio.Scanner: token too long
scanner := bufio.NewScanner(file)
for scanner.Scan() {
// 某一行超过64KB就崩溃
}
// ✅ 解决方法:增加缓冲区
buf := make([]byte, 0, 1024*1024) // 1MB
scanner.Buffer(buf, cap(buf))
九、选择指南:Reader vs Scanner
| 场景 | 推荐工具 | 理由 |
|---|---|---|
| 逐行读取文本文件 | Scanner |
代码简洁,自动处理换行 |
| 需要预读(Peek) | Reader |
Scanner不支持Peek |
| 读取二进制数据 | Reader |
需要ReadBytes/Read方法 |
| 按自定义分隔符分割 | Scanner |
可以自定义Split函数 |
| 交互式命令行 | Reader |
更灵活,支持ReadString |
| 解析大型CSV | 第三方库或Reader |
Scanner处理复杂格式有限 |
| 需要读取性能极致优化 | Reader |
更底层的控制 |
简单原则:
- 读文本行 → 用
Scanner - 读用户输入 → 用
Reader.ReadString('\n') - 其他复杂场景 → 用
Reader
写在最后:你的工具箱升级了
掌握了bufio,你就掌握了Go语言中处理输入输出的核心技能:
✅ 从标准输入、文件、网络连接中高效读取
✅ 用ReadString完美解决读取一整行 的问题
✅ 用Scanner优雅地逐行处理大文件
✅ 用Writer实现批量写入提升性能