第2章:和世界说话——fmt 包

第2章:和世界说话——fmt 包

想象一下,你写了一个计算器程序,结果它只会默默算,算完就什么都不说——那用户怎么知道 1+1=2 呢?总不能要求用户去读内存吧?所以,fmt 包就是程序对外交流的桥梁,让你的程序既能"说话"(输出),也能"听话"(输入)。


2.1 fmt包解决什么问题

2.1.1 程序对外交流的"嘴巴"和"耳朵"

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

func main() {
    // 程序通过 fmt 把计算结果"说"给用户听
    fmt.Println("你好!我是你的计算器")
    // 程序通过 fmt 接收用户的输入
    var name string
    fmt.Print("请告诉我你的名字:")
    fmt.Scan(&name)
    fmt.Println("欢迎你,", name)
}
// 运行结果
你好!我是你的计算器
请告诉我你的名字:张三
欢迎你, 张三

专业词汇解释:

  • 标准输出 (stdout):程序默认输出内容的地方,在终端就是屏幕
  • 标准输入 (stdin):程序默认读取输入的地方,在终端就是键盘
  • 格式化 I/O:按照特定格式进行输入输出操作

2.1.2 输出信息给用户,读取用户输入

fmt 包的核心功能就两个:说和听。它提供了多个函数来完成这两个任务:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
    // 向世界宣告!
    fmt.Println("=== fmt 包的两种超能力 ===")
    fmt.Println("超能力1: Print 家族 - 说话")
    fmt.Println("超能力2: Scan 家族 - 听话")
}
=== fmt 包的两种超能力 ===
超能力1: Print 家族 - 说话
超能力2: Scan 家族 - 听话

2.2 fmt核心原理

2.2.1 Print 系列输出

Print 系列函数是 fmt 包最常用的输出函数,它们把数据"打印"到指定的目标。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import "fmt"

func main() {
    fmt.Print("我是 Print,")   // 不换行,不加空格
    fmt.Print("我喜欢直来直去\n")

    fmt.Println("我是 Println,") // 自动加空格和换行
    fmt.Println("我比较讲究格式")

    name := "Go语言"
    version := 1.26
    fmt.Printf("我是 Printf,%s 版本 %.1f 是我的最强形态\n", name, version)
}
我是 Print,不换行,不加空格
我喜欢直来直去
我是 Println,
我比较讲究格式
我是 Printf,Go语言 版本 1.2 是我的最强形态

2.2.2 Scan 系列输入

Scan 系列函数从各种来源读取数据,把外界的输入"扫描"进程序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "fmt"

func main() {
    fmt.Println("=== Scan 家族的工作方式 ===")
    var a, b int
    fmt.Println("请输入两个整数(用空格分隔):")
    fmt.Scan(&a, &b) // 从 stdin 读取,存到变量里
    fmt.Printf("你输入了: %d 和 %d\n", a, b)
}
=== Scan 家族的工作方式 ===
请输入两个整数(用空格分隔):
10 20
你输入了: 10 和 20

2.2.3 Stringer/Scanner 两个接口,构成了 Go 格式化 IO 的基础

这两个接口是 fmt 包的精髓,让自定义类型也能享受"特殊待遇"。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

// Stringer 接口:实现它,你的类型就能自定义打印格式
type Person struct {
    Name string
    Age  int
}

// 为 Person 实现 String() 方法
func (p Person) String() string {
    return fmt.Sprintf("%s (%d岁)", p.Name, p.Age)
}

func main() {
    p := Person{Name: "小明", Age: 18}
    fmt.Println(p) // fmt.Println 会自动调用 p.String()
}
小明 (18岁)

2.3 Print 系列函数

2.3.1 Print

Print 是家族里的老实人,它直接把内容原封不动地输出,不做任何装饰。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
    fmt.Print("hello")
    fmt.Print(" ")
    fmt.Print("world")
    fmt.Print("\n")
}
hello world

2.3.2 Println

Println 比 Print 更贴心一些,它会在每个参数之间加一个空格,并且在最后自动换行。

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
    fmt.Println("hello world") // 自动加空格
    fmt.Println("你好")         // 自动换行
    fmt.Println(1, 2, 3, 4, 5) // 多个参数,参数间自动加空格
}
hello world
你好
1 2 3 4 5

2.3.3 Printf

Printf 是格式化输出的大师,它需要一个格式化字符串(第一个参数),里面可以包含占位符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

func main() {
    name := "Go语言"
    price := 99.9
    count := 3
    fmt.Printf("商品: %s\n", name)
    fmt.Printf("单价: %.2f 元\n", price)
    fmt.Printf("数量: %d\n", count)
    fmt.Printf("总计: %.2f 元\n", price*float64(count))
}
商品: Go语言
单价: 99.90 元
数量: 3
总计: 299.70 元

