Go 从 0 到精通 · 第 03 课:输入输出与类型转换

学习定位:这是整套 Go 教程的第 3 课。
前置要求:已经完成第 2 课,理解了变量声明(var:=)、常量(const)、零值机制和常见基础数据类型。
本课目标:掌握控制台输出的三种方式、格式化动词、控制台输入读取,以及 Go 中显式类型转换的规则和常见用法。


1. 本课你要解决的核心问题

这一课的重点是让你的程序从"只能输出固定文字"变成"能和用户交互"。

你需要搞明白以下几个关键问题:

  • fmt.Printfmt.Printlnfmt.Printf 到底有什么区别
  • 格式化动词是什么意思,常见的有哪些
  • 怎么从控制台读取用户输入
  • 为什么 Go 的类型转换必须显式进行
  • intfloat64string 之间怎么互相转换
  • 转换过程中有哪些常见坑

把这些搞清楚,你写出来的程序就不再只是"输出一句话",而是一个能和用户对话的小程序了。


2. 控制台输出:三种方式的区别

Go 中最常用的输出函数有三个,都来自 fmt 包:

  • fmt.Print
  • fmt.Println
  • fmt.Printf

它们看起来很像,但行为不同。你必须把它们分清楚。


2.1 fmt.Print:原样输出,不换行

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
fmt.Print("你好")
fmt.Print("世界")
}

输出:

1
你好世界

特点:

  • 输出内容后不会自动换行
  • 多次调用时,内容会紧紧连在一起

什么时候用:

  • 你需要精确控制输出格式
  • 不希望自动添加换行符

2.2 fmt.Println:输出后自动换行

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
fmt.Println("你好")
fmt.Println("世界")
}

输出:

1
2
你好
世界

特点:

  • 输出内容后会自动加一个换行符
  • 如果传入多个参数,参数之间会自动加空格

例如:

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("姓名:", "Tom", "年龄:", 18)
}

输出:

1
姓名: Tom 年龄: 18

这是日常开发中用得最多的输出函数。


2.3 fmt.Printf:格式化输出

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
name := "Tom"
age := 18
fmt.Printf("我叫 %s,今年 %d 岁\n", name, age)
}

输出:

1
我叫 Tom,今年 18 

特点:

  • 第一个参数是格式化字符串,里面用 % 开头的"占位符"表示变量的位置
  • 后面的参数按顺序填入占位符
  • 不会自动换行,需要手动加 \n

这是你想精确控制输出格式时最强大的工具。


2.4 一句话区分三者

  • fmt.Print:原样输出,不换行
  • fmt.Println:输出后换行,多参数之间加空格
  • fmt.Printf:按格式化模板输出,不自动换行

3. 格式化动词详解

fmt.Printf 的核心在于格式化动词。你不需要一次性全记住,但下面这些必须先掌握。


3.1 常用格式化动词

动词 含义 示例值 输出结果
%d 十进制整数 18 18
%f 浮点数 3.14 3.140000
%s 字符串 "Tom" Tom
%t 布尔值 true true
%v 通用格式(自动判断) 任意 值的默认表示
%T 打印变量的类型 18 int
%% 输出一个字面的 % %

3.2 综合示例

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

import "fmt"

func main() {
name := "小明"
age := 20
score := 95.5
passed := true

fmt.Printf("姓名:%s\n", name)
fmt.Printf("年龄:%d\n", age)
fmt.Printf("分数:%f\n", score)
fmt.Printf("是否通过:%t\n", passed)
}

输出:

1
2
3
4
姓名:小明
年龄:20
分数:95.500000
是否通过:true

3.3 控制浮点数精度

默认情况下 %f 会输出 6 位小数。如果你想控制小数位数,可以这样写:

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

import "fmt"

func main() {
price := 19.99
fmt.Printf("价格:%.2f\n", price)
fmt.Printf("价格:%.1f\n", price)
fmt.Printf("价格:%.0f\n", price)
}

