Golang:基本输入输出使用方法总结

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()

建议

  1. 默认都用 fmt.Printlnfmt.Scanln,简单直观,90%的场景够用。

  2. 需要读带空格的字符串时,放弃 fmt.Scan ,直接上 bufio,别挣扎。

  3. 养成检查错误的习惯 ,哪怕只是 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实现批量写入提升性能

相关推荐
Shingmc32 小时前
【Linux】多路转接之epoll
linux·运维·服务器·开发语言·网络
utf8mb4安全女神2 小时前
⽇志管理与深层防⽕墙
java·开发语言·spring boot
Mr.Lu ‍2 小时前
QT调试查看QT内部数据时显示无可用信息,未为 Qt5Cored.dll 加载任何符号
开发语言·qt
qq_452396232 小时前
第九篇:《Dockerfile 指令精讲(二):WORKDIR、ENV、ARG、EXPOSE》
java·开发语言·docker
JAVA社区2 小时前
Java高级全套教程(九)—— SpringCloud超详细实战详解
java·开发语言·后端·spring cloud·面试·职场和发展
wyjcxyyy2 小时前
java反序列化-cc1链
java·c语言·开发语言
山上三树2 小时前
Python 高频报错速查表(开发通用版)
开发语言·python
傻啦嘿哟2 小时前
解决DNS污染:防止OpenClaw解析API域名到虚假地址
开发语言·php
MY_TEUCK2 小时前
【MYTRUCK - AI 应用】MetaGPT 0.8.2 安装与排错完整实录(Python 3.10 + 虚拟环境)
开发语言·人工智能·python·ai