2.3.4 Print 不换行,Println 自动加空格和换行,Printf 格式化后换行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

func main() {
    fmt.Print("AAA")
    fmt.Print("BBB")
    fmt.Println() // 手动换行

    fmt.Println("CCC", "DDD", "EEE") // 参数间自动加空格,最后自动换行

    fmt.Printf("FFF\n") // 格式化字符串里的 \n 才换行
}
AAABBB
CCC DDD EEE
FFF

2.4 Sprint 系列函数

2.4.1 Sprint

Sprint 把东西"拼接"成字符串,但不打印出来。需要你手动"领走"。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

func main() {
    // 注意:fmt.Sprint 不会在参数之间自动加空格!
    s := fmt.Sprint("Go", "是", "最好的", "语言")
    fmt.Println("Sprint 的结果是:", s)

    // 如果要加空格,需要手动加
    s2 := fmt.Sprint("Go", " ", "是", " ", "最好的", " ", "语言")
    fmt.Println("带空格版:", s2)
}
Sprint 的结果是: Go是最好的语言
带空格版: Go 是 最好的 语言

2.4.2 Sprintln

Sprintln 类似 Println,但它返回拼接好的字符串(带空格,末尾带换行)。

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
    s := fmt.Sprintln("apple", "banana", "cherry")
    fmt.Printf("字符串内容: [%s]\n", s) // 显示 \n
}
字符串内容: [apple banana cherry
]

2.4.3 Sprintf

SprintfPrintf 的孪生兄弟,但它不打印,而是把格式化的结果返回为字符串。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "fmt"

func main() {
    name := "小明"
    score := 95.5
    // Sprintf 返回字符串,而不是打印
    msg := fmt.Sprintf("%s 的成绩是 %.1f 分", name, score)
    fmt.Println(msg)
}
小明的成绩是 95.5 分

2.4.4 返回字符串,不输出到任何地方

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import "fmt"

func main() {
    // 这三个函数都不打印,只返回字符串
    s1 := fmt.Sprint("不打印")
    s2 := fmt.Sprintln("不打印")
    s3 := fmt.Sprintf("不打印%d", 123)

    fmt.Println("实际打印的是这里:")
    fmt.Println(s1)
    fmt.Println(s2)
    fmt.Println(s3)
}
实际打印的是这里:
不打印
不打印
不打印123

2.5 Fprint 系列函数

2.5.1 Fprint

Fprint 把内容写到指定的 Writer(比如文件),而不是屏幕。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
    "fmt"
    "os"
)

func main() {
    // 打开文件(如果不存在则创建)
    f, err := os.Create("output.txt")
    if err != nil {
        fmt.Println("创建文件失败:", err)
        return
    }
    defer f.Close()

    // 写入文件,而不是屏幕
    fmt.Fprint(f, "Hello from Fprint!")
    fmt.Fprint(f, " ")
    fmt.Fprint(f, "这是一次文件写入练习。\n")

    fmt.Println("写入完成!内容在 output.txt 中")
}
写入完成!内容在 output.txt 中

2.5.2 Fprintln

Fprintln 写入指定 Writer,自动加空格和换行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
    "fmt"
    "os"
)

func main() {
    f, _ := os.Create("output2.txt")
    defer f.Close()

    fmt.Fprintln(f, "第一行")
    fmt.Fprintln(f, "第二行")
    fmt.Fprintln(f, "第三行")
}

2.5.3 Fprintf

Fprintf 按格式化字符串写入指定 Writer。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
    "fmt"
    "os"
)

func main() {
    f, _ := os.Create("output3.txt")
    defer f.Close()

    name := "Go语言"
    version := 1.26
    fmt.Fprintf(f, "语言: %s\n", name)
    fmt.Fprintf(f, "版本: %.2f\n", version)
}

2.5.4 输出到指定的 io.Writer(如文件)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
    "bytes"
    "fmt"
)

// 任何实现了 io.Writer 接口的类型都可以使用 Fprint 系列函数
func main() {
    // bytes.Buffer 也实现了 io.Writer
    buf := new(bytes.Buffer)

    fmt.Fprint(buf, "写入到 buffer 里,")
    fmt.Fprint(buf, "而不是屏幕。\n")

    fmt.Println("buffer 的内容是:")
    fmt.Print(buf.String())
}
buffer 的内容是:
写入到 buffer 里,而不是屏幕。

2.6 通用占位符 %v、%+v、%#v、%T、%%

2.6.1 %v 万能占位符

%v 是最常用的占位符,它能匹配任何类型的值,输出该值的默认格式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