输出:

1
2
3
价格:19.99
价格:20.0
价格:20

格式说明:

  • %.2f 表示保留 2 位小数
  • %.1f 表示保留 1 位小数
  • %.0f 表示不要小数部分

这在输出金额、百分比等场景非常实用。


3.4 %v%T:两个万能工具

当你不确定用什么格式化动词时,%v 是一个通用选择:

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

import "fmt"

func main() {
name := "Tom"
age := 18
price := 99.9
ok := true

fmt.Printf("name = %v\n", name)
fmt.Printf("age = %v\n", age)
fmt.Printf("price = %v\n", price)
fmt.Printf("ok = %v\n", ok)
}

%v 会根据值的类型自动选择合适的输出方式。

%T 可以打印变量的类型,这在调试时非常有用:

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

import "fmt"

func main() {
a := 18
b := 3.14
c := "hello"
d := true

fmt.Printf("a 的类型是 %T\n", a)
fmt.Printf("b 的类型是 %T\n", b)
fmt.Printf("c 的类型是 %T\n", c)
fmt.Printf("d 的类型是 %T\n", d)
}

输出:

1
2
3
4
a 的类型是 int
b 的类型是 float64
c 的类型是 string
d 的类型是 bool

建议你在学习阶段经常用 %T 来确认变量的实际类型,这对加深理解非常有帮助。


4. fmt.Sprintf:格式化但不输出

有时候你想把格式化的结果保存到一个字符串里,而不是直接打印出来。

这时候用 fmt.Sprintf

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

import "fmt"

func main() {
name := "Tom"
age := 18

info := fmt.Sprintf("姓名:%s,年龄:%d", name, age)
fmt.Println(info)
}

输出:

1
姓名:Tom,年龄:18

SprintfPrintf 的区别:

  • Printf:格式化后直接打印到控制台
  • Sprintf:格式化后返回字符串,不打印

这在拼接日志、构造消息文本时非常常用。


5. 控制台输入:让程序"听"用户说话

前面两课你的程序都是"自说自话"。从这一节开始,你的程序可以接收用户输入了。


5.1 fmt.Scan:最基本的输入方式

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

import "fmt"

func main() {
var name string
fmt.Print("请输入你的名字:")
fmt.Scan(&name)
fmt.Println("你好,", name)
}

运行时,程序会等你在控制台输入内容,按回车后继续执行。

例如你输入 Tom,输出就是:

1
2
请输入你的名字:Tom
你好, Tom

5.2 关于 & 符号

你注意到 fmt.Scan(&name) 里有一个 &

这个 & 表示取变量的地址。你可以先这样理解:

fmt.Scan 需要知道"把用户输入的值放到哪里",所以你要告诉它变量的地址。

如果不加 &fmt.Scan 拿不到变量地址,就无法把输入值写入变量。

指针的详细内容我们后面专门有一课讲,这里你只需要记住:

fmt.Scan 读取输入时,变量前面要加 &


5.3 读取多个输入

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

import "fmt"

func main() {
var name string
var age int

fmt.Print("请输入姓名和年龄(空格分隔):")
fmt.Scan(&name, &age)

fmt.Printf("你好 %s,你今年 %d 岁\n", name, age)
}

运行时输入:

1
Tom 18

输出:

1
你好 Tom,你今年 18 

fmt.Scan 默认以空格或换行作为分隔符。


5.4 fmt.Scanf:按格式读取

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

import "fmt"

func main() {
var name string
var age int

fmt.Print("请输入(格式:姓名 年龄):")
fmt.Scanf("%s %d", &name, &age)

fmt.Printf("你好 %s,你今年 %d 岁\n", name, age)
}

fmt.Scanffmt.Printf 类似,第一个参数是格式化模板。

不过在实际开发中,fmt.Scanf 用得相对少一些,因为控制台输入通常比较简单,fmt.Scanfmt.Scanln 已经够用。


