36·标准库入门

格式化输出 fmt

fmtprintfsprintfformat

第 36 课 - 格式化输出 fmt

学习目标

  • 掌握 fmt 包中常用的打印函数及其区别
  • 熟练使用格式化占位符进行类型安全的输出
  • 学会使用 Sprintf 构建格式化字符串
  • 了解错误输出和扫描输入的基本方法

核心概念

1. 三种基本打印函数

Go 的 fmt 包提供了三种主要的打印函数,它们各有特点:

  • Println - 最简单的输出,会在参数间添加空格,结尾自动换行
  • Printf - 格式化输出,需要指定格式字符串和对应的参数
  • Sprintf - 不直接输出,而是返回格式化后的字符串
// 三种函数的对比
package main

import "fmt"

func main() {
    // Println - 参数间有空格,结尾换行
    fmt.Println("Hello", "World", 123)
    // 输出: Hello World 123\n
    
    // Printf - 需要格式化字符串
    fmt.Printf("Hello %s, age %d\n", "Tom", 25)
    // 输出: Hello Tom, age 25\n
    
    // Sprintf - 返回格式化后的字符串
    msg := fmt.Sprintf("Hello %s, age %d", "Jerry", 30)
    fmt.Println(msg)
    // 输出: Hello Jerry, age 30
}

2. 常用格式化占位符

占位符说明示例
%d十进制整数fmt.Printf("%d", 100)100
%s字符串fmt.Printf("%s", "Go")Go
%f浮点数(默认6位小数)fmt.Printf("%f", 3.14)3.140000
%T值的类型fmt.Printf("%T", 100)int
%v默认格式的值fmt.Printf("%v", []int{1,2})[1 2]
%b二进制fmt.Printf("%b", 5)101
%x十六进制fmt.Printf("%x", 255)ff

3. 格式化宽度和精度控制

package main

import "fmt"

func main() {
    // 宽度控制 - 最小字符数
    fmt.Printf("|%10d|\n", 123)      // 右对齐: |       123|
    fmt.Printf("|%-10d|\n", 123)     // 左对齐: |123       |
    fmt.Printf("|%010d|\n", 123)     // 零填充: |0000000123|
    
    // 浮点数精度
    fmt.Printf("|%.2f|\n", 3.14159)  // 两位小数: |3.14|
    fmt.Printf("|%8.2f|\n", 3.14159) // 宽度8,两位小数: |    3.14|
    
    // 字符串宽度
    fmt.Printf("|%20s|\n", "Go")     // 右对齐: |                  Go|
    fmt.Printf("|%-20s|\n", "Go")    // 左对齐: |Go                  |
}

代码示例

示例1:基本格式化输出

package main

import (
    "fmt"
    "os"
)

type User struct {
    Name string
    Age  int
    City string
}

func main() {
    // 1. 基本打印
    fmt.Println("=== 基本打印 ===")
    fmt.Println("Hello, Go!")
    fmt.Println("数字:", 42, "布尔:", true)
    
    // 2. 格式化输出
    fmt.Println("\n=== 格式化输出 ===")
    name := "张三"
    age := 28
    score := 89.5
    fmt.Printf("姓名: %s, 年龄: %d, 分数: %.1f\n", name, age, score)
    
    // 3. 复杂类型格式化
    fmt.Println("\n=== 结构体格式化 ===")
    user := User{
        Name: "李四",
        Age:  35,
        City: "北京",
    }
    fmt.Printf("用户信息: %+v\n", user)    // 包含字段名
    fmt.Printf("类型: %T\n", user)         // 显示类型
    fmt.Printf("默认格式: %v\n", user)     // 默认格式
    
    // 4. 使用 Sprintf 构建字符串
    fmt.Println("\n=== 构建字符串 ===")
    filePath := fmt.Sprintf("/home/%s/%s.txt", user.Name, "document")
    fmt.Println("文件路径:", filePath)
    
    // 5. 错误输出到 stderr
    fmt.Println("\n=== 错误输出 ===")
    fmt.Fprintln(os.Stderr, "这是一个错误信息")
}

示例2:实用的格式化技巧

package main

import (
    "fmt"
    "time"
)

func main() {
    // 1. 动态宽度
    values := []struct {
        name  string
        value int
    }{
        {"短名", 100},
        {"这是一个很长的名字", 200},
    }
    
    fmt.Println("=== 动态宽度 ===")
    for _, v := range values {
        fmt.Printf("%-20s: %10d\n", v.name, v.value)
    }
    
    // 2. 科学计数法
    fmt.Println("\n=== 科学计数法 ===")
    bigNum := 1234567890.12345
    fmt.Printf("默认: %f\n", bigNum)
    fmt.Printf("科学计数法: %e\n", bigNum)
    fmt.Printf("更简洁: %E\n", bigNum)
    
    // 3. 时间格式化
    fmt.Println("\n=== 时间格式化 ===")
    now := time.Now()
    fmt.Printf("时间: %s\n", now.Format(time.RFC3339))
    fmt.Printf("Unix时间戳: %d\n", now.Unix())
    
    // 4. 构建表格
    fmt.Println("\n=== 简易表格 ===")
    fmt.Printf("%-10s %-15s %-10s\n", "编号", "姓名", "成绩")
    fmt.Println("================================")
    fmt.Printf("%-10d %-15s %-10.1f\n", 1, "王五", 95.5)
    fmt.Printf("%-10d %-15s %-10.1f\n", 2, "赵六", 88.0)
}