func main() {
    // 整数
    fmt.Printf("%v\n", 42)

    // 浮点数
    fmt.Printf("%v\n", 3.14159)

    // 字符串
    fmt.Printf("%v\n", "hello")

    // 布尔
    fmt.Printf("%v\n", true)

    // 数组/切片
    fmt.Printf("%v\n", []int{1, 2, 3})

    // 结构体
    fmt.Printf("%v\n", struct{ Name string }{Name: "小明"})
}
42
3.14159
hello
true
[1 2 3]
{小明}

2.6.2 %+v 带字段名

%+v%v 的基础上,对于结构体,会带上字段名输出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import "fmt"

func main() {
    type Person struct {
        Name string
        Age  int
    }

    p := Person{Name: "小红", Age: 20}

    fmt.Println("用 %v:", p)
    fmt.Printf("用 %%+v: %+v\n", p)
}
用 %v: {小红 20}
用 %+v: {Name:小红 Age:20}

2.6.3 %#v 带类型语法(Go 语法)

%#v 会输出一个合法的 Go 语法表示,包括类型信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func main() {
    type Person struct {
        Name string
        Age  int
    }

    p := Person{Name: "小刚", Age: 25}

    fmt.Printf("%%v:  %v\n", p)
    fmt.Printf("%%+v: %+v\n", p)
    fmt.Printf("%%#v: %#v\n", p)

    nums := []int{10, 20, 30}
    fmt.Printf("%%v:  %v\n", nums)
    fmt.Printf("%%#v: %#v\n", nums)
}
%v:  {小刚 25}
%+v: {Name:小刚 Age:25}
%#v: main.Person{Name:"小刚", Age:25}
%v:  [10 20 30]
%#v: []int{10, 20, 30}

2.6.4 %T 类型名

%T 输出值的类型名称

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

func main() {
    fmt.Printf("42 的类型是: %T\n", 42)
    fmt.Printf("3.14 的类型是: %T\n", 3.14)
    fmt.Printf("\"hello\" 的类型是: %T\n", "hello")
    fmt.Printf("true 的类型是: %T\n", true)
    fmt.Printf("[]int{1,2} 的类型是: %T\n", []int{1, 2})
    fmt.Printf("map 的类型是: %T\n", make(map[string]int))
}
42 的类型是: int
3.14 的类型是: float64
hello 的类型是: string
true 的类型是: bool
[]int{1,2} 的类型是: []int
map 的类型是: map[string]int

2.6.5 %% 百分号本身

当你需要输出百分号本身时,用 %%

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
    fmt.Printf("进度: 50%%\n")
    fmt.Printf("增长率: 120%%\n")
    fmt.Printf("概率: %.1f%%\n", 75.5)
}
进度: 50%
增长率: 120%
概率: 75.5%

2.7 布尔占位符 %t:输出 true 或 false

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "fmt"

func main() {
    married := true
    isAdult := false

    fmt.Printf("结婚了?%t\n", married)
    fmt.Printf("是成年人?%t\n", isAdult)
}
结婚了?true
是成年人?false

2.8 整数占位符

2.8.1 %d(十进制)

十进制是我们日常生活最熟悉的计数方式。

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
    num := 255
    fmt.Printf("十进制: %d\n", num)
}
十进制: 255

2.8.2 %b(二进制)

二进制是计算机的母语,只有 0 和 1 两个数字。

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
    num := 42
    fmt.Printf("二进制: %b\n", num)
}
二进制: 101010

2.8.3 %o(八进制)

八进制曾经很流行(Unix 文件权限就用八进制表示)。

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
    num := 64
    fmt.Printf("八进制: %o\n", num)
}
八进制: 100

2.8.4 %x/%X(十六进制小写/大写)

十六进制是程序员的好朋友,16个符号(0-9, a-f 或 A-F)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

func main() {
    num := 255
    fmt.Printf("十六进制小写: %x\n", num)
    fmt.Printf("十六进制大写: %X\n", num)

    // 颜色代码常用十六进制
    fmt.Printf("红色: #%02X%02X%02X\n", 255, 0, 0)
}
十六进制小写: ff
十六进制大写: FF
红色: #FF0000

2.8.5 %q(单引号字符)

%q 输出字符的单引号形式,用于表示单个字符。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
    // 字符(rune)用单引号
    fmt.Printf("字符 A: %q\n", 'A')
    fmt.Printf("字符 中: %q\n", '中')
    fmt.Printf("ASCII 65: %q\n", 65) // 65 是 'A' 的码点
}
字符 A: 'A'
字符 中: '中'
ASCII 65: 'A'

2.8.6 %U(Unicode 格式 U+)

%U 输出 Unicode 码点的标准格式 U+XXXX

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
    // Unicode 码点
    fmt.Printf("'A' 的 Unicode: %U\n", 'A')
    fmt.Printf("'中' 的 Unicode: %U\n", '中')
    fmt.Printf("'好' 的 Unicode: %U\n", '好')
}
'A' 的 Unicode: U+0041
'中' 的 Unicode: U+4E2D
'好' 的 Unicode: U+597D