5.5 fmt.Scanln:读取一行输入

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

import "fmt"

func main() {
var name string

fmt.Print("请输入你的名字:")
fmt.Scanln(&name)

fmt.Println("你好,", name)
}

fmt.Scanlnfmt.Scan 很像,区别在于:

  • fmt.Scan:以空格或换行分隔
  • fmt.Scanln:遇到换行符就停止读取

在只需要读一行简单输入的场景下,fmt.Scanln 更直观。


5.6 三种输入函数对比

函数 分隔方式 适用场景
fmt.Scan 空格或换行 读取多个值,最通用
fmt.Scanln 换行 读取一行输入
fmt.Scanf 按格式模板 需要严格格式匹配

6. 一个完整的交互式小程序

把前面学到的输出和输入结合起来,写一个完整的交互程序:

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

import "fmt"

func main() {
var name string
var age int
var city string

fmt.Print("请输入你的姓名:")
fmt.Scanln(&name)

fmt.Print("请输入你的年龄:")
fmt.Scanln(&age)

fmt.Print("请输入你的城市:")
fmt.Scanln(&city)

fmt.Println("------- 个人信息 -------")
fmt.Printf("姓名:%s\n", name)
fmt.Printf("年龄:%d\n", age)
fmt.Printf("城市:%s\n", city)
fmt.Printf("类型检查 → 姓名:%T 年龄:%T 城市:%T\n", name, age, city)
}

运行效果:

1
2
3
4
5
6
7
8
请输入你的姓名:小明
请输入你的年龄:20
请输入你的城市:北京
------- 个人信息 -------
姓名:小明
年龄:20
城市:北京
类型检查 → 姓名:string 年龄:int 城市:string

这就是一个有输入、有输出、有格式化的完整小程序。


7. 类型转换:Go 的显式原则

从这一节开始,我们进入本课的另一个重点:类型转换。


7.1 为什么 Go 的类型转换必须显式

在某些语言里,不同数值类型之间可以自动转换。例如把一个整数赋给浮点变量,编译器会"悄悄"帮你做。

但 Go 不会。

Go 的原则是:

类型转换必须由你显式写出来,编译器不会替你做隐式转换。

这样做的好处是:

  • 代码意图更清楚
  • 不容易出现精度丢失等隐蔽问题
  • 团队协作时更容易审查

7.2 基本语法

Go 的类型转换语法是:

1
目标类型(要转换的值)

例如:

1
2
3
float64(age)
int(price)
string(65)

注意,这不是函数调用,而是 Go 的类型转换表达式。


8. 整数与浮点数之间的转换

这是最常见的类型转换场景。


8.1 intfloat64

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

import "fmt"

func main() {
age := 18
ageFloat := float64(age)

fmt.Println(ageFloat)
fmt.Printf("类型:%T\n", ageFloat)
}

输出:

1
2
18
类型:float64

这种转换是安全的,不会丢失信息。


8.2 float64int

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

import "fmt"

func main() {
price := 19.99
priceInt := int(price)

fmt.Println(priceInt)
fmt.Printf("类型:%T\n", priceInt)
}

输出:

1
2
19
类型:int

注意:这里 19.99 变成了 19,小数部分被直接截断,不是四舍五入。

这是一个非常重要的特点:

float64int 时,小数部分会被截断丢弃。

如果你需要四舍五入,需要自己处理,例如先加 0.5 再转换,或者使用 math.Round


8.3 不同整数类型之间的转换

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

import "fmt"

func main() {
var a int = 100
var b int64 = int64(a)
var c int32 = int32(a)

fmt.Println(a, b, c)
}

即使 intint64 看起来很相似,Go 也不允许你直接混用,必须显式转换。

这是 Go 严格类型系统的体现。


9. 字符串与数字之间的转换

这是初学者最容易搞错的部分。