实践练习

练习1:学生信息输出(初级)

创建一个程序,输出以下格式的学生信息:

姓名: 王小明
学号: 2023001
年龄: 20岁
成绩: 88.5分

要求

  1. 使用变量存储这些信息
  2. 使用 Printf 格式化输出
  3. 成绩保留一位小数

预期输出

姓名: 王小明
学号: 2023001
年龄: 20岁
成绩: 88.5分

练习2:商品价格表(中级)

编写一个程序,打印商品价格表:

商品名称        单价    数量    总价
========================================
苹果           5.50       3    16.50
香蕉           3.20       5    16.00
橘子           4.80       2     9.60
========================================
合计                           42.10

要求

  1. 使用结构体存储商品信息
  2. 价格保留两位小数
  3. 各列要对齐

练习3:日志格式化(高级)

创建一个日志格式化函数 FormatLog,它接受日志级别、消息和时间戳,返回格式化的日志字符串。

函数签名

func FormatLog(level, message string, timestamp time.Time) string

要求

  1. 格式:[2024-01-15 10:30:45] [INFO] 服务器启动成功
  2. 日志级别要大写并右对齐(占7个字符宽度)
  3. 时间格式:2006-01-02 15:04:05

测试用例

timestamp := time.Date(2024, 1, 15, 10, 30, 45, 0, time.UTC)
fmt.Println(FormatLog("info", "服务器启动成功", timestamp))
// 输出: [2024-01-15 10:30:45] [   INFO] 服务器启动成功

常见错误

1. 混淆 Println 和 Printf

// 错误示例:使用 Printf 但忘记格式化字符串
fmt.Printf("Hello World")  // 输出: Hello World (没有格式化,应该用 Println)

// 正确做法
fmt.Println("Hello World")  // 输出: Hello World\n

2. 格式化占位符不匹配

// 错误示例:占位符数量与参数数量不匹配
fmt.Printf("Name: %s, Age: %d\n", "Tom")  
// 编译错误:缺少 age 参数

// 正确做法
fmt.Printf("Name: %s, Age: %d\n", "Tom", 25)

3. 字符串连接时忘记类型转换

// 错误示例:直接用 + 连接不同类型
message := "User: " + 123  // 编译错误:不能将 int 与 string 连接

// 正确做法
message := fmt.Sprintf("User: %d", 123)

4. 忽略 Sprintf 的错误返回值

// 错误示例:Sprintf 不会返回错误,但 Fprintf 会
_, err := fmt.Fprintf(os.Stderr, "Error: %s\n", "文件未找到")
if err != nil {
    // 处理写入错误
}

5. 过度使用 %v

// 不推荐:总是用 %v 会丢失类型信息
user := User{Name: "Tom", Age: 25}
fmt.Printf("%v\n", user)  // {Tom 25} - 看不到字段名

// 推荐:使用 %+v 显示字段名
fmt.Printf("%+v\n", user) // {Name:Tom Age:25}

小结

关键要点

  1. 三个主要函数

    • Println - 简单输出,自动换行
    • Printf - 格式化输出,需要格式字符串
    • Sprintf - 返回格式化后的字符串
  2. 常用占位符

    • %d 整数、%s 字符串、%f 浮点数
    • %T 类型、%v 默认格式、%+v 显示字段名
    • %b 二进制、%x 十六进制
  3. 格式化技巧

    • 宽度控制:%10s(最小宽度)、%-10s(左对齐)
    • 精度控制:%.2f(保留两位小数)
    • 零填充:%010d(用零填充到10位)
  4. 最佳实践

    • 优先使用 Sprintf 构建字符串,而不是字符串连接
    • 复杂格式化时使用 Fprintf 输出到特定 writer
    • 调试时使用 %+v 查看结构体详细信息
  5. 注意事项

    • % 本身需要转义:%% 显示一个百分号
    • Printf 不会自动换行,需要手动添加 \n
    • 格式化字符串中的占位符必须与后续参数数量、类型匹配

下一步学习

在下一课中,我们将学习 strings 包,它提供了丰富的字符串处理函数,与 fmt 包配合使用,可以完成更复杂的文本处理任务。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「字符串处理 strings」 以巩固所学知识。