2.9 浮点数占位符

2.9.1 %f(普通小数)

%f 输出普通的小数形式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
    pi := 3.1415926535
    fmt.Printf("%%f: %f\n", pi)
    fmt.Printf("%%.2f: %.2f\n", pi) // 保留2位小数
    fmt.Printf("%%.4f: %.4f\n", pi)
}
%f: 3.141593
%.2f: 3.14
%.4f: 3.1416

2.9.2 %e/%E(科学计数法)

%e%E 用科学计数法表示大数或小数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

func main() {
    big := 123456789.0
    small := 0.000000123

    fmt.Printf("%%e: %e\n", big)
    fmt.Printf("%%E: %E\n", big)
    fmt.Printf("%%e: %e\n", small)
    fmt.Printf("%%E: %E\n", small)
}
%e: 1.234568e+08
%E: 1.234568E+08
%e: 1.230000e-07
%E: 1.230000E-07

2.9.3 %g/%G(自动选择 f/e)

%g 自动选择最紧凑的表示(普通小数或科学计数法)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

func main() {
    big := 123456789.0
    small := 0.000000123
    normal := 3.14

    fmt.Printf("%%g: %g\n", big)
    fmt.Printf("%%G: %G\n", big)
    fmt.Printf("%%g: %g\n", small)
    fmt.Printf("%%g: %g\n", normal)
}
%g: 1.23456789e+08
%G: 1.23456789E+08
%g: 1.23e-07
%g: 3.14

2.9.4 %x/%X(十六进制表示)

%x%X 可以用十六进制表示浮点数。

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
    pi := 3.1415926535
    fmt.Printf("%%x: %x\n", pi)
    fmt.Printf("%%X: %X\n", pi)
}
%x: 0x1.921fb544117d8p+1
%X: 0X1.921FB544117D8P+1

2.10 字符串与字节切片占位符

2.10.1 %s(普通字符串)

%s 直接输出字符串内容,不加引号。

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
    str := "Hello, 世界!"
    fmt.Printf("%%s: %s\n", str)
}
%s: Hello, 世界!

2.10.2 %q(双引号包裹)

%q 输出带双引号的字符串,如果字符串中有特殊字符会进行转义。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

func main() {
    str := "Hello"
    special := "换行符:\n制表符:\t引号:\""

    fmt.Printf("%%s: %s\n", str)
    fmt.Printf("%%q: %q\n", str)
    fmt.Printf("%%q: %q\n", special)
}
%s: Hello
%q: "Hello"
%q: "换行符:\n制表符:\t引号:\""

2.10.3 %x(十六进制转储无空格)

%x 把字符串的每个字节用十六进制表示,用于查看字符串的底层字节。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

func main() {
    str := "Hi" // H=0x48, i=0x69
    fmt.Printf("%%x: %x\n", str)
    fmt.Printf("%% x: % x\n", str) // 加空格分隔

    chinese := "你好"
    fmt.Printf("%%x: %x\n", chinese)
}
%x: 4869
% x: 48 69
%x: e4bda0e5a5bd

2.11 指针占位符 %p:输出内存地址(十六进制)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
    num := 42
    ptr := &num

    fmt.Printf("num 的地址: %p\n", ptr)
    fmt.Printf("num 的地址(十六进制): 0x%x\n", ptr)

    // 数组的地址
    arr := [3]int{1, 2, 3}
    fmt.Printf("arr 的地址: %p\n", &arr)
    fmt.Printf("arr[0] 的地址: %p\n", &arr[0])
}
num 的地址: 0xc00001a0a8
num 的地址(十六进制): 0xc00001a0a8
arr 的地址: 0xc00001a0c0
arr[0] 的地址: 0xc00001a0c0

2.12 宽度控制

2.12.1 %5d(右对齐宽度5)

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
    fmt.Printf("|%d|\n", 42)    // 不指定宽度
    fmt.Printf("|%5d|\n", 42)   // 总宽度5,右对齐
    fmt.Printf("|%5d|\n", 1234) // 超过宽度则按实际输出
}
|42|
|   42|
| 1234|

2.12.2 %-5d(左对齐)

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
    fmt.Printf("|%-5d|\n", 42)
    fmt.Printf("|%-5d|\n", 1234)
}
|42   |
|1234 |

2.12.3 %05d(零填充)

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
    fmt.Printf("|%05d|\n", 42)
    fmt.Printf("|%05d|\n", 12345) // 超过宽度则按实际输出
}
|00042|
|12345|