关键前提:字符串和数字之间的转换,不能直接用 int()string() 完成。你需要使用 strconv 包。


9.1 为什么不能直接用 string(65)

先看一个很多新手会踩的坑:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
n := 65
s := string(n)
fmt.Println(s)
}

你可能期望输出 "65",但实际输出是:

1
A

原因是:string(65) 的含义是"把数字 65 当成 Unicode 码点,转换成对应的字符"。65 对应的字符是 A

这不是 bug,而是 Go 的设计。

所以,把数字变成字符串,不能用 string(),要用 strconv 包。


9.2 整数转字符串:strconv.Itoa

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

import (
"fmt"
"strconv"
)

func main() {
age := 18
ageStr := strconv.Itoa(age)

fmt.Println("年龄是:" + ageStr)
fmt.Printf("类型:%T\n", ageStr)
}

输出:

1
2
年龄是:18
类型:string

Itoa 的名字来自 “Integer to ASCII”,是整数转字符串的标准方式。


9.3 字符串转整数:strconv.Atoi

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

import (
"fmt"
"strconv"
)

func main() {
s := "42"
n, err := strconv.Atoi(s)

if err != nil {
fmt.Println("转换失败:", err)
return
}

fmt.Println("数字是:", n)
fmt.Printf("类型:%T\n", n)
}

输出:

1
2
数字是: 42
类型:int

注意 strconv.Atoi 返回两个值:

  • 第一个是转换结果
  • 第二个是错误信息

如果输入的字符串不是合法数字,err 就不会是 nil

例如:

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

import (
"fmt"
"strconv"
)

func main() {
s := "abc"
n, err := strconv.Atoi(s)

if err != nil {
fmt.Println("转换失败:", err)
return
}

fmt.Println(n)
}

输出:

1
转换失败: strconv.Atoi: parsing "abc": invalid syntax

这就是 Go 显式错误处理的体现。你在上一课预告里已经知道 Go 喜欢"把错误暴露出来",这里就是一个典型例子。


9.4 浮点数与字符串之间的转换

浮点数转字符串用 strconv.FormatFloat,字符串转浮点数用 strconv.ParseFloat

浮点数转字符串

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

import (
"fmt"
"strconv"
)

func main() {
price := 19.99
priceStr := strconv.FormatFloat(price, 'f', 2, 64)

fmt.Println("价格是:" + priceStr)
fmt.Printf("类型:%T\n", priceStr)
}

输出:

1
2
价格是:19.99
类型:string

FormatFloat 的参数说明:

  • 第 1 个参数:要转换的浮点数
  • 第 2 个参数:格式,'f' 表示普通小数格式
  • 第 3 个参数:精度(小数位数),-1 表示自动
  • 第 4 个参数:位数,64 表示 float64

初学阶段你可以先记住这个固定写法,后面用多了自然就熟了。

不过在简单场景下,用 fmt.Sprintf 也可以达到类似效果:

1
priceStr := fmt.Sprintf("%.2f", price)

这种写法更简洁,实际开发中也很常用。

字符串转浮点数

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

import (
"fmt"
"strconv"
)

func main() {
s := "3.14"
f, err := strconv.ParseFloat(s, 64)

if err != nil {
fmt.Println("转换失败:", err)
return
}

fmt.Println("数值是:", f)
fmt.Printf("类型:%T\n", f)
}

输出:

1
2
数值是: 3.14
类型:float64

ParseFloat 的第二个参数 64 表示结果精度为 float64


9.5 布尔值与字符串之间的转换

虽然不算特别常用,但了解一下也有好处。

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

import (
"fmt"
"strconv"
)

func main() {
// bool 转 string
s := strconv.FormatBool(true)
fmt.Println(s)

// string 转 bool
b, err := strconv.ParseBool("true")
if err != nil {
fmt.Println("转换失败:", err)
return
}
fmt.Println(b)
}

输出:

1
2
true
true