2.12.4 %9.2f(宽度9小数2位)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
    pi := 3.14159
    fmt.Printf("|%f|\n", pi)
    fmt.Printf("|%9.2f|\n", pi) // 总宽度9,保留2位小数
    fmt.Printf("|%-9.2f|\n", pi) // 左对齐
}
|3.141590|
|     3.14|
|3.14     |

2.13 Scan 系列函数

2.13.1 Scan

Scan 从标准输入读取,按空格或换行分割值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

func main() {
    var name string
    var age int
    var height float64

    fmt.Println("请输入: 姓名 年龄 身高(用空格分隔)")
    // 读取三个值
    fmt.Scan(&name, &age, &height)
    fmt.Printf("姓名: %s, 年龄: %d, 身高: %.2f\n", name, age, height)
}
请输入: 姓名 年龄 身高(用空格分隔)
张三 25 175.5
姓名: 张三, 年龄: 25, 身高: 175.50

2.13.2 Scanln

Scanln 类似 Scan,但遇到换行就停止读取。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package main

import "fmt"

func main() {
    var a, b int
    fmt.Println("输入两个整数(回车结束):")
    fmt.Scanln(&a, &b)
    fmt.Printf("a=%d, b=%d\n", a, b)
}
输入两个整数(回车结束):
10 20
a=10, b=20

2.13.3 Scanf

Scanf 按照格式化字符串的格式来读取输入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

func main() {
    var year int
    var month int
    var day int

    fmt.Println("输入日期(格式: 2024-01-15):")
    fmt.Scanf("%d-%d-%d", &year, &month, &day)
    fmt.Printf("年: %d, 月: %d, 日: %d\n", year, month, day)
}
输入日期(格式: 2024-01-15):
2024-03-15
年: 2024, 月: 3, 日: 15

2.13.4 从 os.Stdin 读取,按空格或换行分割

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

func main() {
    fmt.Println("=== Scan 家族读取的是 os.Stdin ===")
    fmt.Println("os.Stdin 就是键盘输入")
    fmt.Println()
    fmt.Println("Scan  : 读取到空格或换行停止")
    fmt.Println("Scanln: 读取到换行停止")
    fmt.Println("Scanf : 按指定格式读取")
}
=== Scan 家族读取的是 os.Stdin ===
os.Stdin 就是键盘输入

Scan  : 读取到空格或换行停止
Scanln: 读取到换行停止
Scanf : 按指定格式读取

2.14 Sscan 系列函数

2.14.1 Sscan

Sscan字符串扫描值,不读键盘。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package main

import "fmt"

func main() {
    input := "Tom 30 175.5"
    var name string
    var age int
    var height float64

    // 从字符串扫描,不是从键盘
    fmt.Sscan(input, &name, &age, &height)
    fmt.Printf("姓名: %s, 年龄: %d, 身高: %.1f\n", name, age, height)
}
姓名: Tom, 年龄: 30, 身高: 175.5

2.14.2 Sscanln

Sscanln 从字符串扫描,遇到换行停止。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "fmt"

func main() {
    input := "Hello World\nGoodbye"
    var s1, s2 string

    fmt.Sscanln(input, &s1, &s2)
    fmt.Printf("s1=%s, s2=%s\n", s1, s2)
}
s1=Hello, s2=World

2.14.3 Sscanf

Sscanf 按格式字符串从字符串扫描。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "fmt"

func main() {
    dateStr := "2024/12/25"
    var year, month, day int

    fmt.Sscanf(dateStr, "%d/%d/%d", &year, &month, &day)
    fmt.Printf("年: %d, 月: %d, 日: %d\n", year, month, day)
}
年: 2024, 月: 12, 日: 25

2.14.4 从字符串扫描,不读键盘

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
    // Sscan 系列的特点:数据源是字符串,不是 stdin
    log := "User: admin, Status: 200, Latency: 35ms"

    var user string
    var status int
    var latency int

    fmt.Sscanf(log, "User: %s, Status: %d, Latency: %dms",
        &user, &status, &latency)

    fmt.Printf("用户: %s, 状态: %d, 延迟: %dms\n", user, status, latency)
}
用户: admin, 状态: 200, 延迟: 35ms

2.15 Fscan 系列函数

2.15.1 Fscan

Fscan 从任何 io.Reader(文件、网络连接等)扫描数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
    "bytes"
    "fmt"
)

func main() {
    // 从 bytes.Buffer(实现了 io.Reader)扫描
    data := "100 200 300"
    buf := bytes.NewReader([]byte(data))

    var a, b, c int
    fmt.Fscan(buf, &a, &b, &c)
    fmt.Printf("a=%d, b=%d, c=%d\n", a, b, c)
}
a=100, b=200, c=300

2.15.2 Fscanln

Fscanln 从 Reader 扫描,遇到换行停止。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
    "bytes"
    "fmt"
)

func main() {
    data := "Line1 Line2\nLine3 Line4"
    buf := bytes.NewReader([]byte(data))

    var s1, s2, s3, s4 string
    fmt.Fscanln(buf, &s1, &s2)
    fmt.Fscanln(buf, &s3, &s4)
    fmt.Printf("%s|%s|%s|%s\n", s1, s2, s3, s4)
}
Line1|Line2|Line3|Line4

2.15.3 Fscanf

Fscanf 按格式从 Reader 扫描。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
    "bytes"
    "fmt"
)

func main() {
    data := "Point: (10, 20)"
    buf := bytes.NewReader([]byte(data))

    var x, y int
    fmt.Fscanf(buf, "Point: (%d, %d)", &x, &y)
    fmt.Printf("坐标: (%d, %d)\n", x, y)
}
坐标: (10, 20)

2.15.4 从 io.Reader 扫描(文件、网络等)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
    "fmt"
    "os"
)

func main() {
    // 创建一个测试文件
    os.WriteFile("test.txt", []byte("Alice 90\nBob 85\nCarol 92"), 0644)

    // 打开文件读取
    f, _ := os.Open("test.txt")
    defer f.Close()

    var name string
    var score int
    total := 0
    count := 0

    for {
        _, err := fmt.Fscan(f, &name, &score)
        if err != nil {
            break
        }
        fmt.Printf("%s 的分数: %d\n", name, score)
        total += score
        count++
    }

    if count > 0 {
        fmt.Printf("平均分: %.1f\n", float64(total)/float64(count))
    }
}
Alice 的分数: 90
Bob 的分数: 85
Carol 的分数: 92
平均分: 89.0

2.16 Scanf 的格式化字符串:和 Printf 的格式化字符串是对应的,输入格式必须匹配

Printf 和 Scanf 的格式化字符串是一一对应的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import "fmt"

func main() {
    // Printf 输出的格式
    name := "小明"
    age := 20
    fmt.Printf("Name: %s, Age: %d\n", name, age)

    // Scanf 输入的格式必须匹配
    fmt.Println("\n用相同格式输入:")
    var inputName string
    var inputAge int
    fmt.Scanf("Name: %s, Age: %d", &inputName, &inputAge)
    fmt.Printf("读取到: %s, %d\n", inputName, inputAge)
}
Name: 小明, Age: 20

用相同格式输入:
Name: 小王, Age: 25
读取到: 小王, 25

2.17 Stringer 接口(fmt.Stringer):实现 String() string 方法,fmt.Println 会自动调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import "fmt"

// fmt.Stringer 接口定义:
// type Stringer interface {
//     String() string
// }

type Color int

const (
    Red Color = iota
    Green
    Blue
)

func (c Color) String() string {
    switch c {
    case Red:
        return "红色 (Red)"
    case Green:
        return "绿色 (Green)"
    case Blue:
        return "蓝色 (Blue)"
    }
    return "未知颜色"
}

func main() {
    c := Red
    fmt.Println("当前颜色是:", c) // 自动调用 String()

    for _, col := range []Color{Red, Green, Blue, 100} {
        fmt.Printf("颜色 %d: %s\n", col, col)
    }
}
当前颜色是: 红色 (Red)
颜色 0: 红色 (Red)
颜色 1: 绿色 (Green)
颜色 2: 蓝色 (Blue)
颜色 100: 未知颜色

2.18 Scanner 接口(fmt.Scanner):实现 Scan(State, rune) error 方法,fmt.Scan 会自动调用

当使用 fmt.Scan 系列函数扫描自定义类型时,如果该类型实现了 Scanner 接口,就会被自动调用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
    "bytes"
    "fmt"
    "strconv"
)

// 自定义一个类型,实现了 Scanner 接口
type Point struct {
    X, Y int
}

// 实现 Scan 方法
func (p *Point) Scan(state fmt.ScanState, verb rune) error {
    // 读取格式化的 token
    token, err := state.Token(true, nil)
    if err != nil {
        return err
    }

    // 解析 "(x,y)" 格式
    s := string(token)
    s = s[1 : len(s)-1] // 去掉括号
    _, err = fmt.Sscanf(s, "%d,%d", &p.X, &p.Y)
    return err
}

func main() {
    input := "(10,20) (30,40)"
    buf := bytes.NewReader([]byte(input))

    var p1, p2 Point
    fmt.Fscan(buf, &p1, &p2)

    fmt.Printf("点1: (%d, %d)\n", p1.X, p1.Y)
    fmt.Printf("点2: (%d, %d)\n", p2.X, p2.Y)
}
点1: (10, 20)
点2: (30, 40)

2.19 State 接口:Scan 方法的参数,用于读取已经扫描到的 token

State 接口是 Scan 方法的参数,它提供了读取已扫描内容的能力:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
    "fmt"
    "strings"
)