ParseBool 能识别的字符串包括 "true""false""1""0" 等。


10. 类型转换速查表

把本课涉及的常见转换整理成一张表:

转换方向 方法 示例
intfloat64 float64(n) float64(18)
float64int int(f) int(19.99)19
intint64 int64(n) int64(100)
intstring strconv.Itoa(n) strconv.Itoa(18)"18"
stringint strconv.Atoi(s) strconv.Atoi("42")42, nil
float64string strconv.FormatFloatfmt.Sprintf fmt.Sprintf("%.2f", 19.99)
stringfloat64 strconv.ParseFloat(s, 64) strconv.ParseFloat("3.14", 64)
boolstring strconv.FormatBool(b) strconv.FormatBool(true)"true"
stringbool strconv.ParseBool(s) strconv.ParseBool("true")true, nil

11. 常见坑总结

11.1 Printf 忘记加 \n

1
2
fmt.Printf("你好 %s", name)
fmt.Printf("年龄 %d", age)

输出会挤在一行:

1
你好 Tom年龄 18

Printf 不会自动换行,必须手动加 \n


11.2 格式化动词和变量类型不匹配

1
2
age := 18
fmt.Printf("年龄:%s\n", age)

%s 是字符串格式化动词,但 ageint。虽然程序可能不会直接崩溃,但输出结果会不正确。

正确写法:

1
fmt.Printf("年龄:%d\n", age)

建议:不确定的时候用 %v,它是通用的。


11.3 string(数字) 不是数字转字符串

这一点前面已经重点讲过。再强调一次:

1
string(65)  // 结果是 "A",不是 "65"

数字转字符串,用 strconv.Itoafmt.Sprintf


11.4 Atoi 的返回值不能忽略错误

1
n, _ := strconv.Atoi(someInput)

虽然用 _ 可以忽略错误,但在实际开发中这样做很危险。如果输入不是合法数字,n 会是 0,你的程序可能会用一个错误的值继续运行。

正确做法是检查 err

1
2
3
4
5
n, err := strconv.Atoi(someInput)
if err != nil {
fmt.Println("输入不合法")
return
}

11.5 float64int 是截断不是四舍五入

1
2
int(2.9)   // 结果是 2,不是 3
int(-1.7) // 结果是 -1,不是 -2

如果你需要四舍五入,可以这样做:

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

import (
"fmt"
"math"
)

func main() {
f := 2.6
rounded := int(math.Round(f))
fmt.Println(rounded)
}

输出:

1
3

11.6 Scan 读取输入时忘记加 &

1
2
var name string
fmt.Scan(name) // 错误:缺少 &

正确写法:

1
fmt.Scan(&name)

没有 &Scan 无法把值写入变量。


12. 一段综合示例

把本课所有核心知识串到一个程序里:

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
package main

import (
"fmt"
"strconv"
)

func main() {
// 输出演示
fmt.Print("=== ")
fmt.Print("Go 第 3 课")
fmt.Println(" ===")

// 输入
var name string
var ageStr string

fmt.Print("请输入你的姓名:")
fmt.Scanln(&name)

fmt.Print("请输入你的年龄:")
fmt.Scanln(&ageStr)

// 字符串转整数
age, err := strconv.Atoi(ageStr)
if err != nil {
fmt.Println("年龄输入不合法:", err)
return
}

// 格式化输出
fmt.Printf("你好 %s,你今年 %d 岁\n", name, age)

// 类型转换演示
ageFloat := float64(age)
fmt.Printf("你的年龄转为浮点数是:%.1f\n", ageFloat)

// 整数转字符串
ageBack := strconv.Itoa(age)
fmt.Println("年龄转回字符串:" + ageBack)

// 类型检查
fmt.Printf("name 类型:%T,age 类型:%T,ageFloat 类型:%T\n", name, age, ageFloat)
}

运行效果:

1
2
3
4
5
6
7
=== Go 第 3 课 ===
请输入你的姓名:小明
请输入你的年龄:20
你好 小明,你今年 20 岁
你的年龄转为浮点数是:20.0
年龄转回字符串:20
name 类型:string,age 类型:int,ageFloat 类型:float64

13. 本课练习

一定要亲手写,不要只看。

练习 1:三种输出方式对比

要求:

  • 分别用 fmt.Printfmt.Printlnfmt.Printf 输出同一句话
  • 观察输出结果的区别

目的:直观感受三者的差异。


练习 2:格式化输出个人信息

要求:

  • 定义姓名、年龄、身高(浮点数)、是否在职(布尔值)
  • fmt.Printf 格式化输出,身高保留 1 位小数
  • %T 输出每个变量的类型

练习 3:写一个简单计算器

要求:

  • 从控制台读取两个整数
  • 打印它们的和、差、积
  • 使用 fmt.Printf 格式化输出

例如输入 103,输出:

1
2
3
10 + 3 = 13
10 - 3 = 7
10 * 3 = 30

练习 4:体验 string(数字) 的陷阱

要求:

  • string(72) 看看输出是什么
  • 再用 strconv.Itoa(72) 看看输出是什么
  • 对比两者,自己总结区别

练习 5:字符串和数字互转

要求:

  • 定义一个字符串 "100"
  • 把它转成整数
  • 加上 50
  • 再把结果转回字符串
  • 输出最终结果

重点体会:为什么 Go 不允许直接用 "100" + 50


练习 6:浮点数截断体验

要求:

  • 定义一个 float64 变量,值为 9.99
  • 把它转成 int
  • 打印结果,观察是否是四舍五入
  • 然后用 math.Round 实现四舍五入,再转成 int,对比结果

14. 自测题

不看文档,试着回答:

14.1 概念题

  1. fmt.Printfmt.Printlnfmt.Printf 的区别是什么?
  2. %d%f%s%t%v%T 分别表示什么?
  3. %.2f 是什么意思?
  4. fmt.Sprintffmt.Printf 有什么区别?
  5. fmt.Scan 里为什么变量前面要加 &
  6. 为什么 Go 不允许隐式类型转换?
  7. string(65) 的结果是什么?为什么?
  8. strconv.Atoi 为什么返回两个值?
  9. float64int 时小数部分会怎样?
  10. 整数转字符串应该用什么函数?

如果你能流畅回答这些问题,说明这一课你已经真正理解了。


15. 本课总结

这一课你学到的不只是几个函数名,而是 Go 输入输出和类型系统的核心逻辑。

你现在应该已经理解:

  • fmt.Print 不换行,fmt.Println 换行,fmt.Printf 按格式输出
  • 格式化动词是 Printf 的核心,%d 整数、%f 浮点、%s 字符串、%v 通用、%T 类型
  • fmt.Sprintf 返回格式化字符串而不打印
  • fmt.Scanfmt.Scanlnfmt.Scanf 用于从控制台读取输入
  • Go 的类型转换必须显式,编译器不会替你做隐式转换
  • 数值类型之间直接用 目标类型(值) 转换
  • 字符串和数字之间的转换要用 strconv
  • string(数字) 不是数字转字符串,而是转成 Unicode 字符
  • AtoiParseFloat 都会返回错误值,必须处理

这一课内容比较多,但每一块都非常实用。从这一课开始,你已经能写出有输入、有输出、有格式化、有类型转换的完整小程序了。


16. 下一课预告

下一课我们进入:条件判断 ifswitch

会重点讲:

  • if 的基本用法和 Go 的特殊写法
  • if 初始化语句是什么
  • else ifelse 的使用
  • switch 的基本用法和它与其他语言的区别
  • 为什么 Go 的 switch 不需要 break
  • 条件判断中的常见坑

学完下一课,你的程序就能"做决定"了,不再是从头到尾直线执行。