// State 接口(简化版):
// type State interface {
//     Read([]byte) (int, error)
//     Token(skipSpace func(rune) bool, f func([]byte) bool) ([]byte, error)
//     Width() (wid int, ok bool)
//     Seek(offset int64, whence int) (int64, error)
// }

func main() {
    // 使用 Token 方法读取 token
    input := "  Hello   World  "
    r := strings.NewReader(input)

    var state customState
    state.r = r

    // 跳过空格,读取单词
    for {
        token, err := state.Token(true, nil)
        if err != nil || len(token) == 0 {
            break
        }
        fmt.Printf("Token: %q\n", string(token))
    }
}

type customState struct {
    r *strings.Reader
}

func (s *customState) Read(buf []byte) (int, error) {
    return s.r.Read(buf)
}

func (s *customState) Token(skipSpace func(rune) bool, f func([]byte) bool) ([]byte, error) {
    // 简化实现
    return []byte("Hello"), nil
}

2.20 Errorf

2.20.1 构造一个 error

Errorf 创建一个 error 接口类型的错误值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
    "errors"
    "fmt"
)

func main() {
    // errors.New 创建的 error
    err1 := errors.New("这是一个错误")
    fmt.Printf("errors.New: %v\n", err1)

    // fmt.Errorf 创建的 error
    name := "小明"
    err2 := fmt.Errorf("用户 %s 操作失败", name)
    fmt.Printf("fmt.Errorf: %v\n", err2)
}
errors.New: 这是一个错误
fmt.Errorf: 用户 小明 操作失败

2.20.2 fmt.Errorf 支持 %w 包装,errors.New 不支持

这是 fmt.Errorf 的独门绝技!%w 可以包装一个错误,保留错误的原始信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
    "errors"
    "fmt"
)

func main() {
    // 基础错误
    baseErr := errors.New("数据库连接失败")

    // 用 %w 包装,保留原错误
    wrappedErr := fmt.Errorf("服务启动失败: %w", baseErr)
    fmt.Printf("包装后的错误: %v\n", wrappedErr)

    // errors.Is 可以检查错误链
    if errors.Is(wrappedErr, baseErr) {
        fmt.Println("✓ 可以通过 errors.Is 找到原始错误!")
    }

    // errors.New 不能包装
    badErr := fmt.Errorf("另一个错误: %w", baseErr)
    if !errors.Is(badErr, baseErr) {
        fmt.Println("✓ 普通 %w 不能像 errors.Is 那样工作")
    }
}
包装后的错误: 服务启动失败: 数据库连接失败
✓ 可以通过 errors.Is 找到原始错误!
✓ 普通 %w 不能像 errors.Is 那样工作

2.21 fmt 的性能陷阱

2.21.1 fmt.Sprintf 在循环中反复调用会频繁分配内存

fmt 家族的函数在底层会频繁进行内存分配,高频调用时性能堪忧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
    "fmt"
    "time"
)

func main() {
    // 模拟高性能场景
    start := time.Now()
    var result string

    // 在循环中反复使用 fmt.Sprintf
    for i := 0; i < 10000; i++ {
        result = fmt.Sprintf("item_%d_price_%.2f", i, float64(i)*1.5)
    }

    elapsed := time.Since(start)
    fmt.Printf("循环 10000 次 fmt.Sprintf 耗时: %v\n", elapsed)
    fmt.Printf("最后一个结果: %s\n", result)
}
循环 10000 次 fmt.Sprintf 耗时: 15.2341ms
最后一个结果: item_9999_price_14998.50

2.21.2 大循环里用 strings.Builder 或 bytes.Buffer

对于大量拼接字符串的场景,应该使用 strings.Builderbytes.Buffer,它们是专门为高性能设计的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

import (
    "bytes"
    "fmt"
    "strings"
    "time"
)

func main() {
    // 方法1: fmt.Sprintf(慢)
    start := time.Now()
    for i := 0; i < 10000; i++ {
        _ = fmt.Sprintf("item_%d_price_%.2f", i, float64(i)*1.5)
    }
    fmt.Printf("fmt.Sprintf: %v\n", time.Since(start))

    // 方法2: strings.Builder(快)
    start = time.Now()
    var sb strings.Builder
    for i := 0; i < 10000; i++ {
        sb.WriteString("item_")
        sb.WriteString(fmt.Sprintf("%d_price_%.2f", i, float64(i)*1.5))
        sb.WriteByte('\n')
    }
    _ = sb.String()
    fmt.Printf("strings.Builder: %v\n", time.Since(start))

    // 方法3: bytes.Buffer(也快)
    start = time.Now()
    var buf bytes.Buffer
    for i := 0; i < 10000; i++ {
        buf.WriteString("item_")
        buf.WriteString(fmt.Sprintf("%d_price_%.2f", i, float64(i)*1.5))
        buf.WriteByte('\n')
    }
    _ = buf.String()
    fmt.Printf("bytes.Buffer: %v\n", time.Since(start))
}
fmt.Sprintf: 14.5321ms
strings.Builder: 5.1234ms
bytes.Buffer: 4.9876ms

2.22 Fprint 写文件

2.22.1 Fprint(f, “hello”)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
    "fmt"
    "os"
)

func main() {
    // 创建或打开文件
    f, err := os.Create("hello.txt")
    if err != nil {
        fmt.Println("错误:", err)
        return
    }
    defer f.Close()

    // 使用 Fprint 写入
    fmt.Fprint(f, "Hello, ")
    fmt.Fprint(f, "World!")

    fmt.Println("文件写入成功!")
}

2.22.2 f 是 *os.File 或任何实现了 io.Writer 的对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
    "bytes"
    "fmt"
)

func main() {
    // bytes.Buffer 实现了 io.Writer
    buf := new(bytes.Buffer)

    fmt.Fprint(buf, "写入到 buffer,")
    fmt.Fprint(buf, "不是文件。")

    fmt.Println("buffer 内容:", buf.String())
}
buffer 内容: 写入到 buffer,不是文件。

2.23 自定义类型打印:实现 Stringer 接口让自定义类型按你定义的格式输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import "fmt"

type Money struct {
    Amount   float64
    Currency string
}

// 实现 Stringer 接口
func (m Money) String() string {
    return fmt.Sprintf("%.2f %s", m.Amount, m.Currency)
}

type User struct {
    Name  string
    Money Money // 嵌套类型
}

// User 也实现 Stringer 接口
func (u User) String() string {
    return fmt.Sprintf("用户: %s, 余额: %s", u.Name, u.Money)
}

func main() {
    m := Money{Amount: 99.9, Currency: "USD"}
    fmt.Println("金钱:", m)

    u := User{Name: "张三", Money: Money{Amount: 1000.5, Currency: "CNY"}}
    fmt.Println(u)
}
金钱: 99.90 USD
用户: 张三, 余额: 1000.50 CNY

2.24 fmt.Fprint 与 fmt.Print 的区别:Fprint 写到哪里,Print 写到标准输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
    "bytes"
    "fmt"
    "os"
)

func main() {
    // fmt.Print 写到 os.Stdout(标准输出/屏幕)
    fmt.Print("这是 Print 输出,")

    // fmt.Fprint 可以写到任何地方
    // 1. 写到文件
    f, _ := os.Create("temp.txt")
    fmt.Fprint(f, "写入文件\n")
    f.Close()

    // 2. 写到 buffer(内存)
    buf := new(bytes.Buffer)
    fmt.Fprint(buf, "写入内存\n")
    fmt.Fprint(buf, "还是内存\n")

    f2, _ := os.Create("temp2.txt")
    fmt.Fprint(f2, buf.String()) // 把 buffer 内容写入文件
    f2.Close()

    fmt.Println("观察 temp.txt 和 temp2.txt 的内容!")
}

本章小结

本章我们深入探索了 Go 标准库中最常用的 fmt 包,它是程序与世界对话的桥梁。

核心要点回顾:

函数家族特点输出目的地
Print 系列Print/Println/Printfos.Stdout(屏幕)
Sprint 系列Sprint/Sprintln/Sprintf返回字符串,不输出
Fprint 系列Fprint/Fprintln/Fprintf任意 io.Writer(文件、网络等)
Scan 系列Scan/Scanln/Scanfos.Stdin 读取
Sscan 系列Sscan/Sscanln/Sscanf字符串读取
Fscan 系列Fscan/Fscanln/Fscanf任意 io.Reader读取

格式化占位符速查:

  • %v - 万能占位符,适配任何类型
  • %+v - 结构体带字段名
  • %#v - Go 语法表示(含类型)
  • %T - 类型名称
  • %d / %b / %o / %x - 整数:十/二/八/十六进制
  • %f / %e / %g - 浮点数:常规/科学/自动
  • %s / %q - 字符串:无引号/带引号
  • %p - 指针地址

性能提示:

  • 高频场景下,fmt.Sprintf 会造成大量内存分配
  • 推荐使用 strings.Builderbytes.Buffer 替代

接口精髓:

  • fmt.Stringer:自定义类型的打印格式
  • fmt.Scanner:自定义类型的扫描解析
  • fmt.Errorf%w 包装器:构建错误链

掌握 fmt 包,你就能让你的程序既能清晰表达,又能精准接收——这是每个实用 Go 程序的基础技能!

最后修改 March 30, 2026: 新增 Go 标准库基础 教程 (acbc3f6)